pandasは、DataFrameの値の取得方法がいろいろあるため、値の代入更新もつい「この書き方でいいんだっけ」と混乱する。
基本的には(一般的な代入と同じく)左辺で更新するデータ範囲を、右辺で値を指定するのだが、左辺のデータ範囲の指定方法が様々あるのに加え、右辺での値の指定にも複数方法がある。
df.loc[df['col1']==3, ['col2', 'col3']] = df['col4'] col1 が 3 である行の col2,col3 列を、ともに同行の col4 の値にする
大別すると以下の感じ。 左辺のアクセス関数に例えば配列を渡しても、関数の種類や配列の中身によって、名前か、添字か、どのように解釈されるか異なってくるのがややこしさの元となる。
.__getitem__
(df[xxx]
のような角括弧の記法).loc
.iloc
※ここで、「名前」とは行や列に付けられた名称を差し、「添字」とは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を含まない |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
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 |
pandasの値の更新は、'copy'と'view'の概念を知らないと、更新したはずなのにできていないなんて現象に悩まされる。これはNumPyに由来する概念で、
特に、DataFrameの一部のみを更新する際に混乱しやすい。
どのような処理がviewを返し、copyを返すのか、というのは複雑だが、なんとなくふわっと解釈するなら、更新は「唯一の抽出結果に直接代入しないといけない」。
1 2 3 4 5 6 7 8 9 10 11 |
# 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発で指定したオブジェクトに対する代入は、きちんと反映される。
1 2 3 4 5 6 7 8 9 10 11 12 |
# 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は「行も列も添字で指定する」のどちらかしか無く、「列は名前で指定したいが、行は添字で指定したい」時に困る。
劇的な解決とはいかないが、以下の解決策がある。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# 名前で['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に更新先のDataFrameを代入する。Indexの一致した行が置換される。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
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 |
(例) 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
df.apply(function, axis=1, reduce=False)
axis=1
を指定することで各rowに対する処理になる(指定しないと各column)reduce=False
を指定することで、結果がDataFrameで返る
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
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 ) |
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
1 2 3 4 5 6 7 8 |
# 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になる。
DataFrameやSeriesには、replace, fillna, interpolate など値を埋めたり変換したりする便利な関数が備えられている。
これらは基本的には新しいDataFrameやSeriesを返すが、引数にinplace=Trueを与えると、直接変更するようになる。
だが、locで抽出したものには、たとえ唯一の抽出結果であっても、inplace=Trueは効かない。
1 2 3 4 5 6 7 8 9 10 11 |
# × 更新されない 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 ) |
数百~数千万行のDataFrameに対して更新をかけるのは、かなりコストの重い操作となる。たとえ更新する範囲が一部であっても、その場所の特定に時間がかかる。
もし、groupby() などの分割結果毎に何らかの集計処理し、元のDFに結果を反映させたい場合でも、毎回、元のDFを更新していては相当時間がかかる。
(そもそもgroupbyごとにforループ回すのも速度的によろしいことではないが。。。関数的に書けない処理が必要になることもあるので仕方ない)
そんな時、即時更新はせずリストに溜めて、更新は最後に(メモリが厳しいならある程度溜まった後に)行えば、高速化に繋がる。
下記は一例だが、もっと速い方法もあるかも知れない。(例なので処理内容には特に意味は無い)
1 2 3 4 5 |
# 'AAA' ごとに 'BBB' カラムを 'CCC' の2倍の値で更新 for i, grouped_df in df.groupby( 'AAA' ): # なんか処理する # 毎回、元のDFを更新する → 遅い df.loc[grouped_df.index, 'BBB' ] = grouped_df[ 'CCC' ] * 2 |
1 2 3 4 5 6 7 8 9 10 11 |
# '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 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
# '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 |