pandasのmapでNaNになったら、すぐにfillnaで埋めず、元の値が欠損か、辞書のキーに存在するか、対応する辞書の値も欠損かを別々に調べます。 変換結果の空欄だけを数えても、どこを直すべきかは分かりません。

対象は、CSVを読み込んで商品コードや区分を別の値へ変換したい人です。Seriesの列選択が分かる段階から取り組めます。二つの表の結合で行数が増える問題は、merge後の行数・キーの確認で扱っています。本稿は一つの列と対応表の照合に絞ります。

一次資料確認日:2026年10月22日。Series.map、Series.isna、Series.isin、Series.replaceの現行公式資料を参照しました。コードの実行確認環境はPython 3.12.14、pandas 2.2.3です。現行資料の版と実行環境の版は区別しています。以下の六行と対応表は編集部が作成した架空教材です。

map後の欠損を、元から欠損、辞書に未登録、登録先の値が欠損の三つに分ける。登録されたゼロは欠損に含めない。
変換後の空欄を一種類と考えず、入力と対応表へ戻って確認します。

map後の欠損は、元の値と辞書の対応から切り分ける

辞書にない値が欠損になるのは、対応を指定していないため

通常の辞書を渡すmapは、Seriesの値を辞書のキーと照合して、対応する値へ変換します。辞書にそのキーがなければ、結果は欠損になります。これは、元の値が壊れていると決まったことでも、pandasが勝手に行を削除したことでもありません。

例えば入力がA、B、Dで、対応表がAは10、Bは0だけなら、Dの変換先は指定されていません。Dを見たときにAの10で代用するのか、対象外にするのか、担当者へ確認するのかはデータのルールです。ライブラリの設定だけでは決まりません。

ここで結果の欠損を一律に0へ置き換えると、「Bには正式に0を割り当てた」と「Dは対応が分からない」が同じ数値になります。後から合計だけを見ても、この違いを取り戻せません。ゼロで計算を続けたい場合も、その根拠と変換前の状態を残してください。

辞書にない値が少数だから無視してよい、という判断も別に必要です。例えば六件中一件のDが、集計対象から外してはいけない新しい区分なら、件数が少なくても確認が必要です。本稿では発生率の一般的な許容値を決めず、何件を保留にしたかを明示する練習にします。

mapは対応表の内容を代わりに判断する道具ではありません。まず「一つのコードに一つの変換先がある」「未登録は保留する」「登録済みの0は有効」という教材のルールを置きます。実務へ応用するときは、その三点が自分の資料にも合うか確認してください。

元から欠損の行と、辞書の値が欠損の行を分ける

同じNaNの表示でも、元の列に値が入っていなかった場合と、変換によって欠損になった場合があります。元の列を上書きしてしまうと、後からこの区別を付けにくくなります。最初の練習では、入力、変換後、判定結果を別の列に置きます。

今回の入力には、A、B、C、元から空欄、D、Aを入れます。対応表にはAを10、Bを0、Cを欠損として登録します。Cは辞書にあるので「未登録」ではありません。しかし、対応する値が欠損なので、この教材では値の設定を確認する行として保留します。

この区別は、問い合わせ先を決めるときにも役立ちます。元から空欄なら入力側、Dの未登録ならコード台帳、Cの値欠損なら既存の対応内容を確認します。三つを「変換失敗三件」とだけ報告すると、誰が何を調べるかを決めにくくなります。

欠損の確認にはisnaを使い、NaNとの単純な等号比較で判定しないようにします。一方、空文字や半角スペースがそのまま入っている場合は、欠損としての扱いを別に定義する必要があります。空白を消す処理を入れる前に、元データで空白が何を意味するかを確認してください。

欠損を扱う詳しい入口は、CSVの欠損と重複を確認する練習にまとめています。今回は、欠損の種類を増やすより、元の欠損一件、未登録一件、登録値欠損一件という、手計算できる小さな状態を作ります。

map・replace・mergeは、残したいものから選ぶ

「辞書にない値を残したい」という目的なら、mapを使い続けて欠損だけ埋める前に、replaceが意図に合うかを検討します。replaceで指定した値だけを置き換える処理と、対応表に従って全件を変換する処理は、完成後の列が持つ意味が違います。

例えばAを「標準」、Bを「休止」へ表記修正し、CやDはそのままでよいなら、対象を限定した置換として説明できます。一方、全行を数値の処理区分へ変換したい場合、CやDが文字のまま残った列を完成と見なすのは難しいはずです。

対応表を使う目的と、未登録をどう扱うか
方法合う目的確認すること
Series.map一つの値を対応表の値へ変換する未登録キーと登録値の欠損を別に確認する
Series.replace指定した表記だけ置き換え、それ以外を残す残った元コードが完成列に混ざってよいか
DataFrame.merge別表から複数の説明列を付けるキーの一意性、結合漏れ、行数を確認する

複数の列を付けたい、対応表に有効日や拠点がある、といった場合は、一対一の辞書へ無理に押し込まず表として扱う方法もあります。辞書にすると同じキーへ複数の条件を持たせにくいためです。ただしmergeへ変えれば自動で正しくなるわけでもなく、一意性などの確認は残ります。

本稿のコードは、一つのコードに一つの値というルールが明確な教材です。現場の対応表が複雑な場合に、その条件を省いて短いコードへ直すことは勧めません。方法の短さより、「何を対応と呼ぶか」を説明できることを優先します。

na_actionを変えても、未登録キーの行き先は増えない

mapの例で見かけるna_actionは、欠損入力を関数へ渡すか、といった欠損の扱いに関係する指定です。これを変更すれば、辞書に新しいキーが追加されるわけではありません。未登録のDをどこへ変換するかという問題は、対応表の側に残ります。

「欠損を無視する」と「辞書にない値を元のまま残す」を同じ意味として読まないでください。関数を使う例と辞書を使う例を混ぜてコピーすると、どの段階を変えたのかが分かりにくくなります。まず通常の辞書一つで挙動を確認します。

また、欠損が増えたからといって、すぐにmapの不具合を疑う必要もありません。入力の型、辞書のキー、辞書の値という三つを並べると、設定どおりだが想定と違った箇所を見つけられます。最小の一件を取り出して説明してから、設定変更へ進みましょう。

例えばDの一件だけを抜き出し、「入力は文字列D」「辞書のキーはA・B・C」「Dの登録はない」と書けば、なぜ欠損になったかを人に伝えられます。コードの実行結果だけを示すより、期待した対応先が何だったかも添えると修正方針を相談しやすくなります。

元から欠損の行を保持することと、未登録キーを有効な値へ変換することは別の作業です。前者だけを設定して後者も済んだと考えず、出力の判定列でそれぞれ何件あるかを見直します。教材ではどちらも一件ずつ残し、確認対象として見える状態を作ります。

数字の7と文字列の「007」は、同じコードと決め付けない

辞書のキーと入力の型が違う場合も、期待した変換に届かないことがあります。見た目が数字でも、識別コードは数値の大小ではなく文字列として定義されていることがあります。変換前にdtypeと実際の値を確認してください。

例えば正式コードが「007」なのに、読み込み時点で数値7になっていた場合、あとから文字列にしても「7」にしかなりません。三桁にそろえるべきかはコード体系の仕様です。「何でもゼロ埋めすれば直る」という処理は、異なる識別子を同一視する恐れがあります。

先頭ゼロを保った読み込み方は、PythonでCSVの先頭ゼロと文字コードを確認する方法を参照してください。今回の六行教材はAからDという文字列を使い、ファイルの読み込みによる変化を切り離しています。

空白や全角文字の統一も、同じように元列を残してから試します。「A」と「Aの後ろに空白」が実際に同じ区分なのかを確認し、統一前後で新しい重複や意図しない対応ができていないか調べます。整形後の値だけを残すと、変更の根拠をたどれません。

型の修正を入れたときは、未登録件数が減っただけで終わらず、減った行のIDを確認します。本来別のコードがたまたま同じになっていても、件数だけなら改善に見えるためです。未登録を減らすことより、正しい対応へ変わったことを確かめます。

六行の教材で、変換・保留・検算を一緒に実装する

期待する六件を先に書いてからコードを実行する

教材の行IDはR1からR6です。R1とR6のAは10へ、R2のBは0へ変換できます。R3のCは登録値が欠損、R4は元から欠損、R5のDは未登録として分けます。有効三件と保留三件を合わせて六件になる状態が、今回の期待結果です。

ここで、結果の合計20だけを期待値にしないようにします。二つの10を足すと20ですが、Bのゼロが消えても合計は同じです。合計が一致したことでは、ゼロの行を正しく残せたかを証明できません。行IDと件数も一緒に確かめます。

次のコードはpandasを利用できる環境で動き、ファイルへの保存やネットワーク送信は行いません。辞書のキーには欠損を使わず、対応する値には一つだけ欠損を置きます。判定の重なりを避け、四つの状態へ分けます。

import pandas as pd

df = pd.DataFrame({
    "row_id": ["R1", "R2", "R3", "R4", "R5", "R6"],
    "code": ["A", "B", "C", None, "D", "A"],
})
lookup = {"A": 10, "B": 0, "C": pd.NA}

out = df.copy()
out["mapped"] = out["code"].map(lookup)
source_missing = out["code"].isna()
key_exists = out["code"].isin(list(lookup))
unmapped = ~source_missing & ~key_exists
value_missing = ~source_missing & key_exists & out["mapped"].isna()
valid = ~source_missing & key_exists & out["mapped"].notna()

out["status"] = "matched"
out.loc[source_missing, "status"] = "source_missing"
out.loc[unmapped, "status"] = "unmapped_key"
out.loc[value_missing, "status"] = "mapped_value_missing"

assert out["status"].tolist() == [
    "matched", "matched", "mapped_value_missing",
    "source_missing", "unmapped_key", "matched",
]
assert int(valid.sum()) == 3
assert int(source_missing.sum()) == 1
assert int(unmapped.sum()) == 1
assert int(value_missing.sum()) == 1
assert int(valid.sum() + source_missing.sum()
           + unmapped.sum() + value_missing.sum()) == len(out)
assert out.loc[valid, "row_id"].tolist() == ["R1", "R2", "R6"]
assert out.loc[out["row_id"].eq("R2"), "mapped"].iloc[0] == 0
assert out.loc[valid, "mapped"].sum() == 20
print(out.to_string(index=False))

このコードでは、欠損の見た目がNaN、None、NAなどのどれに見えるかより、isnaによる判定を使っています。表示形式やdtypeは入力とpandasの版によって変わることがあるため、画面の文字列が同じかだけで検算しないようにします。

最初はコードを短くまとめず、それぞれのマスクがどの行を選ぶかを順番に読みます。確認に使う条件が独立して見えると、一行追加した際に何が変わったかを追いやすくなります。動いた処理を、自分の言葉で説明するところまでが練習です。

有効なゼロを落とさず、保留三件を取り出す

Bの変換先0は、教材で正式に割り当てた値です。「0ではない行だけ残す」という条件で抽出すると、この有効なR2を除外してしまいます。欠損を除外したいなら欠損かどうか、正数だけ選びたいなら正数かどうか、と目的を分けます。

数値を含む列へ安易に真偽値変換をかけて、値があるかを調べるのも避けます。0は偽として扱われるため、「入力されている0」と「入力されていない」を取り違えやすくなるからです。本稿では値の大小ではなく、対応関係と欠損の有無から有効を決めています。

保留一覧は、元のコードと判定理由を残した別表にします。R3は対応する値の確認、R4は入力の確認、R5はコード登録の確認です。同じ連絡先へ確認する場合でも、何が分からないのかを具体的に記録すると、修正の取り違えを防ぎやすくなります。

ready = out.loc[valid, ["row_id", "code", "mapped"]].copy()
held = out.loc[~valid, ["row_id", "code", "status"]].copy()

assert ready["row_id"].tolist() == ["R1", "R2", "R6"]
assert held["row_id"].tolist() == ["R3", "R4", "R5"]
assert len(ready) + len(held) == len(df)
assert set(ready["row_id"]).isdisjoint(set(held["row_id"]))
print("有効:", len(ready), "保留:", len(held))

保留を別表へ分けることと、元データから削除することは違います。六件のうち三件を判断待ちにしているという情報を、合計値と一緒に渡してください。「処理できた三件だけで集計した」と明記すれば、全件を対象にした数字との混同を避けられます。

また、保留の件数が多い場合に、確認を省いて全部同じ値へ割り当てる必要はありません。対象期間や入力元を狭めて原因を調べる、小さな単位で対応表を見直すといった方法があります。どの時点の表で確認したかもメモしておきます。

辞書を直した後は、減った欠損ではなく変わった行を確認する

例えば担当者からDの値は5だと確認できた場合は、対応表を更新して再変換します。ただし、辞書にDを追加した事実だけではなく、R5が保留から有効へ変わり、ほかの行の状態が勝手に変わっていないかを確認します。

Cの値も決まった場合と、Dだけ分かった場合は、完成状態が違います。Dを5へ追加しても、元から欠損のR4と登録値欠損のR3は残ります。NaNが三件から二件へ減ったから「すべて直った」と報告しないようにしてください。

変更した対応表は、元の辞書とは別名で試すと比較しやすくなります。本番用の対応表をいきなり上書きするより、小さな入力で期待した行だけが変わることを確かめます。実務では確認日と、値を採用した根拠も一緒に記録します。

revised = {**lookup, "D": 5}
after = df["code"].map(revised)
assert after.loc[df["row_id"].eq("R5")].iloc[0] == 5
assert after.loc[df["row_id"].eq("R2")].iloc[0] == 0
assert int(after.isna().sum()) == 2
assert df["code"].tolist() == ["A", "B", "C", None, "D", "A"]

この例の5は教材の仮定です。実在するコードの変換先を推測して補っているわけではありません。読者が自分の表へ置き換えるときも、「この値なら計算が通る」ではなく、対応表の正しい根拠があるかを確かめてください。

検算は一度作れば終わりではありません。対応表の更新で、新しいコードが追加される場合も、既存コードの意味が変わる場合もあります。変更前後の行ID、欠損理由、ゼロ件数を並べると、集計結果が変わった理由を追いやすくなります。

replaceで値が残った場合も、目的に合うかを検算する

mapの欠損を避けたいという理由だけでreplaceへ変えると、エラーが出ず見た目も埋まった列ができます。しかし、元のコードが残ってよいという要件がなければ、処理が完成したとは限りません。出力の意味を先に決める必要があります。

次の例は、AとBのラベルだけを直す処理です。CとDはそのまま残ります。これはコード一覧の表記修正としては使える場合がありますが、全行を数値へ変換したい目的には合いません。結果が欠損でないことと、値が要件を満たすことは別です。

labels = df["code"].replace({"A": "標準", "B": "休止"})
assert labels.iloc[0] == "標準"
assert labels.iloc[1] == "休止"
assert labels.iloc[2] == "C"
assert labels.iloc[4] == "D"
assert pd.isna(labels.iloc[3])

さらに、mapの結果へ元の列を使ってfillnaすると、未登録キーだけでなく、登録先が欠損だったCも元の値へ戻ります。元コードと変換先が混ざってよいかを決めずに使うと、問題が見えにくくなります。残す対象を明確なマスクで指定するほうが説明しやすい場面もあります。

置換の後に条件抽出する場合は、isinで型と先頭ゼロを確認する方法も参考になります。変換と抽出を一度に書かず、それぞれの結果を一度見てからつなぐと、どこで行が変わったかを把握できます。

相談用のメモには、入力・対応表・期待値・判定を残す

教材を自分の作業へ置き換えるときは、入力の行IDが一意かも確かめます。六行のうち二行が同じIDだと、有効一覧と保留一覧の対応を人が追いにくくなるためです。行番号を識別子として使う場合も、途中の並べ替えで何を指す番号かが変わらないよう注意します。

今回の判定では、四つのマスクが互いに重ならず、合計六件になることを確認しています。実装へ条件を追加した後も、この確認を残します。例えば「対象外」を新設するときは、どの状態より先に判定するかを決めないと、一つの行を二つの理由へ数えてしまうことがあります。

また、確認表の行順が変わったときは、先頭から三行が同じかではなく、行IDと状態の組み合わせを比べます。順番が違うだけなのか、対象が入れ替わったのかを分けるためです。必要な識別情報を残すことは、結果の値そのものを確認する助けになります。

練習をもう一段進めるなら、Aの行を一件追加し、有効件数が四件、合計が三十になることを予想してから実行します。次に未登録のEを一件追加し、合計は変わらず保留だけ増えることを確かめます。数の変化と、状態の変化を別々に説明できるかを見ます。

辞書の値に欠損を許す設計なら、Cを保留ではなく正式な「値なし」として扱うことも考えられます。その場合も未登録と同じ欄へ押し込めず、登録済みという情報を残してください。本稿の保留ルールは教材の選択であり、欠損を持つすべての対応表を不正とみなす説明ではありません。

つまずいたときに共有すると役立つのは、長い元ファイルではなく、再現に必要な数行です。機密情報や顧客名は含めず、架空のコードへ置き換えても同じ問題が起きるか試してください。型や欠損の配置は、原因に関係するので省き過ぎないようにします。

相談メモは「六行中、元欠損一件、未登録一件、登録値欠損一件。有効三件にはゼロ一件を含む。期待したのはDの対応先を確認してR5だけ有効へ移すこと」という形で書けます。これなら、単にNaNを消したいという依頼より、変更する範囲が伝わります。

対応表をいつの版で確認したかも残します。最新版のコード一覧を使うべき処理と、過去時点の区分を再現すべき処理では、使う表が違うことがあります。本稿は一時点の辞書だけを扱いますが、その前提を実務へ持ち込む前に見直してください。

学習の次の段階は、社会人がPythonでCSV分析を始める手順で、読み込みから結果の確認までをつなぐ練習です。講座へ相談する場合は、無料相談で確認したい質問を参考に、コードの実行、表の意味、確認の手順のどこで支援が必要かを分けましょう。

よくある質問

Q. mapでNaNになるのはエラーですか?
A.

通常の辞書にキーがない場合、結果が欠損になるのは公式に示される挙動です。元の欠損や辞書の値の欠損もあり得るので、入力と対応表を別々に確認します。

Q. fillna(0)ですべて埋めてよいですか?
A.

ゼロを使う意味が決まっている場合に限って検討します。未登録と正式なゼロを同じ値にすると、原因が見えなくなるため、先に判定理由を残します。

Q. na_action='ignore'なら辞書にない値も残りますか?
A.

未登録キーへ変換先を追加する指定ではありません。元の値を残す目的なら、replaceなどの方法が要件に合うかを確認します。

Q. 辞書にキーがあれば変換成功ですか?
A.

対応する辞書の値そのものが欠損の場合があります。本稿では登録の有無と、変換後に有効な値があるかを分けて判定します。

Q. 数値7と文字列007が一致しないときは?
A.

識別コードの正式な型と桁数を確認します。数値化で先頭ゼロを失った場合、文字列への再変換だけでは元のコードは復元できません。

Q. 合計が合えば検算は終わりですか?
A.

ゼロの行が消えても合計が変わらない場合があります。行ID、有効と保留の件数、ゼロの保持、判定の重なりも一緒に確認します。

最後に、出力の空欄を埋めることより、なぜ空欄になったかを説明できる状態へ戻しましょう。六行のどれが入力不足、未登録、登録値の不足なのかを分け、有効なゼロを残せれば、次に直す対象を具体的に決められます。