【Python】pandasの連鎖代入をlocで書き直す

PythonのTopに戻る

条件に合う行を更新するなら、df.loc[条件, “列名”] = 値という一回の代入にまとめる。df[“列名”][条件] = 値のような連鎖代入は、Copy-on-Writeでは元の表を更新しない。ここではpandas 2.2.3でCopy-on-Writeを明示的に有効にし、その失敗と修正を確かめる。

最小例で確かめる

以下は説明用に作った小さなデータである。実測データや実行速度の測定結果ではない。コード全体をexample.pyとして保存すれば、入力ファイルを別途用意せずに実行できる。assertは、この例で成り立つべき形や値を確認するために入れてある。

import warnings
import pandas as pd
with pd.option_context("mode.copy_on_write", True):
    df = pd.DataFrame({"value": [1., -2., 3.], "flag": [False, True, False]})
    original = df.copy()
    with warnings.catch_warnings(record=True) as caught:
        warnings.simplefilter("always")
        df["value"][df["flag"]] = 0.  # 意図的な連鎖代入
    print("after chained assignment:", df["value"].tolist())
    found = any(issubclass(w.category, pd.errors.ChainedAssignmentError)
                for w in caught)
    print("chained-assignment warning:", found)
    pd.testing.assert_frame_equal(df, original)
    subset = df[["value"]]
    subset.loc[0, "value"] = 99.
    assert df.loc[0, "value"] == 1.
    df.loc[df["flag"], "value"] = 0.
    print("after loc:", df["value"].tolist())
    print("separate subset:", subset["value"].tolist())
    assert found
    assert df["value"].tolist() == [1., 0., 3.]
    assert subset["value"].tolist() == [99., -2., 3.]

実行結果

after chained assignment: [1.0, -2.0, 3.0]
chained-assignment warning: True
after loc: [1.0, 0.0, 3.0]
separate subset: [99.0, -2.0, 3.0]

どのオブジェクトを更新するか明示する

df[“value”][df[“flag”]]という書き方は、まず列を取り出し、その取り出したオブジェクトへ条件を使って代入する。Copy-on-Writeでは、この操作から元のdfも同時に更新することはできない。例では警告を捕捉し、元の表が変化しないことをassertで確認している。

df.loc[df[“flag”], “value”]なら、変更先のDataFrame、行条件、列名を一つの代入で指定できる。負の値を0へ置き換えるなど、条件付きの修正に使いやすい形である。ただし、この例で修正対象をflagで指定しているのは説明用の規則であり、実際に負の測定値を0へ変えてよいかはデータの意味から決める。

実行版とpandas 3系を区別する

公式ガイドではpandas 3.0からCopy-on-Writeが標準かつ唯一のモードになっている。この記事の実行確認はpandas 2.2.3であり、3系で実行した結果ではない。2.2.3でも有効化できるため、option_contextで例の範囲だけTrueにし、更新の振る舞いを確かめた。

2系で既定設定のまま動いていたコードが、3系への移行時に同じように更新されるとは限らない。警告の文言だけを手掛かりにするより、更新後に変わるべきセルと変わらないセルを検査する方が確実である。この例ではChainedAssignmentErrorという名前の警告カテゴリを確認しており、通常の例外送出と混同しないようにしている。

取り出した表の変更は別の変更になる

subset = df[[“value”]]で取り出した表をlocで書き換えても、Copy-on-Writeでは元のdfは変わらない。これはsubset自身に対する正しい代入である。元表を更新したいなら元表のlocへ代入し、独立した派生結果を作りたいならsubsetをその結果として扱えばよい。

「viewかcopyかを当てて、変更が原本へ戻ることを期待する」という設計を避け、変更対象をコードで直接示すことが重要になる。内部で実際にいつメモリをコピーするかと、ユーザーから見た更新契約は分けて考える。単純な別名alias = dfは同じPythonオブジェクトなので、別の表を作った場合とは異なる。

よくある書き換えの注意点

df[“value”].fillna(…, inplace=True)のように、取り出した列へinplace操作をして元表の変更を期待するコードも見直したい。元の列へ結果を代入し直すか、DataFrameを対象に対応する操作を行う。警告を消すためだけに設定を変更するのではなく、どの表を更新する意図なのかに沿って修正する。

右辺がSeriesの場合にはloc代入でもラベル整列が関係する。連鎖代入を直せば行と値の対応も自動で正しくなる、とまでは言えない。条件マスクのindex、右辺のindex、列のdtype、欠測の扱いを確認する。特に型の異なる値へ置き換える操作では、結果のdtypeまで検査する。

テストには対象行の値だけでなく、非対象行と原本の保存も含めよう。掲載例では連鎖代入が元表を変更しないこと、別のsubsetだけが変わること、locが指定セルを変更することを分けて検査した。移行前後の更新契約を、小さい表で説明できる形にしておくと不具合を追いやすい。

動作確認環境と参考資料

Linux・CPython 3.12.14、NumPy 2.3.5、pandas 2.2.3、SciPy 1.17.0、Matplotlib 3.10.8の環境で掲載コードを実行した。使用するライブラリはコード冒頭のimportを参照してほしい。公式資料の最新版と、この実行確認版は区別している。数値の末尾や表の表示幅は環境によって変わることがある。

関連するTips

PythonのTopに戻る