目次
文書の過去の版を表示しています。
DataFrameの値の更新 - pandas
pandasは、DataFrameへのアクセス方法がいろいろあり、お世辞にも統一的とは言えないので、よく「この書き方でいいんだっけ」と混乱する。
基本は、以下のサイトがまとまっている。
大別すると以下の感じ。 左辺に例えば配列を渡しても、アクセス方法や配列の中身によって、名前か、添字か、boolか、どのように解釈されるか異なってくるのがややこしさの元となる。
- 左辺のアクセス方法
.__getitem__
(df[xxx]
のような角括弧の記法).loc
.iloc
- 左辺のアクセスに指定できる値
- 単一の名前
- 単一の添字
- 名前の配列
- 添字の配列
- 名前のslice
- 添字のslice
- 列数と同じ長さのbool配列
- 行数と同じ長さのbool配列
- DataFrame
- 右辺の指定方法
- 単一の値
- 左辺と同じサイズの配列
ここで、「名前」とは行や列に付けられた名称を差し、「添字」とは0から始まる連番(通常の配列の要素取得に使うもの)を指すものとする。
print(df)
した時は、名前は表示されるが、添字は表示されない。
また、添字は負の値で末尾から数えられるなど、通常の配列の添字と同じようなものである。
0 1 2 ←列の添字 col1 col2 col3 ←列の名前 0 2019/01/01 1 2019/01/08 2 2019/01/15 ↑ ↑ 行の添字 行の名前
配列は numpy.ndarray や pandas.Series などでもよい。
データの扱われ方
getitem | loc | iloc | ||
---|---|---|---|---|
単独の値 [●●] または カンマ区切りで1番目の値 [●●, ○○] | 単一の値 | ①列の名前として解釈 列をSeriesとして取得 | ②行の名前として解釈 1行のみのDataFrameとして取得 | ③行の添字として解釈 1行のみのDataFrameとして取得 |
bool配列 | ④Trueの行のみのDataFrameとして取得 * dfの行数と同じ長さでないとエラー |
|||
その他配列 | ⑤列の名前として解釈 DataFrameとして取得 * 列名が1つでも存在しないとエラー | ⑥行の名前として解釈 DataFrameとして取得 * 行名が1つでも存在しないとエラー | ⑦行の添字として解釈 DataFrameとして取得 * 1つでも範囲外だとエラー |
|
slice[s:t] | ⑧行の名前として解釈 DataFrameとして取得 * s,tがともに列名に存在しないとエラー * tを含む | ⑨行の添字として解釈 DataFrameとして取得 * tを含まない |
||
DataFrame | ⑩下例参照、DataFrameとして取得 * 全要素がboolでないとエラー | * エラー | ||
カンマ区切りで2番目の値 [○○, ●●] | 単一の値 | ①「(1番目の値, 2番目の値)というtuple」という名前の列として解釈 * ともにHashableな値で無いとエラー | ⑪列の名前として解釈 Seriesまたは値として取得 | ⑫列の添字として解釈 Seriesまたは値として取得 |
bool配列 | * エラー | ⑬Trueの列のみのDataFrameとして取得 * dfの列と同じ長さでないとエラー |
||
その他配列 | ⑭列の名前として解釈 DataFrameとして取得 | ⑮列の添字として解釈 DataFrameとして取得 |
||
slice[s:t] | ⑯列の名前として解釈 * tを含む | ⑰列の添字として解釈 * tを含まない |
df # col1 col2 col3 # 10 1 2 3 # 20 4 5 6 # 30 7 8 9 df['col1'] # 1 # col1 # 10 1 # 20 4 # 30 7 df.loc[20] # 2 df.iloc[1] # 3 # col1 col2 col3 # 20 4 5 6 df[[True, False, True]] # 4 df.loc[[True, False, True]] df.iloc[[True, False, True]] # col1 col2 col3 # 10 1 2 3 # 30 7 8 9 df[['col1', 'col3']] # 5 # col1 col3 # 10 1 3 # 20 4 6 # 30 7 9 df.loc[[10, 30]] # 6 df.iloc[[0, 2]] # 7 # col1 col2 col3 # 10 1 2 3 # 30 7 8 9 df[10:30] # 8 df.loc[10:30] # col1 col2 col3 # 10 1 2 3 # 20 4 5 6 # 30 7 8 9 df.iloc[1:2] # 9 # col1 col2 col3 # 20 4 5 6 # 10 idf = pd.DataFrame([[True, False, True], [False, True, True]], index=[10, 20], columns=['col1', 'col2', 'dummy']) # col1 col2 dummy # 10 True False True # 20 False True True df[idf] # col1 col2 col3 # 10 1 NaN NaN # 20 NaN 5 NaN # 30 NaN NaN NaN df.loc[10, 'col1'] # 11 df.iloc[0, 0] # 12 # 1 df.loc[[10, 30], 'col1'] # 11 df.iloc[[0, 2], 0] # 12 # col1 # 10 1 # 30 7
SettingWithCopyWarning
pandasの値の更新は、'copy'と'view'の概念を知らないと、更新したはずなのにできていないなんて現象に悩まされる。これはNumPyに由来する概念で、
- viewは、元のDataFrameの参照を示し、viewに対する更新は元に反映される
- copyは、元のDataFrameとは別物として存在し、copyに対する更新は反映されない(操作によってはSettingWithCopyWarningが出る)
特に、DataFrameの一部のみを更新する際に混乱しやすい。
どのような処理がviewを返し、copyを返すのか、というのは複雑だが、なんとなくふわっと解釈するなら、更新は「唯一の抽出結果に直接代入しないといけない」。
# col1 col2 # 0 1 3 # 1 2 5 df.col1[df.col1 == 2] = 100 # OK df[df.col1 == 2].col1 = 100 # SettingWithCopyWarning df.col1[df.col1 > 1][df.col1 < 3] = 100 # NG(特に警告も出ない) df.loc[df.col1 == 2, 'col1'] = 100 # OK
df.col1
は、df中の1カラムのSeriesを返す。dfはこのSeriesを参照として持っているので、このSeriesに対する更新はdfにも反映される。Seriesに対して“df.col1 == 2
“に合致する行を抽出し、結果に直接代入しているので、OK。
df[df.col1 == 2]
は、まず”df.col1 == 2
“に合致する行を抽出している。この時点で元のdfとは別物となっていて、.col1
で取り出されるSeriesも、元のSeriesとは別物となる。それに対して更新しても、反映されない。
3番目も同様。df.col1[df.col1 > 1]
までで別物となり、さらなる抽出結果に代入しても反映されない。
df.loc[]
は行列の条件を一度に指定して抽出する方法で、抽出した物に直接代入しているのでOK。
pandasでは df[df.col1 == 2].col1 = 100
のように、2回以上に分けて抽出した結果に何かを代入しても、元のdf
の値は置き換わらない。
こういうコードを書くと警告が出る。
SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy self._setitem_with_indexer(indexer, value)
このことについては、詳しい海外記事と、それを日本語訳してくれている方がいるので、それを読むとよい。
上記を読めば後段の説明は不要なのだが、まぁ、せっかくだし残しておこう。
回避方法
最初から行も列も1発で指定したオブジェクトに対する代入は、きちんと反映される。
# col1 col2 col3 # 100 1 2 3 # 200 4 5 6 # 300 7 8 9 df.loc[df['col1'] > 5, ['col2', 'col3']] = 20 print(df) # => # col1 col2 col3 # 100 1 2 3 # 200 4 5 6 # 300 7 20 20
行と列を一度に指定してDataFrame内の要素を抽出する方法は、at, iat, loc, iloc がある。
列はカラム名、行はindexで指定
locは「行も列も名称,インデックスで指定する」、ilocは「行も列もindexで指定する」のどちらかしか無く、「列はカラム名で指定したいが、行はindexで指定したい」時に困る。
劇的な解決とはいかないが、「あらかじめ目的行のインデックスを調べてからlocを使う」または「あらかじめ目的列のindexを調べてからilocを使う」ことで回避できる。
# あらかじめ目的行のインデックスを調べてからlocを使う rows = df.index[[1, 2]] # 2,3行目のインデックスを得る df.loc[rows, ['col2', 'col3']] = 30 # locでインデックス指定 print(df) # => # col1 col2 col3 # 100 1 2 3 # 200 4 30 30 # 300 7 30 30 # あらかじめ目的列のindexを調べてからilocを使う col2_idx = df.columns.get_loc('col2') # 'col2'のindexを得る col3_idx = df.columns.get_loc('col3') df.iloc[[1, 2], [col2_idx, col3_idx]] = 40 print(df) # => # col1 col2 col3 # 100 1 2 3 # 200 4 40 40 # 300 7 40 40
DataFrameの複数columnを更新
更新元のDataFrameに更新先のDataFrameを代入する。Indexの一致した行が置換される。
import pandas as pd # 更新元 df = pd.DataFrame([[1, 2, 3], [4, 5, 6]], columns=['col1', 'col2', 'col3']) print(df) # => # col1 col2 col3 # 0 1 2 3 # 1 4 5 6 # 更新先 upd_df = pd.DataFrame([[7, 8], [9, 10]]) print(upd_df) # => # 0 1 # 0 7 8 # 1 9 10 # 代入 df[['col2', 'col3']] = upd_df print(df) # => # col1 col2 col3 # 0 1 7 8 # 1 4 9 10
関数を適用して更新
- やりたいこと
- 1レコード(row)を引数にとって、複数の値(val1, val2)をタプルで返すfunction(row)がある
- 各レコードにつき、row[1],row[2] を val1,val2で更新したい
(例) id col1 col2 -- col1, col2を --> id col1 col2 0 0 10 10 2値の和と差で 0 0 20 0 1 1 20 15 -- 更新 --> 1 1 35 5
- 各rowに対する関数の適用は、
df.apply(function, axis=1, reduce=False)
axis=1
を指定することで各rowに対する処理になる(指定しないと各column)reduce=False
を指定することで、結果がDataFrameで返る- 指定しないとタプルのSeriesで返るため、そのまま元のDataFrameに代入できない
import pandas as pd # rowを受け取り、col1 と col2 の和と差をタプルで返すサンプル関数 apply_func = lambda row: (row['col1'] + row['col2'], row['col1'] - row['col2']) # サンプルDataFrame df = pd.DataFrame([[1, 20, 10], [2, 10, 9], [3, 11111, 9999]]) df.columns = ['id', 'col1', 'col2'] print(df) # => # id col1 col2 # 0 1 20 10 # 1 2 10 9 # 2 3 11111 9999 # DataFrameの各rowにapply_func関数を適用 trans = df.apply(apply_func, axis=1, reduce=False) print(type(trans), trans.shape) print(trans) # => # <class 'pandas.core.frame.DataFrame'> (3, 2) # 0 1 # 0 30 10 # 1 20 1 # 2 21110 1112 # 元のDataFrameに代入 df[['col1', 'col2']] = trans print(df) # => # id col1 col2 # 0 1 30 10 # 1 2 20 1 # 2 3 21110 1112 # 1行にまとめると、 df[['col1', 'col2']] = df.apply(apply_func, axis=1, reduce=False)
- 蛇足:タプルのSeriesをDataFrameに変換する方法
- pd.DataFrame(series.tolist())
- 「tupleのlist」にすればpd.DataFrame()でDataFrameを生成できる
- series.apply(pd.Series)
- tupleの各要素に対しpd.Seriesを適用する方法もあるが、上と比べてかなり遅い(50倍くらい)
IDごとにはじめて○○する時の値で更新
a b id 1 10 6 1 2 6 -3 1 3 -3 12 1 # id:1 ではじめてbが10以上になる 4 4 23 2 # id:2 ではじめてbが10以上になる 5 12 11 2 6 3 -5 2 ↓ a b id c 1 10 6 1 -3 # 各idではじめてbが10以上になったレコードの 2 6 -3 1 -3 # aの値を持つ、カラムcを追加する 3 -3 12 1 -3 4 4 23 2 4 5 12 11 2 4 6 3 -5 2 4
# bが10以上のレコードのみを抽出後、groupby()して、各id最初のレコードを得る fdf = df[df['b'] >= 10].groupby('id').first() # カラム名リネーム fdf.rename(columns={'a': 'c'}, inplace=True) # idをキーにマージする df = pd.merge(df, fdf['c'], how='left', left_on='id', right_index=True)
但し、あるidに条件を満たすレコードが1つも無かった場合、そのidのカラムcはnanになる。
inplaceは効かない
DataFrameやSeriesには、replace, fillna, interpolate など値を埋めたり変換したりする便利な関数が備えられている。
これらは基本的には新しいDataFrameやSeriesを返すが、引数にinplace=Trueを与えると、直接変更するようになる。
だが、locで抽出したものには、たとえ唯一の抽出結果であっても、inplace=Trueは効かない。
# × 更新されない df.loc[[1,2,3], 'b'].fillna(0, inplace=True) # ○ 更新したければ、inplaceを使わず代入を用いる df.loc[[1,2,3], 'b'] = df.loc[[1,2,3], 'b'].fillna(0) # ○ 単独列を丸ごと取り出したものは効く df['a'].fillna(0, inplace=True) # × 複数列を取り出したものは効かない df[['a', 'b']].fillna(0, inplace=True)