DataFrameの値の更新 - pandas

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。

SettingWithCopyWarning

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で指定

「インデックス」はDataFrameをprintした時に左にある行名(上の例だと100,200,300)、 「index」は、各行各列が先頭から何番目か(配列の添字と一緒、0,1,2)とする。

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倍くらい)
programming/python/packages/pandas/update_multi_column.txt · 最終更新: 2018/09/13 by ikatakos
CC Attribution 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0