pandasの更新が反映されないときは、inplaceを追加する前に「何を更新しているか」を確認します。 Copy-on-Writeが有効な環境では、取り出した列を変更しても親のDataFrameは更新されません。列へ結果を代入するか、親のDataFrameに対して更新する形へ書き直します。
対象は、CSVやExcelの前処理でfillnaを使い、実行したのに欠損が残る、代入した変数がNoneになる、抽出した表だけ変わる、といった状態で困っている人です。ここでは六件の架空受付を使い、書き方の違いを小さい表で照合します。実在の受講体験や業務実績ではありません。
公式資料の確認日:2026年10月30日。pandas移行ガイドのCopy-on-Write、DataFrame.fillna、3.1.0の変更点を参照しています。掲載コードはPython 3.12.14で、pandas 2.2.3のCoW有効時と、現行の3.1.0の両方で確認しています。戻り値の違いも以下で区別します。
inplaceで変わる対象を、六行の表で切り分ける
最初に版と、変更したい表の名前を確認する
同じ一行のコードでも、古い説明の前提と利用中のpandasの動作が違うことがあります。現行の移行ガイドでは、pandas 3.0以降はCopy-on-Writeが唯一の動作方式と案内されています。旧記事の「列を取り出してinplace=Trueにすれば元にも反映される」という説明を、そのまま現在の環境へ当てはめないようにします。
ただし、原因をすべて版の変更に決めつけるのも早計です。別の変数を表示していた、補完する値が欠損ではなく空文字だった、セルを実行する順序が違った、といった可能性もあります。まず版、対象の変数、対象列、実行直前の値を一緒に記録すると、調べる範囲を狭められます。
次の教材ではrawを原本として扱います。idは受付の識別子、deptは部署、amountは記録された金額です。R2、R4、R5は金額不明であり、ゼロ円と判明したわけではありません。コードの挙動を見るための補完と、業務上その補完を採用してよいかという判断は分けてください。
import pandas as pd
import warnings
major_version = int(pd.__version__.split(".")[0])
if major_version < 3:
pd.options.mode.copy_on_write = True # 2系の検証環境用
raw = pd.DataFrame({
"id": [f"R{i}" for i in range(1, 7)],
"dept": ["A", "A", "B", "B", "C", "C"],
"amount": [100, None, 0, None, None, 300],
})
assert raw["id"].is_unique
assert raw["amount"].isna().sum() == 3
assert raw["amount"].sum() == 400
print(pd.__version__)
print(raw)
この設定行は、手元の2系環境でCoWの更新対象を確かめるためのものです。3系で旧動作へ戻す手順として紹介しているわけではありません。古いコードを動かす目的でも、警告を消す設定から始めず、どのオブジェクトを更新する処理なのかを先に読み直します。
列を取り出した後のinplaceは、親の表への指示と違う
df["amount"]は列を表すSeriesを取り出します。その後ろにfillnaをつなげた場合、メソッドを呼び出している相手はそのSeriesです。「inplaceは元に反映する」という短い説明だけでは、その元が親のDataFrameなのか、取り出したSeriesなのかが曖昧になります。
以下は、失敗の形を小さい表で確認するコードです。CoWを有効にした実行環境では、badの欠損三件が残ります。警告のクラス名も記録し、警告が出たことと、データがどうなったかを別々に確かめます。警告名にErrorが含まれていても、ここではwarningsで取得する警告として扱っています。
bad = raw.copy()
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
bad["amount"].fillna(0, inplace=True)
assert bad.equals(raw)
assert bad["amount"].isna().sum() == 3
print([type(w.message).__name__ for w in caught])
画面に赤い文字が出なかった場合も、元の表が変わったとは限りません。実行環境の警告設定や表示方法によって見え方が違うためです。この例ではbad.equals(raw)が真であることから、親の表に補完が反映されていないと確認します。警告の有無だけを成功条件にしません。
また、取り出した列を一度変数に入れてから更新する場合も、親へ戻したいなら代入先を明示します。Seriesの値が変わっていても、rawの値まで変わったと推測しないでください。「列を取り出す」「処理する」「親の列に代入する」の三段階で考えると、更新の経路が見えます。
戻り値を受け取る書き方と、親を更新する書き方を比べる
列全体を補完する場合は、結果をその列へ代入する形にできます。右辺は補完後のSeries、左辺は更新先のDataFrameの列です。この書き方なら、処理結果をどこへ戻すかがコード上に現れます。途中の結果を別の名前で確認してから代入する方法も使えます。
原本を保持したい場合は、DataFrame.fillnaの結果を別の変数へ受け取ります。辞書を渡すと、どの列にどの値を使うかも指定できます。全列へゼロを入れる処理より、amountだけを補完する意図が読み取りやすくなります。備考や分類などの列まで同じ値で埋める必要はありません。
assigned = raw.copy()
assigned["amount"] = assigned["amount"].fillna(0)
new_table = raw.fillna({"amount": 0})
assert assigned.equals(new_table)
assert new_table["amount"].isna().sum() == 0
assert raw["amount"].isna().sum() == 3
一方、親のDataFrameに対して直接inplace=Trueを使う形もあります。問題はinplaceという引数自体ではなく、どの相手に使っているかです。取り出した列へのメソッド呼び出しと、親のDataFrameへのメソッド呼び出しを、同じ更新経路として扱わないようにします。
| 書き方 | 処理する相手 | 確認すること |
|---|---|---|
| df[列].fillna(0, inplace=True) | 取り出したSeries | CoWでは親の表が更新されない |
| df[列] = df[列].fillna(0) | 結果を親の列へ代入 | その列の欠損数と対象ID |
| new = df.fillna({列: 0}) | 結果を新しい表として保持 | 原本とnewの両方 |
| df.fillna({列: 0}, inplace=True) | 親のDataFrame | 2.2.3の戻り値はNone、3.1.0は更新した表 |
比較するときは、一つ前の方法で書き換えた表を使い回さず、raw.copyから始めます。すでに欠損を埋めた表に同じ処理をしても、失敗しているのに欠損ゼロと見える場合があります。修正前の状態を再現してから、修正後の結果と比べることが検証の前提です。
Noneを返す版と、更新した表を返す版を区別する
「inplace=Trueなら戻り値はNone」という説明は、現行のfillnaへ一律には使えません。今回確認した2.2.3ではNoneですが、3.1.0では更新したオブジェクトが返ります。公式の3.1.0変更点でも、fillnaやreplaceのように値を直接更新できるメソッドはinplaceを保持し、3系では更新後のオブジェクトを返すと説明されています。
この違いを確かめるため、表をdirect、戻り値をresultと分けます。どちらの確認環境でもdirectの欠損はゼロになりますが、resultの内容は版で違います。古い環境でNoneになった事実を、3.1.0でも同じ結果になると説明しないことが大切です。
direct = raw.copy()
result = direct.fillna({"amount": 0}, inplace=True)
if major_version < 3: # 確認環境: 2.2.3
assert result is None
else: # 確認環境: 3.1.0
assert result is direct
assert direct["amount"].isna().sum() == 0
assert raw["amount"].isna().sum() == 3
この分岐は、実行確認した二つの版を同じ教材で照合するためのものです。将来のすべての版や、ほかのメソッドの戻り値まで保証するものではありません。少なくとも今回の比較では、Noneという説明だけでは現在の仕様を表せないと分かります。
| 確認環境 | 親DataFrameでinplace=True | 取り出した列でinplace=True |
|---|---|---|
| pandas 2.2.3 / CoW有効 | 親を更新し、Noneを返す | 親は更新されない |
| pandas 3.1.0 | 親を更新し、そのオブジェクトを返す | Seriesを返しても、親は更新されない |
3.1.0で取り出したSeriesが返ってきても、親が補完されたとは判断できません。「戻り値があるか」「どの表が変わったか」を別に確かめる点は同じです。互換性を保つ目的で結果の代入へ書き換える場合も、inplaceを付けない結果を、更新したい変数へ明示的に受ける形から確認します。
さらに3.1.0では、drop、rename、reset_indexなど、実際には値を直接更新しない一部メソッドのinplaceが非推奨になっています。fillnaを含むすべてのinplaceが廃止された、という説明でもありません。今回扱うfillnaの仕様と、別メソッドの非推奨化を混ぜないようにしてください。
ノートブックでは最後の式だけが表示されることがあります。表示が出たかどうかより、必要な変数を明示して内容を確認します。版を更新した後に表示が変わった場合も、処理対象、戻り値、欠損数を分けて比べると、どの変更が影響したかを調べやすくなります。
復旧時に原本まで上書きしたくないなら、読み込み直した直後の表から再開します。セルを何度も実行した履歴がある場合は、どのセルで変数が変わったかを追いかけるより、読み込みと最小の補完だけを別の検証セルにまとめる方法もあります。原本へ戻れる状態を残しておきましょう。
条件に合う一件だけを変えるならlocで対象を示す
金額の欠損すべてをゼロと扱ってよいとは限りません。ここでは「B部署の未記入だけ、確認の結果ゼロと扱う」という架空の条件を追加します。A部署とC部署の未記入は不明のまま残すため、列全体のfillnaと結果が違ってよい例です。
条件は、部署がBであることと、amountが欠損であることの両方です。R3もB部署ですが、すでにゼロが入力されているので変更対象にはしません。対象を欠損に限定することで、元のゼロと補完したゼロを混同しにくくなります。変更前のmaskも確認用に残します。
partial = raw.copy()
mask = partial["dept"].eq("B") & partial["amount"].isna()
changed_ids = partial.loc[mask, "id"].tolist()
partial.loc[mask, "amount"] = 0
assert changed_ids == ["R4"]
assert partial["amount"].isna().sum() == 2
assert partial.loc[partial["id"].eq("R3"), "amount"].iloc[0] == 0
この後の欠損二件は、処理漏れではありません。条件から外したR2とR5を残すという仕様どおりです。「欠損がゼロになるまで補完する」という一律の成功条件にしてしまうと、未確認の値まで埋める方向へ進んでしまいます。対象と除外理由を先に決めてください。
条件付き抽出が想定と違う場合は、更新方法とは別に条件を見直します。部署コードに空白がある、数値と文字列が混在しているなどの確認は、isinで抽出できないときの型と対象の確認ともつながります。更新先が正しくても、条件が違えば別の行を変えてしまいます。
反映されたように見える結果を、IDと欠損数で検算する
合計が同じでも、変更したセルは同じとは限らない
今回の原表では、欠損を除いたamountの合計は400です。欠損三件をゼロにしたassignedも、B部署だけをゼロにしたpartialも合計400になります。数値の合計が一致することだけでは、ゼロを何件追加したかも、不明な値をどこへ残したかも説明できません。
そこで、欠損数、ゼロの数、行数、対象IDを並べて確認します。原本のゼロはR3の一件だけです。全体補完ではR2、R4、R5にもゼロが入り、ゼロは四件になります。部分補完ではR4にだけ追加され、ゼロは二件です。この差は合計の照合だけでは見えません。
assert len(raw) == len(assigned) == len(partial) == 6
assert raw["amount"].sum() == assigned["amount"].sum() == partial["amount"].sum() == 400
assert raw["amount"].eq(0).sum() == 1
assert assigned["amount"].eq(0).sum() == 4
assert partial["amount"].eq(0).sum() == 2
assert partial.loc[partial["amount"].isna(), "id"].tolist() == ["R2", "R5"]
| 表 | 欠損数・ゼロ数 | 意味 |
|---|---|---|
| raw | 欠損3・ゼロ1 | 原本。三件の金額は不明 |
| assigned | 欠損0・ゼロ4 | 三件を一律にゼロへ補完 |
| partial | 欠損2・ゼロ2 | 確認したR4だけをゼロへ更新 |
表の見た目が整うほど、不明な状態が消えたことを見落としやすくなります。更新前の欠損フラグを別に保存すれば、後から補完値だけを確認できます。補完が必要な理由、採用した値、除外した行を、処理のメモに残すと再実行時にも同じ判断を再現できます。
補完した後で部署別の集計へ進む場合は、groupbyの欠損キーと合計の検算も参考になります。金額の欠損と部署名の欠損では、集計へ与える影響が違います。更新した値だけでなく、集計のキーとして残っている値も確認してください。
空文字・文字列のnan・欠損値を先に区別する
fillnaは、空欄に見える文字列を何でも補完するメソッドではありません。空文字、空白一個、文字列のnan、実際の欠損値は同じものではないためです。CSVの読み込み設定によっても、文字列が欠損として解釈されるかどうかが変わる場合があります。
次のSeriesは、表示だけでは違いが分かりにくい五つの値を含みます。この例で欠損として判定されるのはNoneの一件です。文字列のnanや空文字は、そのまま残ります。fillnaが効かなかったと考える前に、実際に何が入っているかと型を確認します。
looks_empty = pd.Series([None, "", " ", "nan", 0], dtype="object")
assert looks_empty.isna().tolist() == [True, False, False, False, False]
filled = looks_empty.fillna("未記入")
assert filled.tolist() == ["未記入", "", " ", "nan", 0]
print([repr(x) for x in looks_empty])
ただし、空文字やnanという文字列を一律に欠損へ変えればよいわけでもありません。コードや名称の一部として有効な値である可能性があるためです。入力仕様を確認し、空白の除去や欠損への変換を認める範囲を決めてから処理します。想定外の値は別の確認表へ残せます。
数値に変換する前処理なら、カンマや円が付いた金額を数値化する手順も参照できます。元から空だった値と、変換に失敗して欠損になった値を区別しておくと、fillnaでエラーの痕跡まで消してしまうことを避けやすくなります。
別の表を表示していないか、変数と実行順を確認する
rawからcopyを作り、copyだけを更新した場合、rawを表示すれば変更前の値が出ます。これは更新の失敗ではなく、確認している表が違う例です。変数名がdf、df2、tempだけだと、時間がたってからどれを出力へ渡すのか分かりにくくなります。
「原本」「条件付き更新後」「集計用」と役割を表す名前を付けると、変えたくない表も見えます。一方、alias = rawのような代入は、新しい独立した表を作る操作ではありません。同じDataFrameに別名を付けただけなら、一方からの更新がもう一方にも見えます。CoWは、別名への代入を原本の自動バックアップにする仕組みではありません。
base = raw.copy()
alias = base
alias.loc[0, "amount"] = 999
assert base.loc[0, "amount"] == 999
assert raw.loc[0, "amount"] == 100
independent = raw.copy()
independent.loc[0, "amount"] = 888
assert raw.loc[0, "amount"] == 100
また、ノートブックで読み込みセルだけ再実行すると、更新した変数が読み込み直した値へ戻ることがあります。反対に、加工セルだけ再実行して、前の加工結果にさらに変更を重ねる場合もあります。上から順に最小の一連の処理を実行し、途中ごとの件数を出すと切り分けやすくなります。
複数の前処理をつないでいるなら、今回の補完の直後で一度検算してから次へ進みます。mapで生じる欠損の種類を分ける記事のような変換を後に置く場合、補完後に別の欠損が増えることもあります。最終表だけを見ず、どの段階で変化したかを追ってください。
保存したCSVが変わらない場合は、書き出す表を確認する
Python内のDataFrameを更新することと、ディスク上のCSVを書き換えることは別の操作です。メモリー上の表を変えただけでは、すでに保存したCSVへ自動的に反映されません。更新後の表をどのパスへ書き出したか、そしてどのファイルを開いているかを確認します。
検証時は、原本のファイルへすぐ上書きするより、加工後を別の名前で保存して読み戻す方法が使えます。表示していたのはassignedなのに、to_csvに渡したのはrawだった、という取り違えもここで見つけられます。エクスプローラーなどで同名ファイルが複数見える場合は、保存先のフォルダも確認しましょう。
以下は実ファイルを上書きせず、文字列バッファで保存と読み戻しの経路を試す例です。CSVではデータ型や空欄の解釈が変わることがあるため、この検算だけですべての実ファイルの保存条件を確認したことにはなりません。今回は欠損数とIDの保持を照合します。
from io import StringIO
csv_text = partial.to_csv(index=False)
reloaded = pd.read_csv(StringIO(csv_text))
assert len(reloaded) == 6
assert reloaded.loc[reloaded["amount"].isna(), "id"].tolist() == ["R2", "R5"]
assert reloaded["id"].tolist() == raw["id"].tolist()
保存後に余分な列が増える問題は、to_csvのindexと読み戻しを確認する記事で扱っています。今回のinplaceと、保存時のindex引数を一緒に変えず、一つの違いずつ確認すると、どの修正が必要だったのかを説明できます。
修正したコードを、再実行できる確認手順へまとめる
最終的には、版の確認、原本の読み込み、補完対象の抽出、更新、対象IDと件数の照合、保存後の確認を一連の手順にします。どの表を更新するかが明確なら、inplaceを使うかどうかだけを覚えるより、別の前処理でも同じ考え方を応用できます。
確認結果の記録は、単に「エラーなし」ではなく、「原本六行、金額欠損三件、B部署のR4だけを補完、残る欠損はR2とR5、合計400」のように対象を残します。別の日に入力が増えた場合も、この記録と比較すれば、データの変化とコードの不具合を分けて考えられます。
ゼロ補完を採用するかどうかは、この教材だけでは決まりません。未記録をゼロと扱う根拠がないなら、欠損を残して確認へ回す方が目的に合うこともあります。警告が消え、表がきれいになったことより、元の情報をどのような意味へ変えたかを説明できる状態を目指してください。
列の更新を直すだけでなく、Pythonの基礎から前処理を学び直したい場合は、Python Winnerも比較先になります。公式には基礎・応用を含む講座とマンツーマンレッスンの案内があります。手元の版と短い再現例を持ち、CoWや欠損補完の質問が講座内で扱えるか、受講総額、予約方法を無料カウンセリング・受講相談で確認してください。この六行の例で原因が分かった場合は、公式資料と手元のデータで練習を続ける方法もあります。(公式情報確認日:2026年10月30日、Python Winner公式)。
よくある質問
Q. inplace=Trueを付ければ元のDataFrameは変わりますか?
呼び出す相手を確認してください。CoWでは、DataFrameから取り出した列へのinplace操作で親は更新されません。列へ結果を代入するか、親DataFrameを更新する形に分けます。
Q. fillnaの結果を代入したらNoneになったのはなぜですか?
今回の2.2.3ではinplace=Trueの戻り値はNoneですが、3.1.0では更新したオブジェクトを返します。まず版を確認してください。結果を代入する形に揃えるなら、inplaceを付けない結果を更新先へ受ける方法を確認します。
Q. locを使っても欠損が残るのは失敗ですか?
条件に含めた行だけを変えたなら、条件外の欠損は残ります。教材ではB部署のR4だけを補完するため、R2とR5は不明のままです。対象IDと残るIDを照合してください。
Q. 空文字やnanという文字列もfillnaで変わりますか?
空文字、空白、文字列のnanは実際の欠損値と別です。reprやisnaで中身を確認します。文字列を欠損へ変える場合も、入力仕様に基づく変換規則を先に決めてください。
Q. 合計が同じなら補完は正しくできていますか?
合計だけでは判定できません。教材では補完前、全体補完、部分補完のすべてで合計400ですが、欠損数とゼロの数が違います。変更したID、残した欠損、行数も確かめます。
Q. この記事はpandas 3.1.0でも実行していますか?
2.2.3のCoW有効時と3.1.0の両方でコードを確認しています。親の更新の有無と、inplaceの戻り値を区別しています。別の版やメソッドでも同じだと推測せず、利用中の環境で照合してください。
更新先を明示し、結果を対象IDで確かめることが修正の中心です。 本文の六行を別の補完値や条件へ変えた場合も、原本、変更対象、残る欠損を先に紙へ書き、コードの出力と比べると理解を確かめられます。