Python pandas で日時関連のデータ操作をカンタンに
概要
Python で日時/タイムスタンプ関連の操作をする場合は dateutil
や arrow
を使っている人が多いと思うが、 pandas
でもそういった処理がわかりやすく書けるよ、という話。
pandas
の本領は多次元データの蓄積/変形/集約処理にあるが、日時操作に関連した強力なメソッド / ユーティリティもいくつか持っている。今回は それらを使って日時操作を簡単に行う方法を書いてく。ということで DataFrame
も Series
もでてこない pandas
記事のはじまり。
※ ここでいう "日時/タイムスタンプ関連の操作" は文字列パース、日時加算/減算、タイムゾーン設定、条件に合致する日時のリスト生成などを想定。時系列補間/リサンプリングなんかはまた膨大になるので別途。
インストール
以下サンプルには 0.15での追加機能も含まれるため、0.15 以降が必要。
pip install pandas
準備
import pandas as pd
初期化/文字列パース
pd.to_datetime
が便利。返り値は Python 標準のdatetime.datetime
クラスを継承したpd.Timestamp
インスタンスになる。
dt = pd.to_datetime('2014-11-09 10:00') dt # Timestamp('2014-11-09 10:00:00') type(dt) # pandas.tslib.Timestamp import datetime isinstance(dt, datetime.datetime) # True
pd.to_datetime
は、まず pandas
独自の日時パース処理を行い、そこでパースできなければ dateutil.parser.parse
を呼び出す。そのため結構無茶なフォーマットもパースできる。
pd.to_datetime('141109 1005') # Timestamp('2014-11-09 10:05:00')
リスト-likeなものを渡すと DatetimeIndex
( pandas
を 普段 使ってない方は 日時のリストみたいなものだと思ってください) が返ってくる。
pd.to_datetime(['2014-11-09 10:00', '2014-11-10 10:00']) # <class 'pandas.tseries.index.DatetimeIndex'> # [2014-11-09 10:00:00, 2014-11-10 10:00:00] # Length: 2, Freq: None, Timezone: None
また、pd.to_datetime
は文字列以外の 日付-like なものも処理できる。自分は numpy.datetime64
を datetime.datetime
に変換するのに使ったりもする。
pd.to_datetime(datetime.datetime(2014, 11, 9)) # Timestamp('2014-11-09 00:00:00') import numpy as np pd.to_datetime(np.datetime64('2014-11-09 10:00Z')) # Timestamp('2014-11-09 10:00:00')
補足 Timestamp.to_datetime()
で Timestamp
-> datetime.datetime
へ変換できる
dt.to_datetime()
# datetime.datetime(2014, 11, 9, 10, 0)
日時の加算/減算
pd.Timestamp
に対して datetime.timedelta
, dateutil.relativedelta
を使って日時の加減算を行うこともできるが、
dt + datetime.timedelta(days=1) # Timestamp('2014-11-10 10:00:00') from dateutil.relativedelta import relativedelta dt + relativedelta(days=1) # Timestamp('2014-11-10 10:00:00')
一般的な時間単位は pandas
が offsets
として定義しているため、そちらを使ったほうが直感的だと思う。使えるクラスの一覧は こちら。
dt # Timestamp('2014-11-09 10:00:00') import pandas.tseries.offsets as offsets # 翌日 dt + offsets.Day() # Timestamp('2014-11-10 10:00:00') # 3日後 dt + offsets.Day(3) # Timestamp('2014-11-12 10:00:00')
pd.offsets
を使うメリットとして、例えば dateutil.relativedelta
では days
と day
を間違えるとまったく違う意味になってしまう。が、pd.offsets
ならより明瞭に書ける。
# relativedelta の場合 days なら 3日後 dt + relativedelta(days=3) # Timestamp('2014-11-12 10:00:00') # day なら月の 3日目 dt + relativedelta(day=3) # Timestamp('2014-11-03 10:00:00') # 月の3日目を取りたいならこっちのが明瞭 dt - offsets.MonthBegin() + offsets.Day(2) # Timestamp('2014-11-03 10:00:00')
時刻部分を丸めたければ normalize=True
。
dt + offsets.MonthEnd(normalize=True) # Timestamp('2014-11-30 00:00:00')
また、 pd.offsets
は datetime.datetime
, np.datetime64
にも適用できる。
datetime.datetime(2014, 11, 9, 10, 00) + offsets.Week(weekday=2) # Timestamp('2014-11-12 10:00:00')
※ np.datetime64
に対して適用する場合は 加算/減算オペレータではなく、pd.offsets
インスタンスの .apply
メソッドを使う。.apply
は Timestamp
/ datetime
にも使える。(というか加減算オペレータも裏側では .apply
を使っている)。
offsets.Week(weekday=2).apply(np.datetime64('2014-11-09 10:00:00Z')) # Timestamp('2014-11-12 10:00:00') offsets.Week(weekday=2).apply(dt) # Timestamp('2014-11-12 10:00:00')
ある日付が offsets
に乗っているかどうか調べる場合は .onOffset
。例えば ある日付が水曜日 ( weekday=2
) かどうか調べたければ、
# 11/09は日曜日 ( weekday=6 ) dt # Timestamp('2014-11-09 10:00:00') pd.offsets.Week(weekday=2).onOffset(dt) # False pd.offsets.Week(weekday=2).onOffset(dt + offsets.Day(3)) # True
タイムゾーン
タイムゾーン関連の処理は tz_localize
と tz_convert
二つのメソッドで行う。引数は 文字列、もしくは pytz
, dateutil.tz
インスタンス (後述)。
上でつくった Timestamp
について、現在の表示時刻 (10:00) をそのままにして タイムゾーンを付与する場合は tz_localize
。
dt # Timestamp('2014-11-09 10:00:00') dt.tz_localize('Asia/Tokyo') # Timestamp('2014-11-09 10:00:00+0900', tz='Asia/Tokyo')
現在の表示時刻を世界標準時 (GMTで10:00) とみて タイムゾーン付与する場合は まず GMT に tz_localize
してから tz_convert
。
dt.tz_localize('GMT').tz_convert('Asia/Tokyo') # Timestamp('2014-11-09 19:00:00+0900', tz='Asia/Tokyo')
一度 タイムゾーンを付与したあとは、tz_convert
でタイムゾーンを変更したり、
dttz = dt.tz_localize('Asia/Tokyo') dttz # Timestamp('2014-11-09 10:00:00+0900', tz='Asia/Tokyo') dttz.tz_convert('US/Eastern') # Timestamp('2014-11-08 20:00:00-0500', tz='US/Eastern')
タイムゾーンを削除したりできる。
tz_localize(None)
:Timestamp
のローカル時刻を引き継いで タイムゾーンを削除tz_convert(None)
:Timestamp
の GMT 時刻を引き継いで タイムゾーンを削除
dttz # Timestamp('2014-11-09 10:00:00+0900', tz='Asia/Tokyo') dttz.tz_localize(None) # Timestamp('2014-11-09 10:00:00') dttz.tz_convert(None) # Timestamp('2014-11-09 01:00:00')
また、tz_localize
, tz_convert
は pytz
, dateutil.tz
を区別なく扱える。標準の datetime.datetime
では pytz
と dateutil.tz
でタイムゾーンの初期化方法が違ったりするのでこれはうれしい。
import pytz ptz = pytz.timezone('Asia/Tokyo') dt.tz_localize(ptz) # Timestamp('2014-11-09 10:00:00+0900', tz='Asia/Tokyo') from dateutil.tz import gettz dtz = gettz('Asia/Tokyo') dt.tz_localize(dtz) # Timestamp('2014-11-09 10:00:00+0900', tz='dateutil//usr/share/zoneinfo/Asia/Tokyo')
条件に合致する日時のリスト生成
単純な条件での生成
pd.date_range
で以下で指定する引数の条件に合致した日時リストを一括生成できる。渡せるのは 下の4つのうち 3つ。例えば start
, periods
, freq
を渡せば end
は自動的に計算される。
start
: 開始時刻end
: 終端時刻periods
: 内部に含まれる要素の数freq
: 生成する要素ごとに変化させる時間単位/周期 ( 1時間ごと、1日ごとなど)。freq
として指定できる文字列は こちら。またpd.offsets
も使える (後述)。
返り値は上でも少しふれた DatetimeIndex
になる。print
された DatetimeIndex
の読み方として、表示結果の 2行目が 生成された日時の始点 (11/01 10:00) と終端 (11/04 09:00)、3行目が生成されたリストの長さ (72コ)と周期 ( H = 1時間ごと) になる。
dtidx = pd.date_range('2014-11-01 10:00', periods=72, freq='H') dtidx # <class 'pandas.tseries.index.DatetimeIndex'> # [2014-11-01 10:00:00, ..., 2014-11-04 09:00:00] # Length: 72, Freq: H, Timezone: None
生成された DatetimeIndex
から Timestamp
を取得する場合、ふつうに要素選択するか、 .asobject.tolist()
を使う。.asobject.tolist()
の意味は、
asobject
:DatetimeIndex
内の要素をTimestamp
オブジェクトに明示的に変換 (メソッドでなくプロパティなので注意)tolist()
:DatetimeIndex
自身と同じ中身のリストを生成
dtidx[1] # Timestamp('2014-11-01 11:00:00', offset='H') dtidx.asobject.tolist() # [Timestamp('2014-11-01 10:00:00', offset='H'), # Timestamp('2014-11-01 11:00:00', offset='H'), # ... # Timestamp('2014-11-04 08:00:00', offset='H'), # Timestamp('2014-11-04 09:00:00', offset='H')]
少し複雑 (n個おき) な条件での生成
また、freq
に offsets
を与えればもう少し複雑な条件でもリスト生成できる。ある日時から3時間ごとの Timestamp
を生成したい場合は以下のように書ける。
pd.date_range('2014-11-01 10:00', periods=72, freq=offsets.Hour(3)) # <class 'pandas.tseries.index.DatetimeIndex'> # [2014-11-01 10:00:00, ..., 2014-11-10 07:00:00] # Length: 72, Freq: 3H, Timezone: None
さらに複雑な条件 (任意の条件) での生成
さらに複雑な条件を使いたい場合は、一度単純な条件でリスト生成し、必要な部分をスライシングして取得するのがよい。例えば 深夜 0時から5時間ごとに1時間目/4時間目の時刻をリストとして取得したい場合は以下のようにする。
これで dateutil.rrule
でやるような複雑な処理も (大部分? すべて?) カバーできるはず。
dtidx = pd.date_range('2014-11-01 00:00', periods=20, freq='H') selector = [False, True, False, False, True] * 4 dtidx[selector].asobject.tolist() # [Timestamp('2014-11-01 01:00:00'), # Timestamp('2014-11-01 04:00:00'), # Timestamp('2014-11-01 06:00:00'), # Timestamp('2014-11-01 09:00:00'), # Timestamp('2014-11-01 11:00:00'), # Timestamp('2014-11-01 14:00:00'), # Timestamp('2014-11-01 16:00:00'), # Timestamp('2014-11-01 19:00:00')]
DatetimeIndex に対する一括処理
最後に、先のセクションで記載した 時刻 加減算, タイムゾーン処理の方法/メソッドは DatetimeIndex
に対しても利用できる。全体について 何かまとめて処理したい場合は DatetimeIndex
の時点で処理しておくと楽。
dtidx # <class 'pandas.tseries.index.DatetimeIndex'> # [2014-11-01 00:00:00, ..., 2014-11-01 19:00:00] # Length: 20, Freq: H, Timezone: None # 1日後にずらす dtidx + offsets.Day() # <class 'pandas.tseries.index.DatetimeIndex'> # [2014-11-02 00:00:00, ..., 2014-11-02 19:00:00] # Length: 20, Freq: H, Timezone: None # タイムゾーンを設定 dtidx.tz_localize('Asia/Tokyo') # <class 'pandas.tseries.index.DatetimeIndex'> # [2014-11-01 00:00:00+09:00, ..., 2014-11-01 19:00:00+09:00] # Length: 20, Freq: H, Timezone: Asia/Tokyo
まとめ
pandas
なら日時/タイムスタンプ関連の操作もカンタン。
Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理
- 作者: Wes McKinney,小林儀匡,鈴木宏尚,瀬戸山雅人,滝口開資,野上大介
- 出版社/メーカー: オライリージャパン
- 発売日: 2013/12/26
- メディア: 大型本
- この商品を含むブログ (9件) を見る
Python lifelines で生存分析
サイトのアクセス履歴をみていたら "Python", "生存分析", "Kaplan-Meier" なんかがちらほらあったので、知っている方法を書いてみる。
生存分析とは
いくつかのサンプルについて、何らかのイベントが起きるまでの時間とイベント発生率との関係をモデル化するもの。よく使われるのは
- 何かの病気になってからの死亡率
- 部品の故障率
など。
この説明がわかりやすい。
Python でやる方法
どちらか。lifelines のほうが手間はすくない。
- lifelines パッケージを使う
- rpy2 でデータを R に渡して survival パッケージ を使う
1. lifelines パッケージを使う
これを使えば Pure-Python で生存分析できる。使えるのは、以下のノンパラ、セミパラ系の手法。Weibull などのパラメトリック系はない (Scipy 使えばできる)。
- Kaplan-Meier
- Nelson-Aalen
- Cox 比例ハザード
インストール
pip install lifelines
ドキュメント
バージョン古いようなのでアップデートされてないかも。
Lifelines — lifelines 0.8.0.1 documentation
やり方
データは pandas の DataFrame
で扱うのがよい。ここでは、lifelinesにもサンプルデータとして入っている lung (肺ガン) データを使う。
lungデータ
https://github.com/CamDavidsonPilon/lifelines/blob/master/datasets/lung.csv
データの意味はこちら。
ここでは、性別でグループ分けしたデータについて、イベントの発生時間 ("time" カラムの値) とそのときの状態 ("death" カラムの値, True
なら死亡, False
なら censored = 観測打ち切り) で Kaplan-Meierモデルを作る。
これは lifelines.KaplanMeierFitter
クラスでできる。引数の詳細はドキュメント参照。
Introduction to using lifelines
import pandas as pd # 表示する行数を指定 pd.options.display.max_rows = 5 # データの読み込み df = pd.read_csv("lung.csv") # 元データには、死亡なら2, censored なら1が入っている # lifelines に渡すため、死亡なら True, censored なら False となるよう変換する df['death'] = df['status'] == 2 df # Unnamed: 0 inst time status age sex ph.ecog ph.karno pat.karno \ # 0 1 3 306 2 74 1 1 90 100 # 1 2 3 455 2 68 1 0 90 90 # .. ... ... ... ... ... ... ... ... ... # 226 227 6 174 1 66 1 1 90 100 # 227 228 22 177 1 58 2 1 80 90 # # meal.cal wt.loss death # 0 1175 NaN True # 1 1225 15 True # .. ... ... ... # 226 1075 1 False # 227 1060 0 False # # [228 rows x 12 columns] from lifelines import KaplanMeierFitter # グラフを描画する Axes ax = None # 性別でグループ分け for name, group in df.groupby('sex'): kmf = KaplanMeierFitter() kmf.fit(group['time'], event_observed=group['death'], label = 'sex=' + str(name)) # 描画する Axes を指定。None を渡すとエラーになるので場合分け if ax is None: ax = kmf.plot() else: ax = kmf.plot(ax=ax) # Kaplan-Meier曲線を描画 import matplotlib.pyplot as plt plt.show()
Kaplan-Meier曲線 (横軸が経過時間、縦軸が生存率 = イベントの未発生率) となる。性別 = 2 (女性) のほうが 同じ経過時間時点での生存率が高い傾向がある。
2. rpy2 でデータを R に渡して survival パッケージ を使う
pandas と rpy2 との連携はこちらを参照。
やり方
やりたいことにもよるが、結果を Python に戻してからの処理がちょっとめんどい。
import numpy as np import pandas as pd # 表示する行数を指定 pd.options.display.max_rows = 5 # rpy2 使って survival::lung データセットを呼び出す # 手元のデータを使うなら不要 import rpy2.robjects as robjects import pandas.rpy.common as com robjects.packages.importr('survival') lung = com.load_data('lung') # rpy2 形式に変換し、 Rへ渡す lungr = com.convert_to_r_dataframe(lung) robjects.r.assign('lungr', lungr); # R で生存分析実施 robjects.r('fitted <- survfit(Surv(time, status) ~ sex, data = lungr)'); # 結果を Python に戻し、各プロパティを numpy.arrayとして取得 fitted = robjects.r['fitted'] time = np.array(fitted.rx2('time')) surv = np.array(fitted.rx2('surv')) lower = np.array(fitted.rx2('lower')) upper = np.array(fitted.rx2('upper')) # strata のキーを順序どおりに取得するには一手間必要 robjects.r('strata_keys <- names(fitted$strata)') strata_keys = com.convert_robj(robjects.r['strata_keys']) strata = com.convert_robj(fitted.rx2('strata')) strata = reduce(lambda x, y: x + y, [ [k] * strata[k] for k in strata_keys]) # DataFrame作成 result = pd.DataFrame({'time': time, 'surv': surv, 'lower': lower, 'upper': upper, 'strata': strata}) result # lower strata surv time upper # 0 0.954230 sex=1 0.978261 11 1.000000 # 1 0.943423 sex=1 0.971014 12 0.999412 # .. ... ... ... ... ... # 204 0.001582 sex=2 0.011111 821 0.078022 # 205 NaN sex=2 0.000000 965 NaN # プロット import matplotlib.pyplot as plt # グラフを描画する Axes ax = None # 生存曲線と 信頼区間を別々に描画する必要がある # strata ごとに同じ色を使うために グループ名 : 色の辞書を作成 colors = {'sex=1': 'green', 'sex=2': 'blue'} for name, group in result.groupby('strata'): # pandas は index を x軸として plot するので、 # time の値を indexに設定 group = group.set_index('time') color = colors[name] # pandas.Series.plot で生存曲線自体を描画 ax = group['surv'].plot(ax=ax, color=colors[name]) # matplotlib.Axes.fill_between で信頼区間を描画 ax.fill_between(group.index, group['lower'], group['upper'], color=colors[name], alpha=0.3) plt.show()
Python pandas アクセサ / Grouperで少し高度なグルーピング/集計
日本語の説明がなさそうなので。
概要
pandas では groupby
メソッドを使って、指定したカラムの値でデータをグループ分けできる。ここでは少し凝った方法を説明。
※ dtアクセサ の追加、またグルーピング関連のバグ修正がいろいろ入っているので、0.15以降が必要。
※簡単な処理については下の記事でまとめ。
はじめに
例えばこんなデータがあったとして、
import pandas as pd import datetime df = pd.DataFrame({'dt1': [datetime.datetime(2014, 10, 1), datetime.datetime(2014, 10, 2), datetime.datetime(2014, 10, 3), datetime.datetime(2014, 10, 4), datetime.datetime(2014, 10, 5), datetime.datetime(2014, 10, 6), datetime.datetime(2014, 10, 7), datetime.datetime(2014, 11, 1), datetime.datetime(2014, 11, 2), datetime.datetime(2014, 11, 3), datetime.datetime(2014, 11, 4), datetime.datetime(2014, 11, 5), datetime.datetime(2014, 11, 6), datetime.datetime(2014, 11, 7)], 'col1': 'a b'.split() * 7}) # col1 dt1 num1 # 0 a 2014-10-01 3 # 1 b 2014-10-02 4 # 2 a 2014-10-03 2 # 3 b 2014-10-04 5 # 4 a 2014-10-05 1 # 5 b 2014-11-01 3 # 6 a 2014-11-02 2 # 7 b 2014-11-03 3 # 8 a 2014-11-04 5 # 9 b 2014-11-05 2
まずは普通の groupby
。DataFrame
を groupby
すると、結果は DataFrameGroupBy
インスタンスとして返ってくる。このとき、groups
プロパティに グループのラベル : グループに含まれる index からなる辞書が入っている。サンプルスクリプトでの結果は、この groups
プロパティを使って表示することにする。
下の結果は、上の DataFrame
をカラム "col1" の値でグループ分けした結果、グループ "a" に 0, 2, 4, 6, 8行目, グループ "b" に 1, 3, 5, 7, 9行目が含まれていることを示す。
df.groupby('col1').groups # {'a': [0L, 2L, 4L, 6L, 8L], # 'b': [1L, 3L, 5L, 7L, 9L]}
groupby へのリスト渡し
groupby
には 対象の DataFrame
と同じ長さのリスト, numpy.array
, pandas.Series
, pandas.Index
が渡せる。このとき、渡した リスト-likeなものをキーとしてグルーピングしてくれる。
grouper = ['g1', 'g2', 'g3', 'g4', 'g5'] * 2 df.groupby(grouper).groups # {'g5': [4L, 9L], # 'g4': [3L, 8L], # 'g3': [2L, 7L], # 'g2': [1L, 6L], # 'g1': [0L, 5L]}
そのため、一度 外部の関数を通せばどんな柔軟な処理でも書ける。"num1" カラムが偶数か奇数かでグループ分けしたければ、
grouper = df['num1'] % 2 df.groupby(grouper).groups # {0: [1L, 2L, 6L, 9L], # 1: [0L, 3L, 4L, 5L, 7L, 8L]}
アクセサ
でも わざわざ関数書くのめんどくさい、、、。そんなときの アクセサ。例えばカラムが datetime64
型のとき、dt
アクセサを利用して datetime64
の各プロパティにアクセスできる。
例えば "dt1" カラムの各日時から "月" を取得するには、
df['dt1'].dt.month # 0 10 # 1 10 # 2 10 # 3 10 # 4 10 # 5 11 # 6 11 # 7 11 # 8 11 # 9 11 # dtype: int64
これを使えば月次のグルーピングも簡単。
df.groupby(df['dt1'].dt.month).groups # {10: [0L, 1L, 2L, 3L, 4L], # 11: [5L, 6L, 7L, 8L, 9L]}
複数要素、例えば年月の組み合わせでグループ分けしたければ、 dt.year
と dt.month
をリストで渡す。
df.groupby([df['dt1'].dt.year, df['dt1'].dt.month]).groups # {(2014L, 11L): [5L, 6L, 7L, 8L, 9L], # (2014L, 10L): [0L, 1L, 2L, 3L, 4L]}
Grouperで周期的なグルーピング
数日おきなど、特定の周期/間隔でグループ分けしければ Grouper
。freq
キーワードの記法は Documentation 参照。
※ Grouper
で datetime64
をグループ分けした場合、groups
プロパティの読み方はちょっと難しくなる (辞書の値として、グループに含まれる indexではなくグループの切れ目になる index が入る)
df.groupby(pd.Grouper(key='dt1', freq='4d')).groups # {Timestamp('2014-10-09 00:00:00', offset='4D'): 5, # Timestamp('2014-11-02 00:00:00', offset='4D'): 10, # Timestamp('2014-10-13 00:00:00', offset='4D'): 5, # Timestamp('2014-10-01 00:00:00', offset='4D'): 4, # Timestamp('2014-10-05 00:00:00', offset='4D'): 5, # Timestamp('2014-10-25 00:00:00', offset='4D'): 5, # Timestamp('2014-10-29 00:00:00', offset='4D'): 6, # Timestamp('2014-10-17 00:00:00', offset='4D'): 5, # Timestamp('2014-10-21 00:00:00', offset='4D'): 5}
読めないので同じ処理をした結果を普通に print
。4日おきに グループ分けされていることがわかる。
対応する日付のない期間はパディングされる。
for name, group in df.groupby(pd.Grouper(key='dt1', freq='4d')): print(name) print(group) # 2014-10-01 00:00:00 # col1 dt1 num1 # 0 a 2014-10-01 3 # 1 b 2014-10-02 4 # 2 a 2014-10-03 2 # 3 b 2014-10-04 5 # 2014-10-05 00:00:00 # col1 dt1 num1 # 4 a 2014-10-05 1 # 2014-10-09 00:00:00 # Empty DataFrame # 2014-10-13 00:00:00 # Empty DataFrame # 2014-10-17 00:00:00 # Empty DataFrame # 2014-10-21 00:00:00 # Empty DataFrame # 2014-10-25 00:00:00 # Empty DataFrame # 2014-10-29 00:00:00 # col1 dt1 num1 # 5 b 2014-11-01 3 # 2014-11-02 00:00:00 # col1 dt1 num1 # 6 a 2014-11-02 2 # 7 b 2014-11-03 3 # 8 a 2014-11-04 5 # 9 b 2014-11-05 2
集計
上のようにグルーピングした後、集約関数、もしくは aggregate
で普通に集約してもよいが、
df.groupby(df['dt1'].dt.month)['num1'].sum() # 10 15 # 11 15 # Name: num1, dtype: int64
pandas.pivot_table
にも リスト-like、 Grouper
を渡して直接集約できる。例えば "dt1" の year を列, month を行として集計したければ、
pd.pivot_table(df, index=df['dt1'].dt.month, columns=df['dt1'].dt.year, values='num1', aggfunc=sum) # 2014 # 10 15 # 11 15
"col1" を列, "dt1"の値を4日おきに行として集計したければ、
pd.pivot_table(df, index=pd.Grouper(key='dt1', freq='4d'), columns='col1', values='num1', aggfunc=sum) # col1 a b # dt1 # 2014-10-01 5 9 # 2014-10-05 1 NaN # 2014-10-29 NaN 3 # 2014-11-02 7 5
補足 Grouper
での集約には二つ既知の バグがあるので注意。
- グルーピング対象の列の値に
NaT
が入っているときvar
,std
,mean
で集約できない first
,last
,nth
で返ってくる行が 通常のgroupby
と異なる。
まとめ
groupby
,pivot_table
にはリスト-likeが渡せる。複雑な処理を書きたければ 外部の関数で配列作成してグルーピング/集計する。- アクセサ,
Grouper
便利。
おまけ
groupby
には 関数、辞書も渡せる。
df.groupby(lambda x: x % 3).groups # {0: [0L, 3L, 6L, 9L], # 1: [1L, 4L, 7L], # 2: [2L, 5L, 8L]}
辞書渡しの場合、存在しないキーはフィルタされる。
grouper = {1: 'a', 2: 'b', 3: 'a'} df.groupby(by=grouper).groups # {'a': [1L, 3L], # 'b': [2L]} [asin:4873116554:detail]
Python rpy2 で pandas の DataFrame を R の data.frame に変換する
pandas の DataFrame
を R へ渡す/また R から Python へデータを戻す方法について、本家のドキュメント が書きかけなのでよくわからない。ということで 以前 下の文書を書いたので訳してみる。
DOC: Complete R interface section by sinhrks · Pull Request #7309 · pydata/pandas · GitHub
rpy2
を使うと pandas
(Python) <-> R 間のデータの相互変換を以下 2通りの方法で行うことができる。
何をどちらに読み込ませるかという話なので、1, 2を組み合わせて処理することもできる。
共通
rpy2 のインストール
pip install rpy2
準備
import numpy as np import pandas as pd # 表示する行数を指定 pd.options.display.max_rows = 5
R のデータセットを Pandas に読み込む
pandas.rpy.common.load_data
で、R のデータセットが pandas.DataFrame
に変換されて読み込まれる。
import pandas.rpy.common as com infert = com.load_data('infert') type(infert) # pandas.core.frame.DataFrame infert.head() # education age parity induced case spontaneous stratum pooled.stratum # 1 0-5yrs 26 6 1 1 2 1 3 # 2 0-5yrs 42 1 1 1 0 2 1 # 3 0-5yrs 39 6 2 1 0 3 4 # 4 0-5yrs 34 4 2 1 0 4 2 # 5 6-11yrs 35 3 1 1 1 5 32
pandas.DataFrame を R に渡せる形式 (rpy2) に変換する
pandas.DataFrame
から R に渡せる形式 (rpy2.robjects.DataFrame
) への変換は com.convert_to_r_dataframe
で行う。
# サンプルの DataFrame 作成 df = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6], 'C':[7,8,9]}, index=["one", "two", "three"]) df # A B C # one 1 4 7 # two 2 5 8 # three 3 6 9 r_dataframe = com.convert_to_r_dataframe(df) type(r_dataframe) # rpy2.robjects.vectors.DataFrame # ipython でやる場合、print をかまさないと出力がうっとおしい print(r_dataframe) # A B C # one 1 4 7 # two 2 5 8 # three 3 6 9 # rpy2.objects.DataFrame に変換後も属性名の確認/操作はできる ( pandas でやっておけば必要ないが) print(r_dataframe.rownames) # [1] "one" "two" "three" In [20]: print(r_dataframe.colnames) [1] "A" "B" "C"
補足 R へ matrix
で渡したい場合は com.convert_to_r_matrix
。
r_matrix = com.convert_to_r_matrix(df) type(r_matrix) # rpy2.robjects.vectors.Matrix print(r_matrix) # A B C # one 1 4 7 # two 2 5 8 # three 3 6 9
rpy2 形式のデータ を pandas.DataFrame に変換する
上の逆変換は com.convert_robj
。rpy2 オブジェクト (rpy2.robjects.DataFrame
) を pandas.DataFrame
に戻す。
com.convert_robj(r_dataframe) # A B C # one 1 4 7 # two 2 5 8 # three 3 6 9 com.convert_robj(r_matrix) # A B C # one 1 4 7 # two 2 5 8 # three 3 6 9
1. R の関数を Python に loadし、Python の名前空間上で操作する
rpy2.robjects.r.__getitem__
で、R の名前空間のオブジェクトを Python の名前空間へ load できる。ここでは R の sum
関数をPython 上に読み出して使う。読み込んだ関数へ渡せるのは rpy2
のオブジェクトのみ。
# 処理するデータ print(r_dataframe) # A B C # one 1 4 7 # two 2 5 8 # three 3 6 9 import rpy2.robjects as robjects # R 上の sum 関数を rsum として Python の名前空間に load rsum = robjects.r['sum'] type(rsum) # rpy2.robjects.functions.SignatureTranslatedFunction # r_dataframe に対して関数適用 (Python の関数として使える) rsum_result = rsum(r_dataframe) # R の関数なので、結果は vector になる type(rsum_result) # rpy2.robjects.vectors.IntVector # 要素を取り出す場合はスライス rsum_result[0] # 45
2. Python のデータを R に渡し、 R の名前空間上で操作する
rpy2
のオブジェクトを R の名前空間へ渡す場合は robjects.r.assign
。
R で 実行する処理(コマンド)を R に渡す (Rに何か処理をさせる) 場合は robjects.r
。
# Python 上の r_dataframe が R 上で rdf という名前で転送される # セミコロンは ipython での出力省略のため robjects.r.assign('rdf', r_dataframe); # R 上で str(rdf) コマンドを実行 robjects.r('str(rdf)'); # 'data.frame': 3 obs. of 3 variables: # $ A:Class 'AsIs' int [1:3] 1 2 3 # $ B:Class 'AsIs' int [1:3] 4 5 6 # $ C:Class 'AsIs' int [1:3] 7 8 9
処理サンプル
これまでの処理の組み合わせで以下のようなことができる。
線形回帰
# データの準備 iris = com.load_data('iris') # setosa のデータをフィルタ setosa = iris[iris['Species'] == 'setosa'] setosa.head() # Sepal.Length Sepal.Width Petal.Length Petal.Width Species # 1 5.1 3.5 1.4 0.2 setosa # 2 4.9 3.0 1.4 0.2 setosa # 3 4.7 3.2 1.3 0.2 setosa # 4 4.6 3.1 1.5 0.2 setosa # 5 5.0 3.6 1.4 0.2 setosa # (ほか、R でやるには面倒な処理があれば pandas で実行... ) # rpy2 オブジェクトに変換 r_setosa = com.convert_to_r_dataframe(setosa) # R の名前空間に送る robjects.r.assign('setosa', r_setosa); # R の lm 関数を実行し、結果の summary を表示する robjects.r('result <- lm(Sepal.Length~Sepal.Width, data=setosa)'); print(robjects.r('summary(result)')) # Call: # lm(formula = Sepal.Length ~ Sepal.Width, data = setosa) # # Residuals: # Min 1Q Median 3Q Max # -0.52476 -0.16286 0.02166 0.13833 0.44428 # # Coefficients: # Estimate Std. Error t value Pr(>|t|) # (Intercept) 2.6390 0.3100 8.513 3.74e-11 *** # Sepal.Width 0.6905 0.0899 7.681 6.71e-10 *** # --- # Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 # # Residual standard error: 0.2385 on 48 degrees of freedom # Multiple R-squared: 0.5514, Adjusted R-squared: 0.542 # F-statistic: 58.99 on 1 and 48 DF, p-value: 6.71e-10 # R 上の result オブジェクトを Python の名前空間に戻す result = robjects.r['result'] print(result.names) # [1] "coefficients" "residuals" "effects" "rank" # [5] "fitted.values" "assign" "qr" "df.residual" # [9] "xlevels" "call" "terms" "model" # 結果は名前付きリストになっている。各要素には .rx でアクセスできる print(result.rx('coefficients')) # $coefficients # (Intercept) Sepal.Width # 2.6390012 0.6904897 # 回帰式の切片, 係数を取得 intercept, coef1 = result.rx('coefficients')[0] intercept # 2.6390012498579694 coef1 # 0.6904897170776046
時系列処理
時系列の場合に気をつけるのは、
convert_to_r_dataframe
はSeries
に対応していないので、rpy2
で直接 ベクトル (ここではrpy2.FloatVector
) を作って渡す。詳細は rpy2 documentation: Vectors and arrays 。ts
オブジェクトへの変換は R で明示的に行う- R から
pandas.DataFrame
へ結果を戻した際に、日時の index を再度 付与する
# 2013年1月〜 月次 4年分のランダムデータを作成 idx = pd.date_range(start='2013-01-01', freq='M', periods=48) vts = pd.Series(np.random.randn(48), index=idx).cumsum() vts # 2013-01-31 0.801791 # ... # 2016-12-31 -3.391142 # Freq: M, Length: 48 # R へ渡す rpy2 オブジェクトを準備 r_values = robjects.FloatVector(vts.values) # R の名前空間へ転送 robjects.r.assign('values', r_values); # R の ts 関数で timeseries に変換 robjects.r('vts <- ts(values, start=c(2013, 1, 1), frequency=12)'); print(robjects.r['vts']) # Jan Feb Mar Apr May Jun # 2013 0.80179068 -0.04157987 -0.15779190 -0.02982779 -2.03214239 -0.09868078 # 2014 5.97378179 6.27023875 4.85958760 6.50728371 5.14595583 5.29780411 # 2015 1.04548632 0.60093762 0.13941486 0.56116450 -0.20040731 1.19696178 # 2016 -0.09101317 -0.79038658 -0.13305769 0.61016756 -0.13059757 -1.28190161 # Jul Aug Sep Oct Nov Dec # 2013 2.84555901 3.96259305 4.45565104 2.86998914 4.52347928 4.38237841 # 2014 5.16001952 3.44611678 3.49705824 2.37352719 0.75428874 1.62569642 # 2015 -0.03488274 0.13323226 -0.78262492 -0.75325348 -0.65414439 0.40700944 # 2016 -2.31702656 -1.78120320 -1.92904062 -0.83488094 -2.31609640 -3.39114197 # R の stl 関数を実行し、 時系列をトレンド/季節性/残差に分解 robjects.r('result <- stl(vts, s.window=12)'); # 結果を Python の名前空間に戻す result = robjects.r['result'] print(result.names) # [1] "time.series" "weights" "call" "win" "deg" # [6] "jump" "inner" "outer" # 結果の時系列を取得し、pandas.DataFrame へ変換 (index は数値型になってしまう) result_ts = result.rx('time.series')[0] converted = com.convert_robj(result_ts) converted.head() # seasonal trend remainder # 2013.000000 0.716947 -1.123112 1.207956 # 2013.083333 0.264772 -0.603006 0.296655 # 2013.166667 -0.165811 -0.082900 0.090919 # 2013.250000 0.528043 0.437206 -0.995077 # 2013.333333 -0.721440 0.938796 -2.249498 # index を再設定 converted.index = idx converted.head() # seasonal trend remainder # 2013-01-31 0.716947 -1.123112 1.207956 # 2013-02-28 0.264772 -0.603006 0.296655 # 2013-03-31 -0.165811 -0.082900 0.090919 # 2013-04-30 0.528043 0.437206 -0.995077 # 2013-05-31 -0.721440 0.938796 -2.249498
結果をプロットしてみる。
import matplotlib.pyplot as plt fig, axes = plt.subplots(4, 1) axes[0].set_ylabel('Original'); ax = vts.plot(ax=axes[0]); axes[1].set_ylabel('Trend'); ax = converted['trend'].plot(ax=axes[1]); axes[2].set_ylabel('Seasonal'); ax = converted['seasonal'].plot(ax=axes[2]); axes[3].set_ylabel('Residuals'); converted['remainder'].plot(ax=axes[3]) plt.show()
補足
とはいえ、いちいち pandas
-> rpy2
形式へ変換するのは面倒なので、robjects.r
で直接 pandas.DataFrame
を受け渡しできるとうれしい。やり方は、
ENH: automatic rpy2 instance conversion by sinhrks · Pull Request #7385 · pydata/pandas · GitHub
Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理
- 作者: Wes McKinney,小林儀匡,鈴木宏尚,瀬戸山雅人,滝口開資,野上大介
- 出版社/メーカー: オライリージャパン
- 発売日: 2013/12/26
- メディア: 大型本
- この商品を含むブログ (9件) を見る
Python traits で型強制 + traitsui でカンタン GUI 作成
Python の Canopy ディストリビューションで有名な Enthought.inc が作っている traits, traitsui というモジュールが結構便利なのだが、日本語の情報がないのでメモ。
概要
traits
は Python のクラスプロパティに特定の型を強制できるモジュールtraitsui
はtraits
の定義に従って、wxPython
,PyQt
,Pyside
の GUI を簡単にデザインできるモジュール
インストール
pip install traits traitsui
この記事の例ではPyQt を使うので入ってなければ入れる (pip では入らない)。Windows なら MSIインストーラがあるので楽。他OSならソースから build するか、各パッケージ管理で。
traits
Defining Traits: Initialization and Validation — Traits 4 User Manual
ほぼそのままですが。
import traits.api # class定義の際は HasTraits を継承させる class Person(traits.api.HasTraits): # traits を使うプロパティはクラス変数とし、許可される型を traits.api から設定 # 文字列 (str) のみ許可 name = traits.api.Str # 整数のみ許可 age = traits.api.Int # 'M', 'F' もしくは 'X'のみ許可 sex = traits.api.Enum('M', 'F', 'X') # インスタンス初期化 p = Person(name='John', age=22, sex='M') # Str 型が設定されたプロパティを書き換え。str では上書きできるが、別の型を入れるとエラー p.name = 'Mike' # NG! p.name = 0 # TraitError: The 'name' trait of a Person instance must be a string, but a value of 0 <type 'int'> was specified. # Int 型が設定されたプロパティを書き換え。int では上書きられるが、別の型を入れるとエラー p.age = 24 # NG! p.age = 2.0 # TraitError: The 'age' trait of a Person instance must be an integer (int or long), but a value of 2.0 <type 'float'> was specified. # Enum 型が設定されたプロパティを書き換え。初期化の際に許可した値以外はエラー # NG! p.sex = 'B' # TraitError: The 'sex' trait of a Person instance must be 'M' or 'F' or 'X', but a value of 'B' <type 'str'> was specified.
という感じで、クラス定義の際に設定しておけば、煩雑な入力値チェックを自分で書く必要がなくなる。より複雑な条件を設定したい場合は自分でチェック関数を書くこともできる。
さらに、ある変数の変更を検知する Handler や 読み取り専用のProperty (cache可) を定義したりもできる。
class Person2(Person): # Personを継承 # name + age を表示名にしてみる disp = traits.api.Property # _xxx_changed は プロパティ xxx の変更時に自動的に実行 def _name_changed(self, value): print('updated with ' + value) # _get_xxx は 自動的にプロパティ xxx の getter になる def _get_disp(self): return '{0} ({1})'.format(self.name, self.age) # 初期化やプロパティ設定でname を変更すると Person._name_changed が実行される p = Person2(name='John', age=22, sex='M') # updated with John p.name = 'Mike' # updated with Mike # disp を読み取ると Person._get_disp が実行される p.disp # Mike (22) # disp は上書きできない p.disp = 'overwrite' # TraitError: The 'disp' trait of a Person2 instance is 'read only'.
traitsui
traits
で定義したプロパティの型に応じて、適切な GUI を表示してくれる。さきほどの Person2
クラスで .configure_traits
メソッドを実行すると、自動でレイアウトされた GUI がポップアップしてくる。各フィールドはクラスで定義した型に応じてテキスト/プルダウンとして表示される。
p.configure_traits()
traitsui
を使うと、この GUI のレイアウトを変えられる。表示方法は HasTraits
を継承したクラスの traits_view
プロパティで設定する。
表示のレイアウト変更 + ラベル付与 + 性別をラジオボタン選択 + 表示名を読み取り専用にしてみる。どういった設定ができるかは膨大なので 下記ドキュメントを参照。
Introduction to Trait Editor Factories — TraitsUI 4 User Manual
from traitsui.api import View, VGroup, HGroup, Item class Person3(Person2): # VGroupは縦方向のレイアウト # HGroupは横方向のレイアウト traits_view = View(VGroup( HGroup(Item('name', label='氏名')), HGroup(Item('age', label='年齢'), Item('sex', label='性別', style='custom')), HGroup(Item('disp',label='表示名', style='readonly')))) p = Person3(name='John', age=22, sex='M') p.configure_traits()
このGUIからユーザがプロパティを変更した場合、traits
で行った定義に従って入力値のチェックや Handler の実行なんかが自動的に行われる。たとえば Int
で定義された年齢フィールドに文字列を入力しようとすると、エラーとなり入力が許可されない。
使い方 (PyQtへの埋め込み)
traits
, traitsui
でデザインした GUI は PyQt のウィンドウ / ダイアログに組み込むことができる。ので PyQt 上で 細かい GUI のレイアウトやイベント制御の処理を書かなくてすむようになる。
import sys # おまじない import sip sip.setapi('QString', 2) sip.setapi('QVariant', 2) import PyQt4.QtCore as QtCore import PyQt4.QtGui as QtGui class AppForm(QtGui.QMainWindow): def __init__(self, parent=None): QtGui.QMainWindow.__init__(self, parent) # ウィンドウタイトル / サイズを指定 self.setWindowTitle('Person Config') self.resize(400, 200) # メインの widget, layout を作成 self.main_frame = QtGui.QWidget() self.main_layout = QtGui.QVBoxLayout() p = Person3(name='John', age=22, sex='M') # Person3 の画面 widget ( GUI のレイアウト) を取得 control = p.edit_traits(parent=self, kind='subpanel').control # layout に widget 追加 self.main_layout.addWidget(control) # メインの領域に layout 追加 self.main_frame.setLayout(self.main_layout) self.setCentralWidget(self.main_frame) if __name__ == "__main__": app = QtGui.QApplication(sys.argv) form = AppForm() form.show() sys.exit(app.exec_())
まとめ
ちょっとした GUI のついたツールが作りたいとき、traits
, traitsui
を使うとラクだし、設計もシンプルにできる。
Python pandas でのグルーピング/集約/変換処理まとめ
これの pandas 版。
準備
サンプルデータは iris で。
補足 (11/26追記) rpy2
を設定している方は rpy2
から、そうでない方は こちら から .csv
でダウンロードして読み込み (もしくは read_csv
のファイルパスとして直接 URL 指定しても読める)。
import pandas as pd import numpy as np # 表示する行数を設定 pd.options.display.max_rows=5 # iris の読み込みはどちらかで # rpy2 経由で R から iris をロード # import pandas.rpy.common as com # iris = com.load_data('iris') # csv から読み込み # http://aima.cs.berkeley.edu/data/iris.csv names = ['Sepal.Length', 'Sepal.Width', 'Petal.Length', 'Petal.Width', 'Species'] iris = pd.read_csv('iris.csv', header=None, names=names) iris # Sepal.Length Sepal.Width Petal.Length Petal.Width Species # 1 5.1 3.5 1.4 0.2 setosa # 2 4.9 3.0 1.4 0.2 setosa # .. ... ... ... ... ... # 149 6.2 3.4 5.4 2.3 virginica # 150 5.9 3.0 5.1 1.8 virginica
グルーピング/集約
ある列の値ごとに集計
Species 列ごとに Sepal.Length 列の合計を算出する場合、
iris.groupby('Species')['Sepal.Length'].sum() # Species # setosa 250.3 # versicolor 296.8 # virginica 329.4 # Name: Sepal.Length, dtype: float64
全列の合計を取得する場合 DataFrame.groupby
から直接集約関数を呼べばよい。集約できない列は勝手にフィルタされる。
iris.groupby('Species').sum() # Sepal.Length Sepal.Width Petal.Length Petal.Width # Species # setosa 250.3 171.4 73.1 12.3 # versicolor 296.8 138.5 213.0 66.3 # virginica 329.4 148.7 277.6 101.3
集約対象列の指定は DataFrame
の列選択と同じ。すっきり。
iris.groupby('Species')[['Petal.Width', 'Petal.Length']].sum() # Petal.Width Petal.Length # Species # setosa 12.3 73.1 # versicolor 66.3 213.0 # virginica 101.3 277.6
メソッド呼び出しではなく、別に用意された集約関数を渡したい場合は .apply
。文字列で渡したいときは 渡す際に eval
。
iris.groupby('Species')[['Petal.Width', 'Petal.Length']].apply(np.sum) # Petal.Width Petal.Length # Species # setosa 12.3 73.1 # versicolor 66.3 213.0 # virginica 101.3 277.6 iris.groupby('Species')[['Petal.Width', 'Petal.Length']].apply(eval('np.sum')) # Petal.Width Petal.Length # Species # setosa 12.3 73.1 # versicolor 66.3 213.0 # virginica 101.3 277.6
また、集約関数を複数渡したい場合は .agg
。列名 : 集約関数の辞書を渡すので、列ごとに集約関数を変えることもできる。
iris.groupby('Species').agg({'Petal.Length': [np.sum, np.mean], 'Petal.Width': [np.sum, np.mean]}) # Petal.Length Petal.Width # sum mean sum mean # Species # setosa 73.1 1.462 12.3 0.246 # versicolor 213.0 4.260 66.3 1.326 # virginica 277.6 5.552 101.3 2.026
行持ち / 列持ち変換
複数列持ちの値を行持ちに展開 (unpivot / melt)
複数列で持っている値を行持ちに展開する処理は、pd.melt
。 DataFrame.melt
ではないので注意。
melted = pd.melt(iris, id_vars=['Species'], var_name='variable', value_name='value') melted # Species variable value # 0 setosa Sepal.Length 5.1 # 1 setosa Sepal.Length 4.9 # .. ... ... ... # 598 virginica Petal.Width 2.3 # 599 virginica Petal.Width 1.8 # # [600 rows x 3 columns]
複数行持ちの値を列持ちに変換 (pivot)
DataFrame.pivot
。集約処理付きの別関数 pd.pivot_table
もある。
# pivotするデータの準備。Species (列にする値) と variable (行にする値) の組がユニークでないとダメ。 unpivot = melted.groupby(['Species', 'variable']).sum() unpivot = unpivot.reset_index() unpivot # Species variable value # 0 setosa Petal.Length 73.1 # 1 setosa Petal.Width 12.3 # .. ... ... ... # 10 virginica Sepal.Length 329.4 # 11 virginica Sepal.Width 148.7 # # [12 rows x 3 columns] unpivot.pivot(index='variable', columns='Species', values='value') # Species setosa versicolor virginica # variable # Petal.Length 73.1 213.0 277.6 # Petal.Width 12.3 66.3 101.3 # Sepal.Length 250.3 296.8 329.4 # Sepal.Width 171.4 138.5 148.7
列の分割 / 結合
列の値を複数列に分割
pandas
には tidyr::separate
に直接対応する処理はない。.str.split
では分割された文字列が一つの列にリストとして格納されてしまう。そのため、分割結果のリストを個々の列に格納しなおす必要がある。
2014/11/17修正: v0.15.1 以降では str.split
の return_type='frame'
オプションを利用して簡単にできるようになったので修正。既定 ( return_type='series'
)では、split
されたリストが object
型として 1列に保存されてしまうので注意。
2015/01/16追記: v0.16.1 以降では return_type
オプションが deprecate され、 expand
オプションに置き換えられた。expand=True
を指定すれば同様の処理ができる。
melted2 = melted.copy() melted2 # Species variable value # 0 setosa Sepal.Length 5.1 # 1 setosa Sepal.Length 4.9 # .. ... ... ... # 598 virginica Petal.Width 2.3 # 599 virginica Petal.Width 1.8 # # [600 rows x 3 columns] melted2[['Parts', 'Scale']] = melted2['variable'].str.split('.', return_type ='frame') melted2 # Species variable value Parts Scale # 0 setosa Sepal.Length 5.1 Sepal Length # 1 setosa Sepal.Length 4.9 Sepal Length # .. ... ... ... ... ... # 598 virginica Petal.Width 2.3 Petal Width # 599 virginica Petal.Width 1.8 Petal Width # # [600 rows x 5 columns] # 不要な列を削除 melted2.drop('variable', axis=1) # Species value Parts Scale # 0 setosa 5.1 Sepal Length # 1 setosa 4.9 Sepal Length # .. ... ... ... ... # 598 virginica 2.3 Petal Width # 599 virginica 1.8 Petal Width # # [600 rows x 4 columns]
.str.extract
で正規表現を使ってもできる。
melted3 = melted.copy() melted3[['Parts', 'Scale']] = melted3['variable'].str.extract('(.+)\.(.+)') melted3 = melted3.drop('variable', axis=1) melted3 # Species value Parts Scale # 0 setosa 5.1 Sepal Length # 1 setosa 4.9 Sepal Length # .. ... ... ... ... # 598 virginica 2.3 Petal Width # 599 virginica 1.8 Petal Width # # [600 rows x 4 columns]
複数列の値を一列に結合
普通に文字列結合すればよい。
melted3['variable'] = melted3['Parts'] + '.' + melted3['Scale'] melted3 # Species value Parts Scale variable # 0 setosa 5.1 Sepal Length Sepal.Length # 1 setosa 4.9 Sepal Length Sepal.Length # .. ... ... ... ... ... # 598 virginica 2.3 Petal Width Petal.Width # 599 virginica 1.8 Petal Width Petal.Width # # [600 rows x 5 columns]
Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理
- 作者: Wes McKinney,小林儀匡,鈴木宏尚,瀬戸山雅人,滝口開資,野上大介
- 出版社/メーカー: オライリージャパン
- 発売日: 2013/12/26
- メディア: 大型本
- この商品を含むブログ (9件) を見る
簡単なデータ操作を Python pandas で行う
先ほどの R の記事と同じ操作を Python pandas でやる。
Python の場合は Rのようなシンボルの概念がないので、変数が評価される環境を意識する必要が(あまり)ない。
準備
サンプルデータは iris で。
補足 (11/26追記) rpy2
を設定している方は rpy2
から、そうでない方は こちら から .csv
でダウンロードして読み込み (もしくは read_csv
のファイルパスとして直接 URL 指定しても読める)。
import pandas as pd # 表示する行数を設定 pd.options.display.max_rows=5 # iris の読み込みはどちらかで # rpy2 経由で R から iris をロード # import pandas.rpy.common as com # iris = com.load_data('iris') # csv から読み込み # http://aima.cs.berkeley.edu/data/iris.csv names = ['Sepal.Length', 'Sepal.Width', 'Petal.Length', 'Petal.Width', 'Species'] iris = pd.read_csv('iris.csv', header=None, names=names) iris # Sepal.Length Sepal.Width Petal.Length Petal.Width Species # 1 5.1 3.5 1.4 0.2 setosa # 2 4.9 3.0 1.4 0.2 setosa # .. ... ... ... ... ... # 149 6.2 3.4 5.4 2.3 virginica # 150 5.9 3.0 5.1 1.8 virginica
列操作
列名操作
参照と変更。変更前後の列名は辞書で渡すので、可変長でも楽。
iris.columns # Index([u'Sepal.Length', u'Sepal.Width', u'Petal.Length', u'Petal.Width', u'Species', u'Petal.Mult'], dtype='object') iris.rename(columns={'Species': 'newcol'}) # Sepal.Length Sepal.Width Petal.Length Petal.Width newcol # 1 5.1 3.5 1.4 0.2 setosa # 2 4.9 3.0 1.4 0.2 setosa # .. ... ... ... ... ... # 149 6.2 3.4 5.4 2.3 virginica # 150 5.9 3.0 5.1 1.8 virginica
変数名を用いて列選択
iris['Species'] # 1 setosa # ... # 150 virginica # Name: Species, Length: 150, dtype: object
文字列リストを用いて、複数列を選択する
iris[['Petal.Length', 'Petal.Width']] # Petal.Length Petal.Width # 1 1.4 0.2 # 2 1.4 0.2 # .. ... ... # 149 5.4 2.3 # 150 5.1 1.8 # # [150 rows x 2 columns]
真偽値リストを用いて列選択する
これは R のほうがシンプル。
iris.loc[:,[False, False, True, True, False]] # Petal.Length Petal.Width # 1 1.4 0.2 # 2 1.4 0.2 # .. ... ... # 149 5.4 2.3 # 150 5.1 1.8 # # [150 rows x 2 columns]
列の属性/値が特定の条件に該当する列を選択する
型が float
の列のみ取り出す。
iris.loc[:,iris.dtypes == float] # Sepal.Length Sepal.Width Petal.Length Petal.Width # 1 5.1 3.5 1.4 0.2 # 2 4.9 3.0 1.4 0.2 # .. ... ... ... ... # 149 6.2 3.4 5.4 2.3 # 150 5.9 3.0 5.1 1.8 # # [150 rows x 4 columns]
public ではないが数値型の列のみ取り出すメソッドもある。
iris._get_numeric_data() # Sepal.Length Sepal.Width Petal.Length Petal.Width Petal.Mult # 1 5.1 3.5 1.4 0.2 0.28 # 2 4.9 3.0 1.4 0.2 0.28 # .. ... ... ... ... ... # 149 6.2 3.4 5.4 2.3 12.42 # 150 5.9 3.0 5.1 1.8 9.18 # # [150 rows x 5 columns]
行操作
値が特定の条件を満たす行を抽出する
iris[iris['Species'] == 'virginica'] # Sepal.Length Sepal.Width Petal.Length Petal.Width Species # 101 6.3 3.3 6.0 2.5 virginica # 102 5.8 2.7 5.1 1.9 virginica # .. ... ... ... ... ... # 149 6.2 3.4 5.4 2.3 virginica # 150 5.9 3.0 5.1 1.8 virginica # # [50 rows x 5 columns]
特定の行番号を抽出する
iris.loc[[2, 3, 4]] # Sepal.Length Sepal.Width Petal.Length Petal.Width Species # 2 4.9 3.0 1.4 0.2 setosa # 3 4.7 3.2 1.3 0.2 setosa # 4 4.6 3.1 1.5 0.2 setosa
ランダムサンプリングしたい場合は index
をサンプリングしてスライス。
import random iris.loc[random.sample(iris.index, 5)] # Sepal.Length Sepal.Width Petal.Length Petal.Width Species # 64 6.1 2.9 4.7 1.4 versicolor # 17 5.4 3.9 1.3 0.4 setosa # 14 4.3 3.0 1.1 0.1 setosa # 4 4.6 3.1 1.5 0.2 setosa # 146 6.7 3.0 5.2 2.3 virginica
代入
iris['Petal.Mult'] = iris['Petal.Width'] * iris['Petal.Length'] iris # Sepal.Length Sepal.Width Petal.Length Petal.Width Species \ # 1 5.1 3.5 1.4 0.2 setosa # 2 4.9 3.0 1.4 0.2 setosa # .. ... ... ... ... ... # 149 6.2 3.4 5.4 2.3 virginica # 150 5.9 3.0 5.1 1.8 virginica # # Petal.Mult # 1 0.28 # 2 0.28 # .. ... # 149 12.42 # 150 9.18 # # [150 rows x 6 columns]
まとめ
pandas
いいですよ。
Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理
- 作者: Wes McKinney,小林儀匡,鈴木宏尚,瀬戸山雅人,滝口開資,野上大介
- 出版社/メーカー: オライリージャパン
- 発売日: 2013/12/26
- メディア: 大型本
- この商品を含むブログ (9件) を見る
Pythonでdata.go.jpからデータを取得する
データカタログサイト data.go.jp が本稼働したので、そこからデータを pandasのデータフレームとして取得するモジュールを書いた。
data.go.jp に限らず data.go...系は CKAN で構築されていることが多いのだが、PyPI 上には使いやすそうなクライアントが現状見当たらない。
インストール
pip install pyopendata
ドキュメント
http://pyopendata.readthedocs.org/en/latest/
データの取得
サンプルとして 鉱工業指数の原指数を業種別/月次 で取ってくる。data.go.jp上のURLのうち、"meti_20140901_0895"がパッケージのID, "aad25837-7e83-4881-9372-1839ecb9b5eb"がリソースのIDになる。リソースのIDがわかればファイルが一意に特定される (ハズ)。
上記リンク先でデータがプレビュー/ダウンロードできるが、"生産", "出荷", "在庫", "在庫率" 4シートからなるExcelになっている。また、ヘッダは3行目から始まっているので冒頭2行は読み飛ばす必要がある。
※対象のファイルは pandasの read_excel, read_csvでパースできるものでないとだめ。
# おまじない from __future__ import unicode_literals import pyopendata as pyod import pandas as pd pd.options.display.mpl_style = 'default' import matplotlib.pyplot as plt plt.ioff() # DataStoreの初期化 store = pyod.CKANStore('http://www.data.go.jp/data') # 取得するリソースIDを指定 resource = store.get('aad25837-7e83-4881-9372-1839ecb9b5eb') # シート名: 不要カラムの辞書 sheets = {'生産': '付加生産ウエイト', '出荷': '出荷ウエイト', '在庫': '在庫ウエイト'} for sheet in sheets: # sheet名を指定して開く。最初の二行は読み飛ばす df = resource.read(sheetname=sheet, skiprows=[0, 1]) # 不要なカラムを削除 df = df.drop(['品目番号', sheets[sheet]], axis=1) # index設定 df = df.set_index('品目名称') # 転置して、時間が行として並ぶようにする df = df.T # 数値に変換できるものを変換 df = df.convert_objects(convert_numeric=True) # 適当な列をフィルタ df = df[['製造工業', '電気機械工業', '機械工具', '乗用車']] # プロット ax = df.plot() ax.set_title(sheet) plt.show()
こんな感じで順番にプロットされる。