StatsFragments

Python, R, Rust, 統計, 機械学習とか

Python pandas データ選択処理をちょっと詳しく <中編>

こちらの続き。

上の記事では bool でのデータ選択について 最後にしれっと書いて終わらせたのだが、一番よく使うところなので中編として補足。

まず __getitem__ix の記法では、次のような指定によって 行 / 列を選択することができた。

  • index, columns のラベルを直接指定しての選択
  • index, columns の番号(順序)を指定しての選択
  • index, columns に対応する bool のリストを指定しての選択

ここでは上記の選択方法をベースとして、ユースケースごとに IndexSeries のプロパティ / メソッドを使ってできるだけシンプルにデータ選択を行う方法をまとめる。

補足 一部の内容はこちらの記事ともかぶる。下の記事のほうが簡単な内容なので、必要な方はまずこちらを参照。

簡単なデータ操作を 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 型のクラスが使われている ( DatetimeIndexIndex のサブクラス)。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.ordercolumns をアルファベット順に並べ替えて、前から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

各行 の値に関数適用して選択したいときは applyapply に渡す関数は 行 もしくは 列を 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 の例と同じ。Seriesisin メソッドを持っているので、"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 or DataFrame.apply

次こそ wherequery

11/18 追記:

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理