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件) を見る