目次
DataFrameの値の更新 - pandas
pandasは、DataFrameの値の取得方法がいろいろあるため、値の代入更新もつい「この書き方でいいんだっけ」と混乱する。
基本的には(一般的な代入と同じく)左辺で更新するデータ範囲を、右辺で値を指定するのだが、左辺のデータ範囲の指定方法が様々あるのに加え、右辺での値の指定にも複数方法がある。
df.loc[df['col1']==3, ['col2', 'col3']] = df['col4'] col1 が 3 である行の col2,col3 列を、ともに同行の col4 の値にする
大別すると以下の感じ。 左辺のアクセス関数に例えば配列を渡しても、関数の種類や配列の中身によって、名前か、添字か、どのように解釈されるか異なってくるのがややこしさの元となる。
- 左辺のアクセス関数
.__getitem__
(df[xxx]
のような角括弧の記法).loc
.iloc
- 左辺のアクセス関数の引数に指定できる値
- 単一の名前、単一の添字
- 名前の配列、添字の配列
- 名前のslice、添字のslice
- 列数と同じ長さのbool配列、行数と同じ長さのbool配列
- DataFrame
- Series
- 右辺の指定方法
- 単一の値
- 左辺と同じサイズの配列
※ここで、「名前」とは行や列に付けられた名称を差し、「添字」とは0から始まる連番(通常の配列の要素取得に使うもの)を指すものとする。
※配列は numpy.ndarray でもよい。
範囲の指定方法
データ範囲の指定方法は、以下のサイトがまとまっている。
loc, ilocは法則性がありわかりやすい。原則 loc[行, 列]
の順で指定する。(bool配列以外は)locが名前、ilocが添字として解釈される。
意味を明確にコーディングしたい場合はこれらを使うのが良い。
対してgetitemは、なんとなくよく使う方で解釈されるため、便利な反面、行なのか列なのか時と場合で変わり、やや紛らわしい。
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
1番目の書き方、df['col1']
は、df中の1カラムのSeriesを返す。dfはこのSeriesを参照として持っているので、このSeriesに対する更新はdfにも反映される。Seriesに対して“df['col1'] == 2
“に合致する行を抽出し、結果に直接代入しているので、OK。
2番目の書き方、df[df['col1'] == 2]
は、まず”df['col1'] == 2
“に合致する行を抽出している。この時点で元のdfとは別物となっていて、その後['col1']
で取り出されるSeriesも、元のSeriesとは別物となる。それに対して更新しても、反映されない。
3番目も同様。df['col1'][df['col1'] > 1]
までで別物となり、さらなる抽出結果に代入しても反映されない。
4番目のdf.loc[]
は行列の条件を一度に指定して抽出する方法で、これは1回の抽出結果に直接代入しているので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内の要素を抽出する方法は、loc, iloc がある。 他にもatとかixとかあるが、柔軟に解釈してくれるおかげでさらに紛れが発生しやすいので、基本はこの2つだけ覚えておけばよい。
列は名前、行は添字で指定
locは「行も列も名前で指定する」、ilocは「行も列も添字で指定する」のどちらかしか無く、「列は名前で指定したいが、行は添字で指定したい」時に困る。
劇的な解決とはいかないが、以下の解決策がある。
- あらかじめ目的行の名前を調べてからlocを使う
- あらかじめ目的列の添字を調べてからilocを使う
# 名前で['col2', 'col3'] 列、添字で[1, 2]行目のデータを更新したい # 【あらかじめ目的行の名前を調べてからlocを使う】 df.index # df.indexで、行の名前の配列を取得 # => [100 200 300] df.index[[1, 2]] # df.indexへの添字アクセスにより2,3行目の名前を得る # => [200 300] df.loc[df.index[[1, 2]], ['col2', 'col3']] = 30 # これをlocに用いることで、目的が達成される df # => # col1 col2 col3 # 100 1 2 3 # 200 4 30 30 # 300 7 30 30 # 【あらかじめ目的列のindexを調べてからilocを使う】 df.columns # => ['col1', 'col2', 'col3'] # df.columnsで、列の名前の配列を取得 col2_idx = df.columns.get_loc('col2') # 'col2', 'col3'のindexを得る col3_idx = df.columns.get_loc('col3') df.iloc[[1, 2], [col2_idx, col3_idx]] = 40 # これをilocに用いることで、目的が達成される 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)
巨大なDFに対する更新はなるべく一度に
数百~数千万行のDataFrameに対して更新をかけるのは、かなりコストの重い操作となる。たとえ更新する範囲が一部であっても、その場所の特定に時間がかかる。
もし、groupby() などの分割結果毎に何らかの集計処理し、元のDFに結果を反映させたい場合でも、毎回、元のDFを更新していては相当時間がかかる。
(そもそもgroupbyごとにforループ回すのも速度的によろしいことではないが。。。関数的に書けない処理が必要になることもあるので仕方ない)
そんな時、即時更新はせずリストに溜めて、更新は最後に(メモリが厳しいならある程度溜まった後に)行えば、高速化に繋がる。
下記は一例だが、もっと速い方法もあるかも知れない。(例なので処理内容には特に意味は無い)
# 'AAA' ごとに 'BBB' カラムを 'CCC' の2倍の値で更新 for i, grouped_df in df.groupby('AAA'): # なんか処理する # 毎回、元のDFを更新する → 遅い df.loc[grouped_df.index, 'BBB'] = grouped_df['CCC'] * 2
# 'AAA' ごとに 'BBB' カラムを 'CCC' の2倍の値で更新 buf = [] for i, grouped_df in df.groupby('AAA'): # なんか処理する # とりあえずバッファに溜める buf.append(grouped_df['CCC'] * 2) # 最後に更新する update_sr = pd.concat(buf) # DataFrameを全部つなげて update_sr.sort_index(inplace=True) # indexは整列されてた方が速い df.loc[update_sr.index, 'BBB'] = update_sr
# 'AAA' ごとに 'BBB1' と 'BBB2' を、何らかの値で更新 update_data = [] update_indices = [] for i, grouped_df in df.groupby('AAA'): # なんか処理する # 更新用numpy配列を作る n = len(grouped_df) si = grouped_df.index[0] ti = grouped_df.index[-1] update_table = np.zeros((n, 2)) update_table[:, 0] = 長さnのBBB1を更新したい値 update_table[:, 1] = 長さnのBBB2を更新したい値 # 蓄積する update_data.append(update_table) update_indices.extend(range(si, ti + 1)) # 最後に更新する update_table = np.concatenate(update_data, axis=0) # 全部縦に繋げる df.loc[update_indices, ['BBB1', 'BBB2']] = update_table