Python pandas 図でみる データ連結 / 結合処理
なんかぼやぼやしているうちにひさびさの pandas
エントリになってしまった。基本的な使い方については網羅したい気持ちはあるので、、、。
今回は データの連結 / 結合まわり。この部分 公式ドキュメント がちょっとわかりにくいので改訂したいなと思っていて、自分の整理もかねて書きたい。
公式の方はもう少し細かい使い方も載っているのだが、特に重要だろうというところだけをまとめる。
連結 / 結合という用語は以下の意味で使っている。まず憶えておいたほうがよい関数、メソッドは以下の 4 つだけ。
- 連結: データの中身をある方向にそのままつなげる。
pd.concat
,DataFrame.append
- 結合: データの中身を何かのキーの値で紐付けてつなげる。
pd.merge
,DataFrame.join
連結 (concatenate)
柔軟な連結 pd.concat
ふたつの DataFrame
の連結は pd.concat
で行う ( DataFrame.concat
ではない)。元データとする DataFrame
を df1
、df2
としてそれぞれ以下のように定義する。
import pandas as pd df1 = pd.DataFrame({'A': ['A0', 'A1', 'A2', 'A3'], 'B': ['B0', 'B1', 'B2', 'B3'], 'C': ['C0', 'C1', 'C2', 'C3'], 'D': ['D0', 'D1', 'D2', 'D3']}, index=[0, 1, 2, 3]) df2 = pd.DataFrame({'A': ['A4', 'A5', 'A6', 'A7'], 'B': ['B4', 'B5', 'B6', 'B7'], 'C': ['C4', 'C5', 'C6', 'C7'], 'D': ['D4', 'D5', 'D6', 'D7']}, index=[4, 5, 6, 7])
基本的な連結
この 2 つのデータに対する pd.concat
の結果は下図 Result で示すもの = 縦方向の連結になる。pd.concat
の引数は連結したい DataFrame
のリスト [df1, df2]
になる。Result の上半分が df1
, 下半分が df2
に対応している。
pd.concat([df1, df2])
引数には 3つ以上の DataFrame
からなるリストを渡してもよい。結果は 各 DataFrame
が順に縦方向に連結されたものになる。
df3 = pd.DataFrame({'A': ['A8', 'A9', 'A10', 'A11'], 'B': ['B8', 'B9', 'B10', 'B11'], 'C': ['C8', 'C9', 'C10', 'C11'], 'D': ['D8', 'D9', 'D10', 'D11']}, index=[8, 9, 10, 11]) pd.concat([df1, df2, df3])
列名が異なる場合の連結
上ふたつの例では連結する各 DataFrame
の 列名 ( columns
)はすべて ['A', 'B', 'C', 'D']
で同一だったので気にする必要はなかったが、内部的には 各列は 列名で紐付けされてから連結されている。
以下のように 列名が異なる場合、元データ両方に存在する 列 ['B', 'D']
はそれそれ紐付けられて縦連結され、片方にしか存在しない列は 空の部分が NaN
でパディングされる。
df4 = pd.DataFrame({'B': ['B2', 'B3', 'B6', 'B7'], 'D': ['D2', 'D3', 'D6', 'D7'], 'F': ['F2', 'F3', 'F6', 'F7']}, index=[2, 3, 6, 7]) pd.concat([df1, df4])
補足 連結方向のラベル ( 縦方向の場合 行名 = index
) 同士は紐付けされず 常にそのまま連結される。出力をみると、 df1
の index
である [0, 1, 2, 3]
と df2
の index
である [2, 3, 6, 7]
は一部重複しているが、紐付けされず元の順序のまま連結されている。連結方向のラベルを重複させたくない場合の処理は後述。
横方向の連結
横方向に連結したい場合は axis=1
を指定。このとき 直前の例とは逆に 紐付けは 連結方向でないラベル = index
について行われる。 連結方向のラベルにあたる columns
はそのまま維持される。
pd.concat([df1, df4], axis=1)
連結処理の指定
上でみたとおり、pd.concat
による連結では、データを "連結方向でないラベル" で紐付けしてから 連結していた。この紐付けは元データをすべて残す形 = 完全外部結合のような形で行われている。
引数の各データに共通のラベルのみを残して連結したい場合は join='inner'
を指定する。下の例では横連結を指定しているため、共通の index
である [2, 3]
に対応する行のみが残る。
pd.concat([df1, df4], axis=1, join='inner')
また、紐付け時に特定のラベルのみを残したい場合もある。そのときは join_axes
で残したいラベルの名前を指定すればよい。
例えば axis=1
横方向に連結するとき、join_axes
に ひとつめの DataFrame
の index
を指定すると、それらだけが残るため 左外部結合のような処理になり、
pd.concat([df1, df4], axis=1, join_axes=[df1.index])
ふたつめの DataFrame
の index
を指定すると 右外部結合のような処理になる。
pd.concat([df1, df4], axis=1, join_axes=[df4.index])
連結方向のラベルの指定
pd.concat
は既定では 連結方向のラベル ( 縦連結の場合は index
、横連結の場合は columns
) に対しては特に変更を行わない。そのため、上の例のように 連結結果に同名のラベルが重複してしまうことがある。
こういう場合、連結元のデータごとに keys
キーワードで指定したラベルを追加で付与することができる。このとき、結果は 複数のレベルをもつ MultiIndex
になる ( MultiIndex
については別途)。これは図だけでもわかりにくいので テキストでの出力も添付。
pd.concat([df1, df4], axis=1, keys=['X', 'Y']) # X Y # A B C D B D F # 0 A0 B0 C0 D0 NaN NaN NaN # 1 A1 B1 C1 D1 NaN NaN NaN # 2 A2 B2 C2 D2 B2 D2 F2 # 3 A3 B3 C3 D3 B3 D3 F3 # 6 NaN NaN NaN NaN B6 D6 F6 # 7 NaN NaN NaN NaN B7 D7 F7
もしくは、ignore_index=True
を指定して 連結方向のラベルを 0 から振りなおすことができる。これは縦連結のときに index
を連番で振りなおす場合に便利。
pd.concat([df1, df4], ignore_index=True)
縦方向のシンプルな連結 DataFrame.append
pd.concat
でたいていの連結はできる。うち、よく使う 縦方向の連結については DataFrame.append
でよりシンプルに書ける。
df1.append(df2)
引数には DataFrame
のリストも渡せる。
df1.append([df2, df4])
また、データに一行追加したい、なんて場合も DataFrame.append
。このとき、引数は 追加する行に対応した Series
になる。
このとき、連結対象の DataFrame
の columns
と Series
の index
どうしが紐付けられて連結される。そのため、行として追加する Series
は以下のような形で作る。Series
が name
属性を持っている場合は 連結後の行の index
は name
で指定されたもの (ここでは 10
) になる。
s1 = pd.Series(['X0', 'X1', 'X2', 'X3'], index=['A', 'B', 'C', 'D'], name=10) df1.append(s1)
name
属性のない Series
を連結したい場合は ignore_index=True
を指定しないとエラーになる。
s2 = pd.Series(['X0', 'X1', 'X2', 'X3'], index=['A', 'B', 'C', 'D']) # NG! df1.append(s2) # TypeError: Can only append a Series if ignore_index=True or if the Series has a name # OK df1.append(s2, ignore_index=True)
補足 append
の際、内部では毎回 元データも含めた全体のコピー処理が走るので、ループで一行ずつ追加するような処理は避けたほうがよい。
結合 (merge)
列の値による結合 pd.merge
ふたつの DataFrame
の結合は pd.merge
もしくは DataFrame.merge
で行う。結合もとの DataFrame
を left
、right
としてそれぞれ以下のように定義する。
left = pd.DataFrame({'key': ['K0', 'K1', 'K2', 'K3'], 'A': ['A0', 'A1', 'A2', 'A3'], 'B': ['B0', 'B1', 'B2', 'B3']}) right = pd.DataFrame({'key': ['K1', 'K3', 'K5', 'K7'], 'C': ['C1', 'C3', 'C5', 'C7'], 'D': ['D1', 'D3', 'D5', 'D7']}, index=[1, 3, 5, 7])
この例では 作成した DataFrame
中の key
カラムを結合時のキーとし、この列の値が同じ行どうしを結合したい。結合時のキーとなる列名は on
キーワードで指定する。既定では内部結合となり、両方のデータに共通の ['K1', 'K3']
に対応する行どうしが結合されて残る。
pd.merge(left, right, on='key')
結合方法は how
キーワードで指定する。指定できるのは、
inner
: 既定。内部結合。両方のデータに含まれるキーだけを残す。left
: 左外部結合。ひとつめのデータのキーをすべて残す。right
: 右外部結合。ふたつめのデータのキーをすべて残す。outer
: 完全外部結合。すべてのキーを残す。
それぞれの出力を順に図示する。
pd.merge(left, right, on='key', how='left')
pd.merge(left, right, on='key', how='right')
pd.merge(left, right, on='key', how='outer')
複数のキーによる結合
キーとして複数の列を指定したい場合は、on
キーワードにキーとする列名のリストを渡せばよい。
left = pd.DataFrame({'key1': ['K0', 'K0', 'K1', 'K2'], 'key2': ['K0', 'K1', 'K0', 'K1'], 'A': ['A0', 'A1', 'A2', 'A3'], 'B': ['B0', 'B1', 'B2', 'B3']}) right = pd.DataFrame({'key1': ['K0', 'K1', 'K1', 'K2'], 'key2': ['K0', 'K0', 'K0', 'K0'], 'C': ['C0', 'C1', 'C2', 'C3'], 'D': ['D0', 'D1', 'D2', 'D3']}) pd.merge(left, right, on=['key1', 'key2'])
index
による結合 DataFrame.join
各データの index
をキーとして結合したい場合は、DataFrame.join
が便利。既定は左外部結合となり、結合方法は how
で変更できる。指定できるオプションは pd.merge
と同じ。
left = pd.DataFrame({'A': ['A0', 'A1', 'A2'], 'B': ['B0', 'B1', 'B2']}, index=['K0', 'K1', 'K2']) right = pd.DataFrame({'C': ['C0', 'C2', 'C3'], 'D': ['D0', 'D2', 'D3']}, index=['K0', 'K2', 'K3']) left.join(right)
left.join(right, how='inner')
left.join(right, how='right')
left.join(right, how='outer')
補足 pd.merge
でも left_index
ならびに right_index
キーワードによって index
をキーとした結合はできる。
まとめ
pandas
でのデータ連結 / 結合まわりを整理した。これ以外の データ変形 (行持ち / 列持ち変換とか) は R の {dplyr}
、{tidyr}
との対比でまとめたやつがあるのだが、列名 や行名が複数のレベルを持つ = MultiIndex
の場合など pandas
固有のものもあるのでまた別途。
- Python pandas でのグルーピング/集約/変換処理まとめ - StatsFragments
- R dplyr, tidyr でのグルーピング/集約/変換処理まとめ - StatsFragments
2015/05/12追記
公式ドキュメントに反映した。こちらのほうが網羅的。
Merge, join, and concatenate — pandas 0.16.2 documentation
Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理
- 作者: Wes McKinney,小林儀匡,鈴木宏尚,瀬戸山雅人,滝口開資,野上大介
- 出版社/メーカー: オライリージャパン
- 発売日: 2013/12/26
- メディア: 大型本
- この商品を含むブログ (10件) を見る
Python pandas strアクセサによる文字列処理
概要
今週の 週刊 pandas
は文字列処理について。やたらと文字数が多くなったのだが、これはデータを都度表示しているせいであって自分の話がムダに長いわけではない、、、と思いたい。
今回はこちらの記事に書いた内容も使うので、適宜ご参照ください。
サンプルデータ
なんか適当な実データないかな?と探していたら 週間少年ジャンプの過去作品の連載作品 / ジャンルなどがまとめられているサイトをみつけた。これを pandas
で集計できる形まで整形することをゴールにしたい。
KTR's Comic Room: Weekly Jump Database
データの読み込み
上記リンクの "ジャンプ連載データ表" を、ファイル名 "jump_db.html" としてローカルに保存した。
補足 pd.read_html
では引数に URL を渡して 直接ネットワークからファイルを読むこともできる。が、今回は データ元サイトへの負荷をさけるため、ローカルの HTML ファイルを読むことにした。
依存パッケージのインストール
pd.read_html
を利用するためには 以下のパッケージが必要。インストールしていない場合はインストールする。
pip install lxml html5lib beautifulsoup4
準備
numpy
, pandas
をロードする。
# おまじない from __future__ import unicode_literals import numpy as np import pandas as pd # 表示する行数を指定 pd.options.display.max_rows = 10
HTML ファイルの読み込み
pd.read_html
では 対象の HTML ファイルに含まれる TABLE
要素 を DataFrame
として読み出す。ひとつの html ファイルには複数の TABLE
が含まれることもあるため、pd.read_html
の返り値は DataFrame
のリストになっている。
# read_htmlの返り値は DataFrame のリストになる dfs = pd.read_html('jump_db.html', header=0) type(dfs) # list # 最初のDataFrameを取り出す df = dfs[0] df # 開始 終了 回数 タイトル 作者 原作者・その他 ジャンル # 0 6801 7144 131 父の魂 貝塚ひろし NaN NaN # 1 6801 6811 11 くじら大吾 梅本さちお NaN NaN # 2 6804 6913 21 おれはカミカゼ 荘司としお NaN NaN # 3 6808 7030 60 漫画コント55号 榎本有也 NaN NaN # 4 6810 6919 21 男の条件 川崎のぼる 原作/梶原一騎 NaN # .. ... ... ... ... ... ... ... # 667 1442 連載中 12+ ハイファイクラスタ 後藤逸平 NaN 近未来,SF,刑事,一芸 # 668 1443 連載中 11+ Sporting Salt 久保田ゆうと NaN スポーツ医学,学園 # 669 1451 連載中 3+ 卓上のアゲハ 古屋樹 NaN 卓球,エロ,ミステリ # 670 1452 連載中 2+ E−ROBOT 山本亮平 NaN エロ,ロボット # 671 1501 連載中 1+ 学糾法廷 小畑健 原作/榎伸晃 学園,裁判 # # [672 rows x 7 columns] # 後続処理でのエラーチェックのため、最初のレコード数を保存しておく original_length = len(df)
最近の新連載はエロばっかりなの、、、??ちょっと心惹かれるがまあそれはいい。
各カラムのデータ型を確認するには df.dtypes
。文字列が含まれるカラムは object
型になっている。
# カラムのデータ型を表示 df.dtypes # 開始 float64 # 終了 object # 回数 object # タイトル object # 作者 object # 原作者・その他 object # ジャンル object # dtype: object
str
アクセサ
以降の処理は pandas
の str
アクセサを中心に行う。pandas
では内部のデータ型が文字列 ( str
もしくは unicode
) 型のとき、 str
アクセサを使って データの各要素に対して文字列メソッドを適用することができる。str
アクセサから使えるメソッドの一覧はこちら。
補足 v0.15.1 時点では str
アクセサからは使えない Python 標準の文字列メソッドもある。そんなときは apply
。apply
についてはこちら。
例えば、文字列を小文字化する lower
メソッドを各要素に対して適用する場合はこんな感じ。
# 各要素が大文字の Series を作成 d.Series(['AAA', 'BBB', 'CCC']) # 0 AAA # 1 BBB # 2 CCC # dtype: object # str アクセサを通じて、各要素に lower メソッドを適用 pd.Series(['AAA', 'BBB', 'CCC']).str.lower() # 0 aaa # 1 bbb # 2 ccc # dtype: object # str アクセサを使わないと AttributeError になり NG! pd.Series(['AAA', 'BBB', 'CCCC']).lower() # AttributeError: 'Series' object has no attribute 'lower'
また、str
アクセサに対するスライシングは各要素へのスライシングになる。各要素の最初の二文字を取り出す場合、
pd.Series(['AAA', 'BBB', 'CCC']).str[0:2] # 0 AA # 1 BB # 2 CC # dtype: object
データの処理
ここからサンプルデータへの処理を始める。
連載継続中かどうかのフラグ立て
元データでは、各作品が現在も連載中かどうかは以下いずれかでわかる。
- "終了" カラムの値が "連載中" である
- "回数" カラムの末尾に
+
がついている
# 回数 カラムの値を確認 df['回数'] # 0 131 # 1 11 # 2 21 # ... # 669 3+ # 670 2+ # 671 1+ # Name: 回数, Length: 672, dtype: object
が、このままでは機械的に扱いにくいため、連載中かどうかで bool
のフラグをたてたい。そのためには、以下どちらかの処理を行えばよい。
- "終了" カラムに対する論理演算を行う
- "回数" カラムの末尾を
str.endswith
で調べる
# 終了カラムの値で判別する場合 df['終了'] == '連載中' # 0 False # 1 False # 2 False # ... # 669 True # 670 True # 671 True # Name: 終了, Length: 672, dtype: bool # 回数カラムの値で判別する場合 df['回数'].str.endswith('+') # 0 False # 1 False # 2 False # ... # 669 True # 670 True # 671 True # Name: 回数, Length: 672, dtype: bool # 結果を 連載中 カラムとして代入 df['連載中'] = df['回数'].str.endswith('+')
連載回数のパース
続けて、現在は文字列 ( object
) 型として読み込まれている連載回数を数値型として扱いたい。が、上のとおり 連載中のレコードには末尾に +
, 確実な連載回数が取得できているレコードは [ ]
で囲われているため、そのままでは数値型へ変換できない。
# そのまま変換しようとすると NG! df['回数'].astype(float) # ValueError: invalid literal for float(): 1+
数値に変換するためには、文字列中の数値部分だけを切り出してから数値に変換すればよい。str.extract
を使うとデータに対して指定した正規表現とマッチする部分を抽出できる。抽出した数値部分に対して astype
を使って float
型へ変換する。
# 数値部分だけを抽出 (この時点では各要素は文字列) ['回数'].str.extract('([0-9]+)') # 0 131 # 1 11 # 2 21 # ... # 669 3 # 670 2 # 671 1 # Name: 回数, Length: 672, dtype: object # float 型へ変換 (表示の dtype 部分で型がわかる) df['回数'].str.extract('([0-9]+)').astype(float) # 0 131 # 1 11 # 2 21 # ... # 669 3 # 670 2 # 671 1 # Name: 回数, Length: 672, dtype: float64
補足 1件 元データが "??" のものがある。この文字列は数値を含まないため、str.extract
の結果は NaN
になる。NaN
を含むデータは int
型へは変換できない。
# NG! NaN があるため int 型へは変換できない df['回数'].str.extract('([0-9]+)').astype(int) # ValueError: cannot convert float NaN to integer
上記の結果を元のカラムに代入して上書きする。その後、dropna
で NaN
のデータを捨てる。dropna
についてはこちら。
# 結果を上書き df['回数'] = df['回数'].str.extract('([0-9]+)').astype(float) # 回数 が NaN のデータを捨てる df = df.dropna(axis=0, subset=['回数']) # 1レコードがフィルタされたことを確かめる assert len(df) + 1 == original_length df.shape # (671, 8)
連載開始時のタイムスタンプ取得
"開始" カラムの値から 年度, 週を取得。元データは、通常は "9650" のように年下二桁 + 週 の4桁、合併号では "9705.06" のようなドット区切りの値が入っている。そのため、read_heml
では float
型としてパースされている。これを datetime
型に変換したい。
ここでは N 号は その年の N 週 月曜に発行されたものとして日付を埋める。実際には号数と週番号はずれる / 合併号は発行曜日がずれる / 昔は発行曜日が違っていた、と様々な問題があるがここでは無視する。
素直にやるなら apply
を使ってこんな感じで書ける。pd.offsets
についてはこちら。
import datetime def parse_date(x): # 小数点を切り捨て x = np.floor(x) y, w = divmod(x, 100) # y が50より大きければ 1900年代, それ以外なら2000年代として扱う y += 1900 if y > 50 else 2000 d = datetime.datetime(int(y), 1, 1) # 週次のオフセットを追加 d += pd.offsets.Week(int(w), weekday=0) return d df['開始'].apply(parse_date) # 0 1968-01-08 # 1 1968-01-08 # 2 1968-01-29 # ... # 669 2014-12-22 # 670 2014-12-29 # 671 2015-01-05 # Name: 開始, Length: 671, dtype: datetime64[ns]
今回は文字列処理の記事なのであえて文字列として処理する。最初の手順は以下。
- 処理対象のカラムを
astype
で文字列型に変換し、適当な変数に代入する。 str.split
で小数点前後で文字列を分割。返り値をDataFrame
で受け取るためreturn_type='frame'
を指定。- 小数点の前の文字列のみ = 1列目のみ を処理対象の変数に代入しなおす。
# 処理対象のカラムを文字列型に変換 dt = df['開始'].astype(str) dt # 0 6801.0 # 1 6801.0 # 2 6804.0 # ... # 669 1451.0 # 670 1452.0 # 671 1501.0 # Name: 開始, Length: 671, dtype: object # str.split で小数点前後を分割 dt.str.split('.', return_type='frame') # 0 1 # 0 6801 0 # 1 6801 0 # 2 6804 0 # 3 6808 0 # 4 6810 0 # .. ... .. # 667 1442 0 # 668 1443 0 # 669 1451 0 # 670 1452 0 # 671 1501 0 # # [671 rows x 2 columns] # 小数点の前の文字列のみを処理対象の変数に代入 dt = dt.str.split('.', return_type='frame')[0] dt # 0 6801 # 1 6801 # 2 6804 # ... # 669 1451 # 670 1452 # 671 1501 # Name: 0, Length: 671, dtype: object
ここで、values
プロパティを使って内部の値を確認する。00年代の値は数値の桁数が異なるため、文字列長も異なっていることがわかる。このままではパースしにくいため、以下の処理を行う。
str.pad
で指定した長さまで空白文字でパディングする。str.replace
で空白文字を "0" に置き換える。
# 変数内部の値を確認 dt.values # array([u'6801', u'6801', u'6804', u'6808', u'6810', u'6811', u'6811', # ..... # u'9933', u'9934', u'9943', u'9943', u'9944', u'1', u'2', u'12', # u'13', u'23', u'24', u'32', u'33', u'34', u'38', u'47', u'48', # ..... # u'1451', u'1452', u'1501'], dtype=object) # str.pad で指定した長さまでパディング dt.str.pad(4).values # array([u'6801', u'6801', u'6804', u'6808', u'6810', u'6811', u'6811', # ..... # u'9933', u'9934', u'9943', u'9943', u'9944', u' 1', u' 2', # u' 12', u' 13', u' 23', u' 24', u' 32', u' 33', u' 34', # ..... # u'1441', u'1442', u'1443', u'1451', u'1452', u'1501'], dtype=object) # str.replace で空白文字を "0" に置換 dt.str.pad(4).str.replace(' ', '0').values # array([u'6801', u'6801', u'6804', u'6808', u'6810', u'6811', u'6811', # ..... # u'0012', u'0013', u'0023', u'0024', u'0032', u'0033', u'0034', # ..... # u'1441', u'1442', u'1443', u'1451', u'1452', u'1501'], dtype=object) # ここまでの処理結果を代入 dt = dt.str.pad(4).str.replace(' ', '0')
ここまでで、変数 dt
の中身は "年2桁 + 週2桁" の4文字からなる文字列になった。こいつに対して、
- スライシングによって文字列を 年, 週の部分文字列に分割する。
- 年については下2桁しかないため、
where
で場合分けして 西暦4桁の文字列にする。where
についてはこちら。これでpd.to_datetime
で処理できる形になる。 pd.to_datetime
で日付型にパース。入力は年のみのため、format='%Y'
を指定する。pd.to_datetime
についてはこちら。
# スライシングで年, 週に分割 year = dt.str[0:2] week = dt.str[2:4] # 西暦4桁の文字列を作成 year = ('19' + year).where(year > '50', '20' + year) year # 0 1968 # 1 1968 # 2 1968 # ... # 669 2014 # 670 2014 # 671 2015 # Name: 0, Length: 671, dtype: object # pd.to_datetime で日付型にパース year = pd.to_datetime(year, format='%Y') year # 0 1968-01-01 # 1 1968-01-01 # 2 1968-01-01 # ... # 669 2014-01-01 # 670 2014-01-01 # 671 2015-01-01 # Name: 0, Length: 671, dtype: datetime64[ns]
ここまでできれば、あと必要な処理は、
- 週の文字列から、
pd.offsets.Week
インスタンスを作る - 年から生成したタイムスタンプを
Week
分ずらす
# 週の文字列から、pd.offsets.Week インスタンスを作る week = [pd.offsets.Week(w, weekday=0) for w in week.astype(int)] week # [<Week: weekday=0>, # <Week: weekday=0>, # <4 * Weeks: weekday=0>, # ..... # <52 * Weeks: weekday=0>, # <Week: weekday=0>] # 10. 年から生成したタイムスタンプを 週番号分ずらす dt = [y + w for y, w in zip(year, week)] dt # [Timestamp('1968-01-08 00:00:00'), # Timestamp('1968-01-08 00:00:00'), # Timestamp('1968-01-29 00:00:00'), # ..... # Timestamp('2014-12-29 00:00:00'), # Timestamp('2015-01-05 00:00:00')] # 元の DataFrame に対して代入 df.loc[:, '開始'] = dt
原作有無でのフラグ立て
元データの "原作者・その他" カラムをみると、ここには原作ほか関係者 (監修とか) の名前も入っている。
df[~pd.isnull(df['原作者・その他'])]['原作者・その他'] # 4 原作/梶原一騎 # 9 原作/スドウテルオ # 28 原作/藤井冬木 # ... # 660 原作/成田良悟 # 661 原作/下山健人 # 671 原作/榎伸晃 # Name: 原作者・その他, Length: 111, dtype: object
"原作者・その他" カラムの値から 原作者がいる場合だけフラグを立てたい。具体的には、カラムに "原作/"を含む値が入っているときは 原作ありとして扱いたい。
そんな場合は str.contains
。
補足 元データでは 原作者は常に先頭に来ているので、str.startswith
でもよい。
df['原作者・その他'].str.contains('原作/') # 0 NaN # ... # 671 True # Name: 原作者・その他, Length: 672, dtype: object # 結果をカラムに代入 df['原作あり'] = df['原作者・その他'].str.contains('原作/')
重複データの削除
一部のシリーズでは 全体と 各部(第一部、二部など) で重複してデータが取得されている。目視でそれっぽいシリーズを抽出して表示してみる。
# 重複しているシリーズ候補 series = ['コブラ', 'ジョジョの奇妙な冒険', 'BASTARD!!', 'みどりのマキバオー', 'ONE PIECE', 'ボボボーボ・ボーボボ', 'DEATH NOTE', 'スティール・ボール・ラン', 'トリコ', 'NARUTO'] for s in series: # 重複しているシリーズ候補を タイトルに含むデータを取得 dup = df[df['タイトル'].str.contains(s)] print(dup[['開始', '回数', 'タイトル']]) # 開始 回数 タイトル # 299 8701.02 593 ジョジョの奇妙な冒険 # 300 8701.02 44 ジョジョの奇妙な冒険(第1部) # 314 8747.00 69 ジョジョの奇妙な冒険(第2部) # 335 8916.00 152 ジョジョの奇妙な冒険(第3部) # 378 9220.00 174 ジョジョの奇妙な冒険(第4部) # 433 9552.00 154 ジョジョの奇妙な冒険(第5部) # 489 1.00 [158] ジョジョの奇妙な冒険第6部・ストーンオーシャン # .....後略
上の例で言うと、"ジョジョの奇妙な冒険" シリーズの回数として 1部 〜 5部の回数分が入っているようだ。同様にシリーズ全体の連載回数に含まれていると思われるレコードを目視で抽出、削除。
# 削除対象タイトル dups = ['ジョジョの奇妙な冒険(第1部)', 'ジョジョの奇妙な冒険(第2部)', 'ジョジョの奇妙な冒険(第3部)', 'ジョジョの奇妙な冒険(第4部)', 'ジョジョの奇妙な冒険(第5部)', 'BASTARD!!〜暗黒の破壊神〜(闇の反逆軍団編)', 'BASTARD!!〜暗黒の破壊神〜(地獄の鎮魂歌編)', 'みどりのマキバオー(第1部)', 'みどりのマキバオー(第2部)', 'ONE PIECE《サバイバルの海 超新星編》', 'ONE PIECE《最後の海 新世界編》', 'ボボボーボ・ボーボボ(第1部)', '真説ボボボーボ・ボーボボ', 'DEATH NOTE(第1部)', 'DEATH NOTE(第2部)', 'スティール・ボール・ラン 1st Stage', 'スティール・ボール・ラン 2nd Stage', 'トリコ〜人間界編〜', 'トリコ〜グルメ界編〜', 'NARUTO(第一部)', 'NARUTO(第二部)'] # タイトルの値が dups に含まれるレコードを除外 df = df[~df['タイトル'].isin(dups)] # dups 分のレコードがフィルタされたことを確かめる assert len(df) + len(dups) + 1 == original_length df.shape # (652, 9)
ジャンルデータの作成
"ジャンル" カラムには複数のジャンルが カンマ区切りで含まれている。
df['ジャンル'] # 0 NaN # 1 NaN # 2 NaN # ... # 669 卓球,エロ,ミステリ # 670 エロ,ロボット # 671 学園,裁判 # Name: ジャンル, Length: 652, dtype: object
これを集計しやすい形にするため、各ジャンルに該当するかどうかを bool
でフラグ立てしたい。
このカラムには "秘密警察" とか "ゴム人間" なんて値も入っているため、ユニークな件数をカウントして上位ジャンルのみフィルタする。
まずは、カンマ区切りになっている値を分割して、ジャンル個々の値からなる Series
を作る。
# NaN をフィルタ genres = df['ジャンル'].dropna() # カンマ区切りの値を split して、一つのリストとして結合 genres = reduce(lambda x, y: x + y, genres.str.split(',')) # genres = pd.Series(genres) genres # 0 硬派 # 1 不良 # 2 アニメ化 # ... # 1358 ロボット # 1359 学園 # 1360 裁判 # Length: 1361, dtype: object
Series
内の要素がそれぞれいくつ含まれるかをカウントするには value_counts()
。
genres.value_counts() # ギャグ 76 # 学園 75 # アニメ化 74 # ... # 演歌 1 # 学園ショートギャグ 1 # スポーツ? 1 # Length: 426, dtype: int64 # 上位10件を表示 genres.value_counts()[:10] # ギャグ 76 # 学園 75 # アニメ化 74 # 格闘 36 # 一芸 32 # ファンタジー 32 # ゲーム化 30 # 変身 28 # ラブコメ 28 # 不良 23 # dtype: int64
とりあえず 以下の3ジャンルをみることにする。各レコードについてジャンルは複数あてはまりうる。そのため、それぞれのフラグについて dummy のカラムを作成し、ジャンルに該当する場合に True
を入れる。
genres = ['ギャグ', '格闘', 'ラブコメ'] # str アクセサを利用するため NaN 値をパディング df['ジャンル'] = df['ジャンル'].fillna('') for genre in genres: df[genre] = df['ジャンル'].str.contains(genre)
できあがり
# 必要なカラムのみにフィルタ df = df[['開始', '回数', 'タイトル', '連載中', '原作あり', 'ギャグ', '格闘', 'ラブコメ']] df
できたデータはこんな感じ。俺たちの分析はここからだ!
index | 開始 | 回数 | タイトル | 連載中 | 原作あり | ギャグ | 格闘 | ラブコメ |
---|---|---|---|---|---|---|---|---|
0 | 1968-01-08 | 131 | 父の魂 | False | NaN | False | False | False |
... | ... | ... | ... | ... | ... | ... | ... | ... |
667 | 2014-10-20 | 12 | ハイファイクラスタ | True | NaN | False | False | False |
668 | 2014-10-27 | 11 | Sporting Salt | True | NaN | False | False | False |
669 | 2014-12-22 | 3 | 卓上のアゲハ | True | NaN | False | False | False |
670 | 2014-12-29 | 2 | E−ROBOT | True | NaN | False | False | False |
671 | 2015-01-05 | 1 | 学糾法廷 | True | True | False | False | False |
まとめ
pandas
で文字列処理する場合は str
アクセサ。
補足 また、以下のサイトには毎週の掲載順位を含むデータがある。こちらのほうが興味深いが、前処理グレードがちょっと高かったのであきらめた。
おまけ: かんたんに集計
自分としては 想定の前処理ができた時点でもういいかって感じなのだが、簡単に集計してみる。
新連載数
各年 (年度ではなく calendar year) の新連載開始数を時系列でプロット。69年10月に週刊化されたらしく、直後の新連載数が特に多い。
import matplotlib.pyplot as plt summarised = df.groupby(df['開始'].dt.year)['タイトル'].count() ax = summarised.plot(figsize=(7, 3)) ax.set_title('新連載数')
連載回数
連載回数の頻度分布をみてみると、80年より前には10週以下で短期連載 or 連載終了した作品が比較的 多そう。80年代以降の分布が現在の感覚に近いと思う。
# 連載終了したデータのみにフィルタ fin = df[~df['連載中']] fin1 = fin[fin['開始'].dt.year < 1980] fin2 = fin[fin['開始'].dt.year >= 1980] fig, axes = plt.subplots(2, figsize=(7, 4)) bins = np.arange(0, 1000, 5) fin1['回数'].plot(kind='hist', ax=axes[0], bins=bins, xlim=(0, 100), label='80年より前に連載開始', legend=True) fin2['回数'].plot(kind='hist', ax=axes[1], bins=bins, xlim=(0, 100), label='80年以降に連載開始', legend=True, color='green') fig.suptitle('連載回数の頻度分布')
連載の継続率
連載の継続率 = 生存率とみて lifelines
を使って Kaplan-Meier 曲線を引く。横軸が時間経過 (週)、縦軸がその時点まで連載継続している確率になる。
lifelines
についてはこちら。
from lifelines import KaplanMeierFitter def to_pct(y, position): s = str(100 * y) return s + '%' from matplotlib.ticker import FuncFormatter pctformatter = FuncFormatter(to_pct) df2 = df[df['開始'].dt.year >= 1980] kmf = KaplanMeierFitter() kmf.fit(df2['回数'], event_observed=~df2['連載中']) ax = kmf.plot(figsize=(7, 3), xlim=(0, 200)) ax.yaxis.set_major_formatter(pctformatter)
50週 = 1年 連載継続できるのは 30 % くらいか、、、厳しい世界だホント。
ジャンル別の連載の継続率
上と同じ、でジャンル別。
ax = None for genre in genres: group = df2[df2[genre]] kmf = KaplanMeierFitter() kmf.fit(group['回数'], event_observed=~group['連載中'], label='ジャンル=' + genre) # 描画する Axes を指定。None を渡すとエラーになるので場合分け if ax is None: ax = kmf.plot(figsize=(7, 3), xlim=(0, 200), ci_show=False) else: ax = kmf.plot(ax=ax, figsize=(7, 3), xlim=(0, 200), ci_show=False) ax.yaxis.set_major_formatter(pctformatter)
もう少し偏るかと思ったがそうでもなかった。ラブコメは 150 回くらいに壁がありそう。ニセコイがこの壁を乗り越えてくれることを切に願う。
Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理
- 作者: Wes McKinney,小林儀匡,鈴木宏尚,瀬戸山雅人,滝口開資,野上大介
- 出版社/メーカー: オライリージャパン
- 発売日: 2013/12/26
- メディア: 大型本
- この商品を含むブログ (9件) を見る
Python pandas の算術演算 / 集約関数 / 統計関数まとめ
概要
恒例の pandas
記事。今回は 基本的な算術演算についてまとめた。このあたりの挙動は numpy
と一緒で直感的だと思うが、知っていないとハマるポイントがいくつかあるので。
準備
サンプルは DataFrame
のみ。だが内容は Series
でも同じ ( 行/列 2次元のデータに関するに記載は除く)。
import numpy as np import pandas as pd df = pd.DataFrame({'C1': [1, 1, 1], 'C2': [1, 1, 1], 'C3': [1, 1, 1]}) df # C1 C2 C3 # 0 1 1 1 # 1 1 1 1 # 2 1 1 1
四則演算
まずは基本的な例。
# スカラーの加算はデータ全体に対して適用 ( ブロードキャスト ) される df + 1 # C1 C2 C3 # 0 2 2 2 # 1 2 2 2 # 2 2 2 2 # np.array の加算は 各行に対する加算 df + np.array([1, 2, 3]) # C1 C2 C3 # 0 2 3 4 # 1 2 3 4 # 2 2 3 4 # 要素の長さが違う場合は ValueError df + np.array([1, 2]) # ValueError: Wrong number of items passed 2, place
とはいえ、演算をデータ全体へブロードキャストさせたいことはマレだと思う。特定の列 あるいは 特定セルに対して演算を行う場合は、以下の記事でまとめたデータ選択記法を使って計算結果を代入する。
補足 データへの演算自体は非破壊的な処理だが、代入によって元のデータが変更されることに注意。
# 1列目に [1, 2, 3] を足す df['C1'] = df['C1'] + np.array([1, 2, 3]) df # C1 C2 C3 # 0 2 1 1 # 1 3 1 1 # 2 4 1 1 # 3行目 / 3列目の値に 5 を足す df.iloc[2, 2] += 5 df # C1 C2 C3 # 0 2 1 1 # 1 3 1 1 # 2 4 1 6 # 複数列に対する演算も OK # (裏側では DataFrame 選択 -> 計算 -> 代入 をしているだけなので) df[['C1', 'C2']] -= 5 df # C1 C2 C3 # 0 -3 -4 1 # 1 -2 -4 1 # 2 -1 -4 6
算術演算メソッド
演算の挙動をもうすこし制御したい場合は、DataFrame.add
。DataFrame
や Series
は、add
のほかにも Python 標準モジュールの operators
と対応する算術演算メソッド / 論理演算メソッドを持つ。利用できるメソッドの一覧はこちら。オプションはどれも重要なので順番に。
列に対するブロードキャスト (axis=0
)
上記のような1列の加算ではなく、列全体に対してブロードキャストをしたい場合は axis=0
を指定。
axis=0
もしくはaxis='index'
で列に対する演算axis=1
もしくはaxis='columns'
で行に対する演算 (デフォルト)
# データは元に戻す df = pd.DataFrame({'C1': [1, 1, 1], 'C2': [1, 1, 1], 'C3': [1, 1, 1]}) df # C1 C2 C3 # 0 1 1 1 # 1 1 1 1 # 2 1 1 1 df.add(np.array([1, 2, 3]), axis=0) # C1 C2 C3 # 0 2 2 2 # 1 3 3 3 # 2 4 4 4 df.add(np.array([1, 2, 3]), axis='index') # 略
欠測値 ( NaN
) 要素へのパディング (fill_value
)
データに 欠測値 ( NaN
) が含まれるとき、その要素への演算の結果も NaN
になる。これは numpy
の挙動と同じ。
# numpy の挙動 np.nan + 1 # nan # NaN を含む DataFrame を定義する df_nan = pd.DataFrame({'C1': [1, np.nan, np.nan], 'C2': [np.nan, 1, np.nan], 'C3': [np.nan, np.nan, 1]}) df_nan # C1 C2 C3 # 0 1 NaN NaN # 1 NaN 1 NaN # 2 NaN NaN 1 # NaN を含むセルの演算結果は NaN df.add(df_nan) # C1 C2 C3 # 0 2 NaN NaN # 1 NaN 2 NaN # 2 NaN NaN 2
これに気づかず四則演算していると、なんか合計があわないな?ということになってしまう。この挙動を変えたい場合は fill_value
。演算の実行前に fill_value
で指定した値がパディングされる。ただし、演算対象の要素が 両方とも NaN
の場合は NaN
のまま。
df.add(df_nan, fill_value=0) # C1 C2 C3 # 0 2 1 1 # 1 1 2 1 # 2 1 1 2 # データの順序が変わっても有効 ( fill_value は演算対象 両方のデータに適用される) df_nan.add(df, fill_value=0) # C1 C2 C3 # 0 2 1 1 # 1 1 2 1 # 2 1 1 2 # 要素が 両方とも NaN の場合はパディングされない df_nan.add(df_nan, fill_value=0) # C1 C2 C3 # 0 2 NaN NaN # 1 NaN 2 NaN # 2 NaN NaN 2 # DataFrame について、対象データが array, Series の場合の処理は未実装 (NotImplementedError) なので注意 df.add(np.array([1, np.nan, 1]), fill_value=0) # NotImplementedError: fill_value 0 not supported
最後のオプション、 level
は index
が複数の階層 (レベル) 持つ場合 ( MultiIndex
を持つ場合 ) の制御を行う。このオプションの挙動は近日公開予定 (?) の MultiIndex
編で。
行列積
*
で DataFrame
同士の積をとった場合は、各要素同士の積になる。
df2 = pd.DataFrame({'C1': [2, 3], 'C2': [4, 5]}) df2 # C1 C2 # 0 2 4 # 1 3 5 df3 = pd.DataFrame({'C1': [1, -2], 'C2': [-3, 4]}) df3 # C1 C2 # 0 1 -3 # 1 -2 4 df2 * df3 # C1 C2 # 0 2 -12 # 1 -6 20
行列の積をとりたい場合は DataFrame.dot
。ただし、行列の積をとるためには元データの columns
と 引数の index
のラベルが一致している必要がある (説明 次節)。そのため、上のように columns
/ index
が一致しないデータから直接 行列の積をとることはできない。そんなときは DataFrame.values
プロパティで引数側の内部データを numpy.array
として取り出せばよい。
# NG! df2.dot(df3) # ValueError: matrices are not aligned # values プロパティで内部データを numpy.array として取り出せる df3.values # array([[ 1, -3], # [-2, 4]], dtype=int64) # OK! df2.dot(df3.values) # 0 1 # 0 -6 10 # 1 -7 11
引数側を転置すれば、元データの columns
と 引数の index
のラベルが一致する。このようなデータはそのまま DataFrame.dot
で積をとれる。
df3.T # 0 1 # C1 1 -2 # C2 -3 4 df2.dot(df3.T) # 0 1 # 0 -10 12 # 1 -12 14
補足 そもそも Python には行列積の演算子がない。が、現在 PEP-465 で行列積として @
演算子が提案されている。
ラベルによる演算の制御
ここまでは index
, columns
が一致するデータのみを対象にしてきた。実際には演算を行うデータの index
, columns
が異なることもある。その場合の挙動には少し注意が必要。
Series
, DataFrame
同士の演算は、 index
, columns
のラベルが一致する要素同士で行われる。例えば以下のように index
と columns
がずれているとき、対応しない要素は NaN
となる。
df4 = pd.DataFrame({'C1': [1, 1, 1], 'C2': [1, 1, 1], 'C3': [1, 1, 1]}) df4 # C1 C2 C3 # 0 1 1 1 # 1 1 1 1 # 2 1 1 1 df5 = pd.DataFrame({'C2': [1, 1, 1], 'C3': [1, 1, 1], 'C4': [1, 1, 1]}, index=[1, 2, 3]) df5 # C2 C3 C4 # 1 1 1 1 # 2 1 1 1 # 3 1 1 1 df4 + df5 # C1 C2 C3 C4 # 0 NaN NaN NaN NaN # 1 NaN 2 2 NaN # 2 NaN 2 2 NaN # 3 NaN NaN NaN NaN
どう直せばよいかは状況による。index
, columns
のずれ自体は OK で、 NaN
でのパディングが気に入らない場合は fill_value
。
df4.add(df5, fill_value=0) # C1 C2 C3 C4 # 0 1 1 1 NaN # 1 1 2 2 1 # 2 1 2 2 1 # 3 NaN 1 1 1
index
, columns
に関係なく、対応する位置の要素同士で演算したい場合は DataFrame.values
を使って取得した numpy.array
のみを渡せばよい。
df4 + df5.values # C1 C2 C3 # 0 2 2 2 # 1 2 2 2 # 2 2 2 2
もしくは、以下のようにして データの index
もしくは columns
を揃えてから演算する。
df4
のindex
をdf5
にそろえる場合は、df4.index
に代入。df5
のindex
をdf4
にそろえる場合は、df5.index
に代入。もしくは、DataFrame.reset_index(drop=True)
でindex
を 0 から振りなおし
# df4 の index を df5 にそろえる df4.index = [1, 2, 3] df4 # C1 C2 C3 # 1 1 1 1 # 2 1 1 1 # 3 1 1 1 # df5 の index を df4 にそろえる df5.reset_index(drop=True) # C2 C3 C4 # 0 1 1 1 # 1 1 1 1 # 2 1 1 1 # reset_index デフォルトでは、元の index をカラムとして残す df5.reset_index() # index C2 C3 C4 # 0 1 1 1 1 # 1 2 1 1 1 # 2 3 1 1 1
集約関数
Series
, DataFrame
は 合計を取得する sum
, 平均を計算する mean
などの一連の集約関数に対応するメソッドを持つ。一覧は こちら。
df6 = pd.DataFrame({'C1': ['A', 'B', 'C'], 'C2': [1, 2, 3], 'C3': [4, 5, 6]}) df6 # C1 C2 C3 # 0 A 1 4 # 1 B 2 5 # 2 C 3 6
集約系の関数は既定では列方向に対して適用される。基本的には 数値型のみが集約対象になる。が、一部の関数では数値以外の型に対してもがんばってくれるものもある。
# mean は数値型のみ集約 df6.mean() # C2 2 # C3 5 # dtype: float64 # sum は 文字列も集約 df6.sum() # C1 ABC # C2 6 # C3 15 # dtype: object # 数値以外が不要な場合は numeric_only=True df6.sum(numeric_only=True) # C2 6 # C3 15 # dtype: int64 # 行方向へ適用したい場合は axis = 1 # このとき、数値型以外は除外して関数適用される df6.sum(axis=1) # 0 5 # 1 7 # 2 9 # dtype: int64 # 明示的に含めようとするとエラー df6.sum(axis=1, numeric_only=False) # TypeError: cannot concatenate 'str' and 'long' objects
複数列のデータをまとめて関数適用したい、という場合もたまーにある。pandas
には直接対応するメソッドはないが、以下のようにすればできる。
# values.flatten() で 2列分の値を ひとつの numpy.array として取得 df6[['C2', 'C3']].values.flatten() # array([1, 4, 2, 5, 3, 6], dtype=int64) # 集約関数適用 np.mean(df6[['C2', 'C3']].values.flatten()) # 3.5
補足 numpy.array.flatten()
は1次元化した array
のコピーを返す。
統計関数
また、分散を計算する var
, 標準偏差を計算する std
などの統計関数に対応するメソッドもある。一覧は上と同じく こちら。関数の適用方向など、挙動は集約関数と一緒。
1点 覚えておくべき箇所は、pandas
では分散 / 標準偏差について不偏推定量の計算がデフォルトになっている。これは numpy
の挙動 ( 標本統計量を返す ) とは異なる。この挙動は pandas
, numpy
ともに ddof
オプションで制御できる。
pandas
: 不偏推定量の計算 (ddof=True
) がデフォルト。numpy
: 標本統計量の計算 (ddof=False
) がデフォルト。
補足 ddof
= Delta Degrees of Freedom
df7 = pd.DataFrame({'C1': [1, 2, 3, 4], 'C2': [4, 5, 6, 7], 'C3': [2, 3, 3, 2]}) # C1 C2 C3 # 0 1 4 2 # 1 2 5 3 # 2 3 6 3 # 3 4 7 2 # 不偏分散 df7.var() # C1 1.666667 # C2 1.666667 # C3 0.333333 # dtype: float64 # 標本分散 df7.var(ddof=False) # C1 1.25 # C2 1.25 # C3 0.25 # dtype: float64 # 標本分散 (numpy) np.var(df7) # C1 1.25 # C2 1.25 # C3 0.25 # dtype: float64 # 不偏標準偏差 df7.std() # C1 1.290994 # C2 1.290994 # C3 0.577350 # dtype: float64 # 標本標準偏差 df7.std(ddof=False) # C1 1.118034 # C2 1.118034 # C3 0.500000 # dtype: float64 # 標本標準偏差 (numpy) np.std(df7) # C1 1.118034 # C2 1.118034 # C3 0.500000 # dtype: float64
基本統計量の表示 ( describe
)
最後。基本統計量をまとめて計算したい場合は DataFrame.describe()
。
df7.describe() # C1 C2 C3 # count 4.000000 4.000000 4.00000 # mean 2.500000 5.500000 2.50000 # std 1.290994 1.290994 0.57735 # min 1.000000 4.000000 2.00000 # 25% 1.750000 4.750000 2.00000 # 50% 2.500000 5.500000 2.50000 # 75% 3.250000 6.250000 3.00000 # max 4.000000 7.000000 3.00000
まとめ
pandas
での算術演算、集約関数、統計関数の挙動をまとめた。ポイントは、
DataFrame
への演算は適用される方向に注意。演算方向を指定する場合、列方向ならaxis=0
, 行方向はaxis=1
。- 演算したい要素に
NaN
が含まれる場合、必要に応じてfill_value
。 - 演算したい要素同士のラベルは一致させること。
Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理
- 作者: Wes McKinney,小林儀匡,鈴木宏尚,瀬戸山雅人,滝口開資,野上大介
- 出版社/メーカー: オライリージャパン
- 発売日: 2013/12/26
- メディア: 大型本
- この商品を含むブログ (9件) を見る
pandas でメモリに乗らない 大容量ファイルを上手に扱う
概要
分析のためにデータ集めしていると、たまに マジか!? と思うサイズの CSV に出くわすことがある。なぜこんなに育つまで放っておいたのか、、、? このエントリでは普通には開けないサイズの CSV を pandas
を使ってうまいこと処理する方法をまとめたい。
サンプルデータ
たまには実データ使おう、ということで WorldBankから GDPデータを落とす。以下のページ右上の "DOWNLOAD DATA" ボタンで CSV を選択し、ローカルに zip を保存する。解凍した "ny.gdp.mktp.cd_Indicator_en_csv_v2.csv" ファイルをサンプルとして使う。
http://data.worldbank.org/indicator/NY.GDP.MKTP.CD?page=1
補足 pandas
の Remote Data Access で WorldBank のデータは直接 落っことせるが、今回は ローカルに保存した csv を読み取りたいという設定で。
chunksize
を使って ファイルを分割して読み込む
まず、pandas
で普通に CSV を読む場合は以下のように pd.read_csv
を使う。サンプルデータではヘッダが 3行目から始まるため、冒頭の 2行を skiprows
オプションで読み飛ばす。
import pandas as pd fname = 'ny.gdp.mktp.cd_Indicator_en_csv_v2.csv' df = pd.read_csv(fname, skiprows=[0, 1]) df.shape # (258, 59) df.head() # Country Name Country Code Indicator Name Indicator Code 1961 # 0 Aruba ABW GDP (current US$) NY.GDP.MKTP.CD NaN # 1 Andorra AND GDP (current US$) NY.GDP.MKTP.CD NaN # 2 Afghanistan AFG GDP (current US$) NY.GDP.MKTP.CD 5.488889e+08 # 3 Angola AGO GDP (current US$) NY.GDP.MKTP.CD NaN # 4 Albania ALB GDP (current US$) NY.GDP.MKTP.CD NaN # # [5 rows x 59 columns]
では読み取り対象ファイルが メモリに乗らないほど大きい場合はどうするか? read_csv
に chunksize
オプションを指定することでファイルの中身を 指定した行数で分割して読み込むことができる。chunksize
には 1回で読み取りたい行数を指定する。例えば 50 行ずつ読み取るなら、chunksize=50
。
reader = pd.read_csv(fname, skiprows=[0, 1], chunksize=50)
chunksize
を指定したとき、返り値は DataFrame
ではなく TextFileReader
インスタンスとなる。
type(reader) # pandas.io.parsers.TextFileReader
TextFileReader
を for
でループさせると、ファイルの中身を指定した行数ごとに DataFrame
として読み取る。TextFileReader
は 現時点での読み取り位置 (ポインタ) を覚えており、ファイルの中身をすべて読み取るとループが終了する。
for r in reader: print(type(r), r.shape) # (<class 'pandas.core.frame.DataFrame'>, (50, 59)) # (<class 'pandas.core.frame.DataFrame'>, (50, 59)) # (<class 'pandas.core.frame.DataFrame'>, (50, 59)) # (<class 'pandas.core.frame.DataFrame'>, (50, 59)) # (<class 'pandas.core.frame.DataFrame'>, (50, 59)) # (<class 'pandas.core.frame.DataFrame'>, (8, 59))
もしくは、TextFileReader.get_chunk
で、現在の読み取り位置 (ポインタ) から N 行を読み取る。
# 上のループで全ての要素は読み取り済みになっているので、再度 reader を初期化 reader = pd.read_csv(fname, skiprows=[0, 1], chunksize=50) # 先頭から 5行を読み込み reader.get_chunk(5) # Country Name Country Code Indicator Name Indicator Code 1961 # 0 Aruba ABW GDP (current US$) NY.GDP.MKTP.CD NaN # 1 Andorra AND GDP (current US$) NY.GDP.MKTP.CD NaN # 2 Afghanistan AFG GDP (current US$) NY.GDP.MKTP.CD 5.488889e+08 # 3 Angola AGO GDP (current US$) NY.GDP.MKTP.CD NaN # 4 Albania ALB GDP (current US$) NY.GDP.MKTP.CD NaN # # [5 rows x 59 columns] # 次の4行 (6行目 - 9行目)を読み込み reader.get_chunk(4) # Country Name Country Code Indicator Name Indicator Code 1961 # 0 Andean Region ANR GDP (current US$) NY.GDP.MKTP.CD NaN # 1 Arab World ARB GDP (current US$) NY.GDP.MKTP.CD NaN # 2 United Arab Emirates ARE GDP (current US$) NY.GDP.MKTP.CD NaN # 3 Argentina ARG GDP (current US$) NY.GDP.MKTP.CD NaN # # [4 rows x 59 columns]
補足 chunksize
オプションは、 read_csv
と read_table
で利用できる 。
補足 pandas
の read_csv
はかなり速いので、パフォーマンス面でも pandas
を使うのはよいと思う。
A new high performance, memory-efficient file parser engine for pandas | Wes McKinney
python - Fastest way to parse large CSV files in Pandas - Stack Overflow
でも、分割された DataFrame
を扱うのはめんどうなんだけど?
分析に必要なレコードは 全データのごく一部、なんてこともよくある。chunk
させて読み込んだ 各グループに対して前処理をかけると、サイズが劇的に減ってメモリに乗る。そんなときは、前処理をかけた後、各 DataFrame
を結合してひとつにしたい。
DataFrame
の結合には pd.concat
。TextFileReader
から読み込んだ 各 DataFrame
はそれぞれが 0 から始まる index
を持つ。pd.concat
は既定では index
を維持して DataFrame
を結合するため、そのままでは index
が重複してしまう。 重複を避けるためには、結合後に index
を振りなおす = ignore_index=True
を指定する必要がある。
よく使う表現は、
def preprocess(x): # 不要な行, 列のフィルタなど、データサイズを削減する処理 return x reader = pd.read_csv(fname, skiprows=[0, 1], chunksize=50) df = pd.concat((preprocess(r) for r in reader), ignore_index=True) df.shape # (258, 59) df.head() # Country Name Country Code Indicator Name Indicator Code 1961 # 0 Aruba ABW GDP (current US$) NY.GDP.MKTP.CD NaN # 1 Andorra AND GDP (current US$) NY.GDP.MKTP.CD NaN # 2 Afghanistan AFG GDP (current US$) NY.GDP.MKTP.CD 5.488889e+08 # 3 Angola AGO GDP (current US$) NY.GDP.MKTP.CD NaN # 4 Albania ALB GDP (current US$) NY.GDP.MKTP.CD NaN # # [5 rows x 59 columns]
メモリ上の DataFrame
のサイズを確認したい
DataFrame
を適切な大きさに分割するために、DataFrame
のメモリ上の占有サイズが知りたい。これは v0.15.0 以降であれば可能。
DataFrame
全体のメモリ上のサイズを表示するには DataFrame.info()
。 表示された情報の最終行、 "memory size" をみる。ただし、この表示には object
型のカラムが占めているメモリは含まれないので注意。"+" がついているのは実際の占有領域が表示よりも大きいことを示す。
df.info() # <class 'pandas.core.frame.DataFrame'> # Int64Index: 258 entries, 0 to 257 # Data columns (total 59 columns): # Country Name 258 non-null object # Country Code 258 non-null object # Indicator Name 258 non-null object # Indicator Code 258 non-null object # 1961 127 non-null float64 # ... # 2012 222 non-null float64 # 2013 216 non-null float64 # 2014 0 non-null float64 # Unnamed: 58 0 non-null float64 # dtypes: float64(55), object(4) # memory usage: 116.9+ KB
ちゃんと表示する場合は DataFrame.memory_usage(index=True)
。index=True
は index
の占有メモリも表示に含めるオプション。表示はカラム別になるため、全体のサイズを見たい場合は sum
をとる。
df.memory_usage(index=True) # Index 2064 # Country Name 1032 # Country Code 1032 # Indicator Name 1032 # Indicator Code 1032 # 1961 2064 # ... # 2013 2064 # 2014 2064 # Unnamed: 58 2064 # Length: 60, dtype: int64 df.memory_usage(index=True).sum() # 119712
DataFrame
を複製せずに処理する
DataFrame
のメソッドを呼び出した場合、一般には DataFrame
をコピーしてから処理を行って返す (非破壊的な処理を行う)。大容量の DataFrame
ではこのコピーによって 実メモリのサイズを超え MemoryError
になる / もしくはコピーに非常に時間がかかることがある。
例えば、欠損値の補完を fillna
で行う処理をみてみる。fillna
の返り値では NaN
が 0 でパディングされているが、元データは変更されていない。fillna
では、コピーした DataFrame
に対して処理を行っていることが "1961" カラムの値を比較するとわかる。
df.fillna(0).head() # Country Name Country Code Indicator Name Indicator Code 1961 # 0 Aruba ABW GDP (current US$) NY.GDP.MKTP.CD 0.000000e+00 # 1 Andorra AND GDP (current US$) NY.GDP.MKTP.CD 0.000000e+00 # 2 Afghanistan AFG GDP (current US$) NY.GDP.MKTP.CD 5.488889e+08 # 3 Angola AGO GDP (current US$) NY.GDP.MKTP.CD 0.000000e+00 # 4 Albania ALB GDP (current US$) NY.GDP.MKTP.CD 0.000000e+00 # dfはそのまま df.head() # Country Name Country Code Indicator Name Indicator Code 1961 # 0 Aruba ABW GDP (current US$) NY.GDP.MKTP.CD NaN # 1 Andorra AND GDP (current US$) NY.GDP.MKTP.CD NaN # 2 Afghanistan AFG GDP (current US$) NY.GDP.MKTP.CD 5.488889e+08 # 3 Angola AGO GDP (current US$) NY.GDP.MKTP.CD NaN # 4 Albania ALB GDP (current US$) NY.GDP.MKTP.CD NaN # # [5 rows x 59 columns]
このコピー処理を避けたい場合は inplace=True
を指定する。こいつが指定されると、各メソッドは DataFrame
のコピーを行わず、呼び出し元の DataFrame
を直接処理する (破壊的な処理を行う)。また、inplace=True
が指定されたメソッドは返り値をもたない ( None
を返す ) ため、代入を行ってはならない。
# 返り値はないため、代入してはダメ df.fillna(0, inplace=True) # df 自体が変更された df.head() # Country Name Country Code Indicator Name Indicator Code 1961 # 0 Aruba ABW GDP (current US$) NY.GDP.MKTP.CD 0.000000e+00 # 1 Andorra AND GDP (current US$) NY.GDP.MKTP.CD 0.000000e+00 # 2 Afghanistan AFG GDP (current US$) NY.GDP.MKTP.CD 5.488889e+08 # 3 Angola AGO GDP (current US$) NY.GDP.MKTP.CD 0.000000e+00 # 4 Albania ALB GDP (current US$) NY.GDP.MKTP.CD 0.000000e+00 # # [5 rows x 59 columns]
やりたいメソッドが inplace
オプションを持つかどうかは API ガイド で確認。
eval
による処理の最適化
numexpr
のインストール
pd.eval
を使うには numexpr
パッケージが必要なので、入っていなければインストールする。
pip install numexpr
eval
のメリット
pd.eval
では文字列表現された式を受け取って評価できる。pd.eval
を使うと以下 2つのメリットがある。
- 大容量の
DataFrame
を効率的に処理できる - 数値演算を一括して処理できる
補足 numexpr
のソースは読んだことがないので詳細不明だが、 pandas
では連続するオペレータは逐次処理されていく。例えば "A + B + C" であれば まず "A + B" の結果から DataFrame
を作って、さらに "+ C" して DataFrame
を作る。DataFrame
生成にはそれなりにコストがかかるので この辺の内部処理が最適化されるのかな、と思っている。
補足 逆に、小さい処理では eval
から numexpr
を呼び出すコストの方が メリットよりも大きくなるため、pd.eval
を使う場合は通常処理とのパフォーマンス比較を行ってからにすべき。今回のサンプルくらいのデータサイズであれば eval
よりも 通常の演算のほうが速い。
eval
でサポートされる式表現は、公式ドキュメントにあるとおり、
- 数値演算オペレータ。ただし、シフト演算
<<
と>>
を除く。 - 比較演算オペレータ。連結も可能 (
2 < df < df2
とか) - 論理演算オペレータ (
not
も含む)。 list
とtuple
のリテラル表現 ([1, 2]
や(1, 2)
)- プロパティアクセス (
df.a
) __getitem__
でのアクセス (df[0]
)
eval
の利用
例えばサンプルデータの "2011" 〜 "2013" カラムの値を足し合わせたいときはこう書ける。
pd.eval("df['2011'] + df['2012'] + df['2013']") # 0 2.584464e+09 # 1 0.000000e+00 # 2 5.910162e+10 # 3 3.411516e+11 # 4 3.813926e+10 # ... # 256 6.218183e+10 # 257 3.623061e+10 # Length: 258, dtype: float64
また、pd.eval
ではなく DataFrame.eval
として呼んだ場合、式表現中はデータのカラム名を変数名としてもつ名前空間で評価される ( DataFrame.query
と同じ )。DataFrame.query
については以下の記事参照。
まとめ
pandas
で大容量ファイルを扱うときは、
chunksize
で分割して読み込み- メソッド呼び出しは
inplace=True
- オペレータを使う処理は
pd.eval
これさえ覚えておけば、大容量ファイルが送りつけられた場合でも安心。
Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理
- 作者: Wes McKinney,小林儀匡,鈴木宏尚,瀬戸山雅人,滝口開資,野上大介
- 出版社/メーカー: オライリージャパン
- 発売日: 2013/12/26
- メディア: 大型本
- この商品を含むブログ (9件) を見る
Python pandas データ選択処理をちょっと詳しく <後編>
概要
こちらの続き。これで pandas
でのデータ選択についてはひとまず終わり。
Python pandas データ選択処理をちょっと詳しく <前編> - StatsFragments
Python pandas データ選択処理をちょっと詳しく <中編> - StatsFragments
サンプルデータの準備
データは 前編と同じものを使う。ただし変数名は変えた。
import pandas as pd s1 = pd.Series([1, 2, 3], index = ['I1', 'I2', 'I3']) df1 = pd.DataFrame({'C1': [11, 21, 31], 'C2': [12, 22, 32], 'C3': [13, 23, 33]}, index = ['I1', 'I2', 'I3']) s1 # I1 1 # I2 2 # I3 3 # dtype: int64 df1 # C1 C2 C3 # I1 11 12 13 # I2 21 22 23 # I3 31 32 33
where
でのデータ選択
これまでみたとおり、Series
から __getitem__
すると、条件に該当する要素のみが返ってくる。
s1[s1 > 2] # I3 3 # dtype: int64
が、ときには元データと同じ長さのデータがほしい場合がある。Series.where
を使うと 条件に該当しない ラベルは NaN
でパディングし、元データと同じ長さの結果を返してくれる。
where
の引数はデータと同じ長さの bool
型の numpy.array
もしくは Series
である必要がある。まあ Series
への論理演算では長さは変わらないので、以下のような使い方なら特別 意識する必要はないかも。
s1.where(s1 > 2) # I1 NaN # I2 NaN # I3 3 # dtype: float64
NaN
以外でパディングしたいんだよ、ってときは 第二引数に パディングに使う値を渡す。NaN
でなく 0 でパディングしたければ、
s1.where(s1 > 2, 0) # I1 0 # I2 0 # I3 3 # dtype: int64
また、第二引数にはデータと同じ長さの numpy.array
や Series
も渡せる。このとき、パディングはそれぞれ対応する位置にある値で行われ、第一引数の条件に該当しないデータを 第二引数で置換するような動きになる。つまり if - else
のような表現だと考えていい。
# 第一引数の条件に該当しない s1 の 1, 2番目の要素が array の 1, 2 番目の要素で置換される s1.where(s1 > 2, np.array([4, 5, 6])) # I1 4 # I2 5 # I3 3 # dtype: int64 # 置換用の Series を作る s2 = pd.Series([4, 5, 6], index = ['I1', 'I2', 'I3']) s2 # I1 4 # I2 5 # I3 6 # dtype: int64 # 第一引数の条件に該当しない s1 の 1, 2番目の要素が Series s2 の 1, 2 番目の要素で置換される s1.where(s1 > 2, s2) # I1 4 # I2 5 # I3 3 # dtype: int64
DataFrame
でも同様。
df1.where(df1 > 22) # C1 C2 C3 # I1 NaN NaN NaN # I2 NaN NaN 23 # I3 31 32 33 # 0 でパディング df1.where(df1 > 22, 0) # C1 C2 C3 # I1 0 0 0 # I2 0 0 23 # I3 31 32 33 # 置換用の DataFrame を作る df2 = pd.DataFrame({'C1': [44, 54, 64], 'C2': [45, 55, 65], 'C3': [46, 56, 66]}, index = ['I1', 'I2', 'I3']) df2 # C1 C2 C3 # I1 44 45 46 # I2 54 55 56 # I3 64 65 66 # df1 のうち、22以下の値を df2 の値で置換 df1.where(df1 > 22, df2) # C1 C2 C3 # I1 44 45 46 # I2 54 55 23 # I3 31 32 33
where
がことさら便利なのは以下のようなケース。
DataFrame
に新しいカラムを作りたい。 (あるいは既存の列の値を置き換えたい)- 新しいカラムは、"C2" 列の値が 30を超える場合は "C1" 列の値を使う。
- それ以外は "C3" 列の値を使う。
これが一行でかける。
df1['C4'] = df1['C1'].where(df1['C2'] > 30, df1['C3']) df1 # C1 C2 C3 C4 # I1 11 12 13 13 # I2 21 22 23 23 # I3 31 32 33 31
mask
でのデータ選択
where
の逆の操作として mask
がある。こちらは第一引数の条件に該当するセルを NaN
でマスクする。ただし第二引数の指定はできないので 使いどころは限られる。
2015/05/06追記 v0.16.1以降で、DataFrame.mask
は where
と同じ引数を処理できるようになった。
df1.mask(df1 > 22) # C1 C2 C3 C4 # I1 11 12 13 13 # I2 21 22 NaN NaN # I3 NaN NaN NaN NaN # v0.16.1以降 df1.mask(df1 > 22, 0) # C1 C2 C3 # I1 11 12 13 # I2 21 22 0 # I3 0 0 0
query
でのデータ選択
最後にquery
。query
で何か新しい処理ができるようになるわけではないが、__getitem__
と同じ操作がよりシンプル な表現で書ける。
numexpr
のインストール
query
を使うには numexpr
パッケージが必要なので、入っていなければインストールする。
pip install numexpr
サンプルデータの準備
また、さきほどの例で列追加したのでサンプルデータを作り直す。
df1 = pd.DataFrame({'C1': [11, 21, 31], 'C2': [12, 22, 32], 'C3': [13, 23, 33]}, index = ['I1', 'I2', 'I3']) df1 # C1 C2 C3 # I1 11 12 13 # I2 21 22 23 # I3 31 32 33
query
の利用
__getitem__
を利用したデータ選択では、論理演算の組み合わせで bool
の Series
を作ってやる必要がある。そのため、[]
内で元データへの参照 ( 下の例では df1
)が繰り返しでてくるし、複数条件の組み合わせの際は 演算順序の都合上 ()
がでてきたりと式が複雑になりがち。これはわかりにくい。
df1[df1['C1'] > 20] # C1 C2 C3 # I2 21 22 23 # I3 31 32 33 df1[df1['C2'] < 30] # C1 C2 C3 # I1 11 12 13 # I2 21 22 23 df1[(df1['C1'] > 20) & (df1['C2'] < 30)] # C1 C2 C3 # I2 21 22 23
同じ処理は query
を使うとすっきり書ける。query
の引数にはデータ選択に使う条件式を文字列で渡す。この式が評価される名前空間 = query
名前空間の中では、query
を呼び出したデータの列名が あたかも変数のように参照できる。
df1.query('C1 > 20 & C2 < 30') # C1 C2 C3 # I2 21 22 23
ただし、query
名前空間の中で使える表現は限られるので注意。例えば以下のような メソッド呼び出しはできない。
# NG! df1.query('C1.isin([11, 21])') # NotImplementedError: 'Call' nodes are not implemented
同じ処理を行う場合は in
演算子を使う。
# in を使えば OK df1.query('C1 in [11, 21]') # C1 C2 C3 # I1 11 12 13 # I2 21 22 23
また、numexpr
で利用できる関数 の呼び出しは query
名前空間上では使えないっぽい。
df1.query('C1 > sqrt(400)') # NotImplementedError: 'Call' nodes are not implemented
そのため、query
に渡す式表現では論理表現 以外を含めないほうがよい。条件によって式表現を変えたい、なんて場合は 式表現を都度 文字列として連結 + 生成するか、ローカル変数に計算結果を入れて式表現に渡す。
ローカル変数を式表現中に含める際は、変数名を @
ではじめる。
x = 20 # NG! df1.query('C1 > x') # UndefinedVariableError: name 'x' is not defined # OK! df1.query('C1 > @x') # C1 C2 C3 # I2 21 22 23 # I3 31 32 33
query
名前空間上で index
の値を参照する場合は、式表現中で index
と指定する。
df1.query('index in ["I1", "I2"]') # C1 C2 C3 # I1 11 12 13 # I2 21 22 23
index
が列名と重複した場合は 列名が優先。
df_idx = pd.DataFrame({'index': [1, 2, 3]}, index=[3, 2, 1]) df_idx # index # 3 1 # 2 2 # 1 3 df_idx.query('index >= 2') # index # 2 2 # 1 3
まとめ
where
を使うと 元データと同じ形のデータが取得できる。また、if - else
的表現が一行で書ける。- 複数の条件式を組み合わせる場合は
query
を使うとシンプルに書ける。
全三回で pandas
でのデータ選択処理をまとめた。公式ドキュメント、またAPIガイドには ここで記載しなかったメソッド / 例もあるので、より尖った使い方をしたい人は読んでおくといいと思う。
Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理
- 作者: Wes McKinney,小林儀匡,鈴木宏尚,瀬戸山雅人,滝口開資,野上大介
- 出版社/メーカー: オライリージャパン
- 発売日: 2013/12/26
- メディア: 大型本
- この商品を含むブログ (9件) を見る
Python pandas データ選択処理をちょっと詳しく <中編>
こちらの続き。
上の記事では bool
でのデータ選択について 最後にしれっと書いて終わらせたのだが、一番よく使うところなので中編として補足。
まず __getitem__
や ix
の記法では、次のような指定によって 行 / 列を選択することができた。
index
,columns
のラベルを直接指定しての選択index
,columns
の番号(順序)を指定しての選択index
,columns
に対応するbool
のリストを指定しての選択
ここでは上記の選択方法をベースとして、ユースケースごとに Index
や Series
のプロパティ / メソッドを使ってできるだけシンプルにデータ選択を行う方法をまとめる。
補足 一部の内容はこちらの記事ともかぶる。下の記事のほうが簡単な内容なので、必要な方はまずこちらを参照。
簡単なデータ操作を Python pandas で行う - StatsFragments
準備
今回のサンプルは DataFrame
だけで。
import pandas as pd import numpy as np df = pd.DataFrame({'N1': [1, 2, 3, 4, 5, 6], 'N2': [10, 20, 30, 40, 50, 60], 'N3': [6, 5, 4, 3, 2, 1], 'F1': [1.1, 2.2, 3.3, 4.4, 5.5, 6.6], 'F2': [1.1, 2.2, 3.3, 4.4, 5.5, 6.6], 'S1': ['A', 'b', 'C', 'D', 'E', 'F'], 'S2': ['A', 'X', 'X', 'X', 'E', 'F'], 'D1': pd.date_range('2014-11-01', freq='D', periods=6)}, index=pd.date_range('2014-11-01', freq='M', periods=6), columns=['N1', 'N2', 'N3', 'F1', 'F2', 'S1', 'S2', 'D1']) df # N1 N2 N3 F1 F2 S1 S2 D1 # 2014-11-30 1 10 6 1.1 1.1 A A 2014-11-01 # 2014-12-31 2 20 5 2.2 2.2 b X 2014-11-02 # 2015-01-31 3 30 4 3.3 3.3 C X 2014-11-03 # 2015-02-28 4 40 3 4.4 4.4 D X 2014-11-04 # 2015-03-31 5 50 2 5.5 5.5 E E 2014-11-05 # 2015-04-30 6 60 1 6.6 6.6 F F 2014-11-06
補足 後の例のために index
は月次の日付型にした。pd.date_range
の使い方は以下の記事参照。
Python pandas で日時関連のデータ操作をカンタンに - StatsFragments
以下の例では __getitem__
を使ってシンプルに書けるところは __getitem__
で書く。もちろん ix
, (ラベルや番号なら loc
, iloc
) を使っても書ける。
index
, columns
のラベルを特定の条件で選択
前編で index
, columns
のラベルそのものを直接指定すればデータ選択できるのはわかった。が、ラベルが特定の条件を満たすとき (ラベルが特定の文字で始まるとか) だけ選択するにはどうすれば?というのがここでの話。
補足 内部実装の話になるが、 index
, columns
は どちらも pd.Index
型のクラスが使われている ( DatetimeIndex
は Index
のサブクラス)。index
, columns
とも裏側にあるオブジェクトは同一のため、このセクションで記載する方法は 行 / 列が入れ替わっても使える。
df.index # <class 'pandas.tseries.index.DatetimeIndex'> # [2014-11-30, ..., 2015-04-30] # Length: 6, Freq: M, Timezone: None df.columns # Index([u'N1', u'N2', u'N3', u'F1', u'F2', u'S1', u'S2', u'D1'], dtype='object')
ラベルに関数適用して選択したい
pd.Index.map
。たとえば 大文字の "N" から始まるラベル名のみ抽出したいなら
df.columns.map(lambda x: x.startswith('N')) # array([ True, True, True, False, False, False, False, False], dtype=bool) df.ix[:, df.columns.map(lambda x: x.startswith('N'))] # N1 N2 N3 # 2014-11-30 1 10 6 # 2014-12-31 2 20 5 # 2015-01-31 3 30 4 # 2015-02-28 4 40 3 # 2015-03-31 5 50 2 # 2015-04-30 6 60 1
ということで map
を使えば index
, columns
のラベルに対してあらゆる関数を適用してデータ選択できる。
さらに、よく使うと思われるケースではより簡便な方法が用意されている。
2015/05/06追記 v0.16.1 で Index
にも .str
アクセサが追加され、文字列処理関数を直接呼び出せるようになった。そのため、上の例は df.ix[:, df.columns.str.startswith('N')]
とも書ける。.str
アクセサについては以降の記載を参照。
リストに含まれるラベルだけ選択したい
たとえば選択したいラベルのリストがあり、そこに含まれるものだけ選択したいなんてことがある。選択候補リストに余計なラベルが含まれていると、__getitem__
では KeyError
になり、ix
では NaN
(値のない) 列ができてしまう。
df[['N1', 'N2', 'N4']] # KeyError: "['N4'] not in index" df.ix[:, ['N1', 'N2', 'N4']] # N1 N2 N4 # 2014-11-30 1 10 NaN # 2014-12-31 2 20 NaN # 2015-01-31 3 30 NaN # 2015-02-28 4 40 NaN # 2015-03-31 5 50 NaN # 2015-04-30 6 60 NaN
いや NaN
とかいいから 存在する列だけが欲しいんだけど、、、というときに pd.Index.isin
。
df.columns.isin(['N1', 'N2', 'N4']) # array([ True, True, False, False, False, False, False, False], dtype=bool) df.ix[:, df.columns.isin(['N1', 'N2', 'N4'])] # N1 N2 # 2014-11-30 1 10 # 2014-12-31 2 20 # 2015-01-31 3 30 # 2015-02-28 4 40 # 2015-03-31 5 50 # 2015-04-30 6 60
ラベルをソートして選択したい
pd.Index.order
。columns
をアルファベット順に並べ替えて、前から3つを取得したければ、
df.columns.order() # Index([u'D1', u'F1', u'F2', u'N1', u'N2', u'N3', u'S1', u'S2'], dtype='object') df.columns.order()[:3] # Index([u'D1', u'F1', u'F2'], dtype='object') df[df.columns.order()[:3]] # D1 F1 F2 # 2014-11-30 2014-11-01 1.1 1.1 # 2014-12-31 2014-11-02 2.2 2.2 # 2015-01-31 2014-11-03 3.3 3.3 # 2015-02-28 2014-11-04 4.4 4.4 # 2015-03-31 2014-11-05 5.5 5.5 # 2015-04-30 2014-11-06 6.6 6.6
特定の年, 月, etc... のデータだけ選択したい
DatetimeIndex
へのプロパティアクセスを使う。使えるプロパティはこちら。
index
が 2015年の日付になっている行のみ抽出するときは、pd.DatetimeIndex.year
で 年のみを含む numpy.array
を作って論理演算する。
df.index.year # array([2014, 2014, 2015, 2015, 2015, 2015], dtype=int32) df.index.year == 2015 # array([False, False, True, True, True, True], dtype=bool) df[df.index.year == 2015] # N1 N2 N3 F1 F2 S1 S2 D1 # 2015-01-31 3 30 4 3.3 3.3 C X 2014-11-03 # 2015-02-28 4 40 3 4.4 4.4 D X 2014-11-04 # 2015-03-31 5 50 2 5.5 5.5 E E 2014-11-05 # 2015-04-30 6 60 1 6.6 6.6 F F 2014-11-06
補足 前編でまとめた原則には書かなかったが、DatetimeIndex
(と PeriodIndex
という日時関連の別クラス ) を index
に持つ Series
, DataFrame
では、 例外的に __getitem__
の引数として日時-like な文字列が使えたりもする。詳しくは こちら。そのため、同じ処理は以下のようにも書ける。
df['2015'] # N1 N2 N3 F1 F2 S1 S2 D1 # 2015-01-31 3 30 4 3.3 3.3 C X 2014-11-03 # 2015-02-28 4 40 3 4.4 4.4 D X 2014-11-04 # 2015-03-31 5 50 2 5.5 5.5 E E 2014-11-05 # 2015-04-30 6 60 1 6.6 6.6 F F 2014-11-06
ラベルが重複したデータを削除したい
2015/05/06修正 v0.16向けの内容に変更
これは v0.16 以降であれば簡単。例示のため index
に "A" が3つ重複したデータを作る。
df_dup = pd.DataFrame({'N1': [1, 2, 3, 4], 'N2': [6, 5, 4, 3], 'S1': ['A', 'B', 'C', 'D']}, index=['A', 'A', 'A', 'B']) df_dup # N1 N2 S1 # A 1 6 A # A 2 5 B # A 3 4 C # B 4 3 D
Index.duplicated()
で Index
の値が重複しているかどうかがわかるため、これを使って列選択すればよい。重複している値が True
となっているため ~
で論理否定をとって行選択すると、
df_dup.index.duplicated() # array([False, True, True, False], dtype=bool) df_dup[~df_dup.index.duplicated()] # N1 N2 S1 # A 1 6 A # B 4 3 D
このとき、各重複グループの一番最初のデータは削除されない。この挙動は take_last
オプションで変更できる。
take_last=False (Default)
: 重複しているグループの最初のデータを残すtake_last=True
: 重複しているグループの最後のデータを残す。
df_dup[~df_dup.index.duplicated(take_last=True)] # N1 N2 S1 # A 3 4 C # B 4 3 D
いやいや重複データは全削除したいんですけど?という場合は Dummy 列で groupby
して filter
。
df_dup.groupby(["Dummy"]).filter(lambda x:x.shape[0] == 1) # N1 N2 S1 Dummy # B 4 3 D B df_dup.groupby(["Dummy"]).filter(lambda x:x.shape[0] == 1)[['N1', 'N2', 'S1']] # N1 N2 S1 # B 4 3 D
補足 直感的でないな、と思った方が多いと思うが 開発者でもタスクとしては認識している ので、、、。
列, 行の値から特定の条件で選択
上のセクションでは index
, columns
自体のラベルからデータ選択する方法を書いた。ここからは データの中身 (行 / 列 / もしくは各セルの値 ) をもとにデータ選択する方法を記載する。
補足 DataFrame
では 実データは列持ち (各列が特定の型のデータを保持している) なので、ここからの方法では行/列の方向を意識する必要がある。
特定の型の列のみ取り出す
DataFrame.dtypes
プロパティで各カラムの型が取得できるので、それらに対して論理演算をかける。
df.dtypes # N1 int64 # N2 int64 # N3 int64 # F1 float64 # F2 float64 # S1 object # S2 object # D1 datetime64[ns] # dtype: object df.dtypes == np.float64 # N1 False # N2 False # N3 False # F1 True # F2 True # S1 False # S2 False # D1 False # dtype: bool df.ix[:, df.dtypes == np.float64] # F1 F2 # 2014-11-30 1.1 1.1 # 2014-12-31 2.2 2.2 # 2015-01-31 3.3 3.3 # 2015-02-28 4.4 4.4 # 2015-03-31 5.5 5.5 # 2015-04-30 6.6 6.6
値が特定の条件を満たす行/列を選択したい
たいていは 普通の演算でいける。"N1" カラムの値が偶数の行だけ抽出するには、
df['N1'] % 2 == 0 # 2014-11-30 False # 2014-12-31 True # 2015-01-31 False # 2015-02-28 True # 2015-03-31 False # 2015-04-30 True # Freq: M, Name: N1, dtype: bool df[df['N1'] % 2 == 0] # N1 N2 N3 F1 F2 S1 S2 D1 # 2014-12-31 2 20 5 2.2 2.2 b X 2014-11-02 # 2015-02-28 4 40 3 4.4 4.4 D X 2014-11-04 # 2015-04-30 6 60 1 6.6 6.6 F F 2014-11-06
各列の合計値が 50 を超えるカラムを抽出するには、
df.sum() # N1 21.0 # N2 210.0 # N3 21.0 # F1 23.1 # F2 23.1 # dtype: float64 indexer = df.sum() > 50 indexer # N1 False # N2 True # N3 False # F1 False # F2 False # dtype: bool df[indexer.index[indexer]] # N2 # 2014-11-30 10 # 2014-12-31 20 # 2015-01-31 30 # 2015-02-28 40 # 2015-03-31 50 # 2015-04-30 60
各行 の値に関数適用して選択したいときは apply
。apply
に渡す関数は 行 もしくは 列を Series
として受け取って処理できるものでないとダメ。apply
での関数の適用方向は axis
オプションで決める。
axis=0
: 各列への関数適用axis=1
: 各行への関数適用
"N1" カラムと "N2" カラムの積が 100 を超える行だけをフィルタする場合、
df.apply(lambda x: x['N1'] * x['N2'], axis=1) # 2014-11-30 10 # 2014-12-31 40 # 2015-01-31 90 # 2015-02-28 160 # 2015-03-31 250 # 2015-04-30 360 # Freq: M, dtype: int64 df.apply(lambda x: x['N1'] * x['N2'], axis=1) > 100 # 2014-11-30 False # 2014-12-31 False # 2015-01-31 False # 2015-02-28 True # 2015-03-31 True # 2015-04-30 True # Freq: M, dtype: bool df[df.apply(lambda x: x['N1'] * x['N2'], axis=1) > 100] # N1 N2 N3 F1 F2 S1 S2 D1 # 2015-02-28 4 40 3 4.4 4.4 D X 2014-11-04 # 2015-03-31 5 50 2 5.5 5.5 E E 2014-11-05 # 2015-04-30 6 60 1 6.6 6.6 F F 2014-11-06
補足 上ではあえて apply
を使ったが、各列同士は直接 要素の積をとれるため別に apply
が必須ではない。
df[df['N1'] * df['N2'] > 100] # N1 N2 N3 F1 F2 S1 S2 D1 # 2015-02-28 4 40 3 4.4 4.4 D X 2014-11-04 # 2015-03-31 5 50 2 5.5 5.5 E E 2014-11-05 # 2015-04-30 6 60 1 6.6 6.6 F F 2014-11-06
リストに含まれる値だけ選択したい
index
の例と同じ。Series
も isin
メソッドを持っているので、"S1" カラムの値が "A" もしくは "D" の列を選択するときは、
df['S1'].isin(['A', 'D']) # 2014-11-30 True # 2014-12-31 False # 2015-01-31 False # 2015-02-28 True # 2015-03-31 False # 2015-04-30 False # Freq: M, Name: S1, dtype: bool df[df['S1'].isin(['A', 'D'])] # N1 N2 N3 F1 F2 S1 S2 D1 # 2014-11-30 1 10 6 1.1 1.1 A A 2014-11-01 # 2015-02-28 4 40 3 4.4 4.4 D X 2014-11-04
値をソートして選択したい
DataFrame.sort
。ソート順序の変更など、オプションの詳細はこちら。
"N2" カラムの値が大きいものを 上から順に 3行分 取得するには、ソートして 行番号でスライスすればよい。
df.sort('N2', ascending=False)[:3] # N1 N2 N3 F1 F2 S1 S2 D1 # 2015-04-30 6 60 1 6.6 6.6 F F 2014-11-06 # 2015-03-31 5 50 2 5.5 5.5 E E 2014-11-05 # 2015-02-28 4 40 3 4.4 4.4 D X 2014-11-04
特定の年, 月, etc... のデータだけ選択したい
日時型のカラムに対しては、dt
アクセサを利用して日時型の各プロパティにアクセスできる。使えるプロパティはこちら。
"D1" カラムの日付が 2日, 3日, 5日の行だけ取得したければ、dt
アクセサ + isin
で、
df['D1'] # 2014-11-30 2014-11-01 # 2014-12-31 2014-11-02 # 2015-01-31 2014-11-03 # 2015-02-28 2014-11-04 # 2015-03-31 2014-11-05 # 2015-04-30 2014-11-06 # Freq: M, Name: D1, dtype: datetime64[ns] df['D1'].dt.day # 2014-11-30 1 # 2014-12-31 2 # 2015-01-31 3 # 2015-02-28 4 # 2015-03-31 5 # 2015-04-30 6 # Freq: M, dtype: int64 df['D1'].dt.day.isin([2, 3, 5]) # 2014-11-30 False # 2014-12-31 True # 2015-01-31 True # 2015-02-28 False # 2015-03-31 True # 2015-04-30 False # Freq: M, dtype: bool df[df['D1'].dt.day.isin([2, 3, 5])] # N1 N2 N3 F1 F2 S1 S2 D1 # 2014-12-31 2 20 5 2.2 2.2 b X 2014-11-02 # 2015-01-31 3 30 4 3.3 3.3 C X 2014-11-03 # 2015-03-31 5 50 2 5.5 5.5 E E 2014-11-05
補足 dt
アクセサは集計でもかなり強力なのでオススメ。
Python pandas アクセサ / Grouperで少し高度なグルーピング/集計 - StatsFragments
Python 組み込みの文字列関数を使ってデータ選択したい
object
型のカラムに対しては、str
アクセサを利用して、Python 組み込みの 文字列関数を実行した結果を Series
として取得できる。使えるメソッドはこちら。したがって、文字列関数を実行するだけならわざわざ apply
を使わなくても済む。
str.lower
を使って値が小文字の列を選択してみる。
# "S1" カラムの値を小文字化 df['S1'].str.lower() # 2014-11-30 a # 2014-12-31 b # 2015-01-31 c # 2015-02-28 d # 2015-03-31 e # 2015-04-30 f # Freq: M, Name: S1, dtype: object df['S1'].str.lower() == df['S1'] # 2014-11-30 False # 2014-12-31 True # 2015-01-31 False # 2015-02-28 False # 2015-03-31 False # 2015-04-30 False # Freq: M, Name: S1, dtype: bool df[df['S1'].str.lower() == df['S1']] # N1 N2 N3 F1 F2 S1 S2 D1 # 2014-12-31 2 20 5 2.2 2.2 b X 2014-11-02
2015/05/06追記 v0.16以降で .str
から呼び出せる関数が追加されている。追加関数には str.islower
もあるため、上の例は以下のように書ける。
df[df['S1'].str.islower()] # N1 N2 N3 F1 F2 S1 S2 D1 # 2014-12-31 2 20 5 2.2 2.2 b X 2014-11-02
値が重複したデータを削除したい
DataFrame.drop_duplicates
。重複のうち、最初のひとつ もしくは 最後のひとつ どちらかを残す挙動は Index
のときと同様。
df_dup2 = pd.DataFrame({'N1': [1, 1, 3, 1], 'N2': [1, 1, 4, 4], 'S1': ['A', 'A', 'B', 'C']}) df_dup2 # N1 N2 S1 # 0 1 1 A # 1 1 1 A # 2 3 4 B # 3 1 4 C # 完全に重複している行を、最初の一つを残して削除 df_dup2.drop_duplicates() # N1 N2 S1 # 0 1 1 A # 2 3 4 B # 3 1 4 C # N1 カラムの値が重複している行を、最初の一つを残して削除 df_dup2.drop_duplicates(subset=['N1']) # N1 N2 S1 # 0 1 1 A # 2 3 4 B
重複している値 全てを削除したければ、Index
の場合と同じく groupby
して filter
。
df_dup2.groupby(["S1"]).filter(lambda x:x.shape[0] == 1) # N1 N2 S1 # 2 3 4 B # 3 1 4 C
欠測値 ( NaN
) のデータを削除したい
DataFrame.dropna
。チェックするデータの方向 (行方向 or 列方向), 条件などのオプションの詳細はこちら。
サンプルとして、いくつかの欠測値 ( NaN
) を含むデータを作成。
df_na = pd.DataFrame({'N1': [1, 2, np.nan, 4], 'N2': [6, 5, 4, np.nan], 'S1': [np.nan, 'B', 'C', 'D']}) df_na # N1 N2 S1 # 0 1 6 NaN # 1 2 5 B # 2 NaN 4 C # 3 4 NaN D # NaN がひとつでもある行を削除 df_na.dropna() # N1 N2 S1 # 1 2 5 B # N2 カラムに NaN がある行を削除 df_na.dropna(subset=['N2']) # N1 N2 S1 # 0 1 6 NaN # 1 2 5 B # 2 NaN 4 C # 0 or 1 行目に NaN がある列を削除 df_na.dropna(axis=1, subset=[0, 1]) # N1 N2 # 0 1 6 # 1 2 5 # 2 NaN 4 # 3 4 NaN
まとめ
pandas
で行 / 列からデータ選択する方法をざっとまとめた。少し複雑なデータ選択をするときにやるべき流れは、
- 対象となる 行 / 列のデータと選択条件を確認する
- API ガイドにやりたい処理に直接 対応するものがないか探す。
- 直接 対応するものがなければ、全体をいくつかの単純な処理に分解して、最終的に
__getitem__
やix
に渡せる形にできるか考える。 - 自作関数を使ったほうが早そうだな、、、というときには
Index.map
orDataFrame.apply
。
次こそ where
と query
。
11/18 追記:
Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理
- 作者: Wes McKinney,小林儀匡,鈴木宏尚,瀬戸山雅人,滝口開資,野上大介
- 出版社/メーカー: オライリージャパン
- 発売日: 2013/12/26
- メディア: 大型本
- この商品を含むブログ (9件) を見る
Python pandas データ選択処理をちょっと詳しく <前編>
概要
書いていて長くなったため、まず前編として pandas
で データを行 / 列から選択する方法を少し詳しく書く。特に、個人的にはけっこう重要だと思っている loc
と iloc
について 日本語で整理したものがなさそうなので。
サンプルデータの準備
import pandas as pd s = pd.Series([1, 2, 3], index = ['I1', 'I2', 'I3']) df = pd.DataFrame({'C1': [11, 21, 31], 'C2': [12, 22, 32], 'C3': [13, 23, 33]}, index = ['I1', 'I2', 'I3']) s # I1 1 # I2 2 # I3 3 # dtype: int64 df # C1 C2 C3 # I1 11 12 13 # I2 21 22 23 # I3 31 32 33
__getitem__
での選択
Series
, もしくは DataFrame
に対して、__getitem__
でアクセスした場合の挙動は以下のようになる。
Series
:index
からの選択DataFrame
:columns
からの選択 (例外あり、下表 * の箇所)
なんかあいまいな言い方なのは 下表のとおりいくつかの形式で index
, columns
を指定できるから。使える形式は Series
と DataFrame
で少し違うので注意。
Series |
DataFrame |
---|---|
index の名前 (もしくは名前のリスト) |
columns の名前 (もしくは名前のリスト) |
index の順序 (番号) (もしくは番号のリスト) |
columns の順序 (番号)のリスト (番号のみはダメ) ただし、slice を渡した場合は index からの選択 * |
index と同じ長さの bool のリスト |
index と同じ長さの bool のリスト * |
- | データと同じ次元の bool の DataFrame |
※ 上で "リスト" と書いている箇所は numpy.array
, pd.Series
, slice
なんかのリスト - like でもよい。
11/15追記: 元データと同じ次元の DataFrame
が渡せることの記載漏れ、またDataFrame
へ slice
を渡した場合の挙動が間違っていたので修正。
s[0] # 1 s['I1'] # 1 df['C1'] # I1 11 # I2 21 # I3 31 # Name: C1, dtype: int64 # 番号を数値として渡すとNG! df[1] # KeyError: 1 # 番号のリストならOK (columns からの選択) df[[1]] # C2 # I1 12 # I2 22 # I3 32 # 番号のスライスもOK (index からの選択) df[1:2] # C1 C2 C3 # I2 21 22 23 # NG! df['I1'] # KeyError: 'I1' s[[True, False, True]] # I1 1 # I3 3 # dtype: int64 df[[True, False, True]] # C1 C2 C3 # I1 11 12 13 # I3 31 32 33 # bool の DataFrame を作る df > 21 # C1 C2 C3 # I1 False False False # I2 False True True # I3 True True True # bool の DataFrame で選択 df[df > 21] # C1 C2 C3 # I1 NaN NaN NaN # I2 NaN 22 23 # I3 31 32 33
引数による 返り値の違い
引数を値だけ (ラベル, 数値)で渡すと次元が縮約され、Series
では単一の値, DataFrame
では Series
が返ってくる。元のデータと同じ型の返り値がほしい場合は 引数をリスト型にして渡せばいい。
# 返り値は 値 s['I1'] # 1 # 返り値は Series s[['I1']] # I1 1 # dtype: int64 # 返り値は Series df['C1'] # I1 11 # I2 21 # I3 31 # Name: C1, dtype: int64 # 返り値は DataFrame df[['C1']] # C1 # I1 11 # I2 21 # I3 31
index
, columns
を元にした選択 ( ix
, loc
, iloc
)
ix
プロパティを使うと DataFrame
で index
, columns
両方を指定してデータ選択を行うことができる ( Series
の挙動は __getitem__
と同じ)。
引数として使える形式は __getitem__
と同じだが、ix
では DataFrame
も以下すべての形式を使うことができる。
- 名前 (もしくは名前のリスト)
- 順序 (番号) (もしくは番号のリスト)
index
, もしくはcolumns
と同じ長さのbool
のリスト
ix
はメソッドではなくプロパティなので、呼び出しは以下のようになる。
Series.ix[?]
:?
にはindex
を特定できるものを指定DataFrame.ix[?, ?]
:?
にはそれぞれindex
,columns
の順に特定できるものを指定
# 名前による指定 s.ix['I2'] # 2 df.ix['I2', 'C2'] # 22 # 順序による指定 s.ix[1] # 2 df.ix[1, 1] # 22 # 名前のリストによる指定 s.ix[['I1', 'I3']] # I1 1 # I3 3 # dtype: int64 df.ix[['I1', 'I3'], ['C1', 'C3']] # C1 C3 # I1 11 13 # I3 31 33 # bool のリストによる指定 s.ix[[True, False, True]] # I1 1 # I3 3 # dtype: int64 df.ix[[True, False, True], [True, False, True]] # C1 C3 # I1 11 13 # I3 31 33 # 第一引数, 第二引数で別々の形式を使うこともできる df.ix[1:, "C1"] # I2 21 # I3 31 # Name: C1, dtype: int64
DataFrame.ix
の補足
DataFrameで第二引数を省略した場合は index への操作になる。
df.ix[1] # C1 21 # C2 22 # C3 23 # Name: I2, dtype: int64
DataFrame
で columns
に対して操作したい場合、以下のように第一引数を空にするとエラーになる ( R に慣れているとやりがち、、 )。第一引数には :
を渡す必要がある (もしくは ix
を使わず 直接 __getitem__
する )。
df.ix[, 'C3'] # SyntaxError: invalid syntax df.ix[:, 'C3'] I1 13 I2 23 I3 33 Name: C3, dtype: int64
引数による 返り値の違い
引数の型による返り値の違いは __getitem__
の動きと同じ。Series
については挙動もまったく同じなので、ここでは DataFrame
の場合だけ例示。
# 返り値は 値 df.ix[1, 1] # 22 # 返り値は Series df.ix[[1], 1] # I2 22 # Name: C2, dtype: int64 # 返り値は DataFrame df.ix[[1], [1]] # C2 # I2 22
ということで、だいたいの場合は ix
を使えばよしなにやってくれる。
とはいえ
ラベル名 もしくは 番号 どちらかだけを指定してデータ選択したい場合もある。例えば、
index
,columns
がint
型であるindex
,columns
に重複がある
df2 = pd.DataFrame({1: [11, 21, 31], 2: [12, 22, 32], 3: [13, 23, 33]}, index = [2, 2, 2]) df2 # 1 2 3 # 2 11 12 13 # 2 21 22 23 # 2 31 32 33
内部的には ix
は ラベルを優先して処理を行うため、上記のデータについては以下のような動作をする。
index
に2
を指定すると、3行目ではなくindex
のラベルが 2 の行を選択columns
に[1, 2]
を指定すると、2, 3列目ではなくcolumns
のラベルが 1, 2 の列を選択
df2.ix[2, [1, 2]] 1 2 2 11 12 2 21 22 2 31 32
つまり 上記のようなデータ、(特にデータによって index
, columns
の値が変わるとか、、) では ix
を使うと意図しない挙動をする可能性がある。明示的に index
, columns
を番号で指定したい!というときには iloc
を使う。
df2.iloc[2, [1, 2]] 2 32 3 33 Name: 2, dtype: int64 # 3列目は存在しないので NG! df2.iloc[2, 3] # IndexError: index out of bounds
同様に、明示的にラベルのみで選択したい場合は loc
。
df2.loc[2, [1, 2]] 1 2 2 11 12 2 21 22 2 31 32 # ラベルが 1 の index は存在しないので NG! df.loc[1, 2] # KeyError: 'the label [1] is not in the [index]'
ix
, iloc
, loc
については文法 / 挙動は基本的に一緒で、使える引数の形式のみが異なる。整理すると、
使える引数の形式 | ix |
iloc |
loc |
---|---|---|---|
名前 (もしくは名前のリスト) | ○ | - | ○ |
順序 (番号) (もしくは番号のリスト) | ○ | ○ | - |
index , もしくは columns と同じ長さの bool のリスト |
○ | ○ | ○ |
一般に、pandas
でスクリプトを書くような場合には index
, columns
をどちらの形式で指定して選択するか決まっているはず。そんなときは loc
, iloc
を使っておいたほうが安全だと思う。
プロパティによるアクセス
Series
の index
, DataFrame
の columns
はプロパティとしてもアクセスできる。が、オブジェクト自体のメソッド/プロパティと衝突した場合は メソッド/プロパティが優先されるので使わないほうがよい。例えば 上の ix
を列名に持つデータがあったとすると、
s.I1 # 1 df3 = pd.DataFrame({'C1': [11, 21, 31], 'C2': [12, 22, 32], 'ix': [13, 23, 33]}, index = ['I1', 'I2', 'I3']) df3 # C1 C2 ix # I1 11 12 13 # I2 21 22 23 # I3 31 32 33 df3.C1 # I1 11 # I2 21 # I3 31 # Name: C1, dtype: int64 # NG! df3.ix <pandas.core.indexing._IXIndexer at 0x10bb31890>
bool
のリストによってデータ選択ができるということは
pandas.Series
や numpy.array
への演算は原則 リスト内の各要素に対して適用され、結果は真偽値の numpy.array
(もしくは Series
) になる。また、真偽値の numpy.array
同士で論理演算することもできる。
df.columns == 'C3' # array([False, False, True], dtype=bool) df.columns.isin(['C1', 'C2']) # array([ True, True, False], dtype=bool) (df.columns == 'C3') | (df.columns == 'C2') # array([False, True, True], dtype=bool)
そのため、上記のような条件式をそのまま行列選択時の引数として使うことができる。
df.ix[df.index.isin(['I1', 'I2']), df.columns == 'C3'] # C3 # I1 13 # I2 23
上の例ではあまりありがたみはないと思うが、外部関数で bool
のリストを作ってしまえばどんなに複雑な行列選択でもできるのは便利。
まとめ
pandas
で行 / 列からデータ選択するとき、
- 手元で対話的にちょっと試す場合は
ix
が便利。 - ある程度の期間使うようなスクリプトを書く場合は 少し面倒でも
iloc
,loc
が安全。
後編はもう少し複雑なデータ選択( where
, query
) あたりを予定。
11/15追記: 取消線した内容とは違うが続きを書いた。
Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理
- 作者: Wes McKinney,小林儀匡,鈴木宏尚,瀬戸山雅人,滝口開資,野上大介
- 出版社/メーカー: オライリージャパン
- 発売日: 2013/12/26
- メディア: 大型本
- この商品を含むブログ (9件) を見る
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 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件) を見る