explodeは、空リストを行ごと消す処理ではありません。空リストは欠損値の行になり、元から欠損だった行も残ります。 展開後のNaNだけを見ると、どちらだったか区別できなくなるため、処理前の状態と元のIDを残して検算します。

対象は、CSVや表をpandasで読み込み、商品タグや複数回答を一件ずつの行へ展開したい人です。教材には七件の架空データを使います。文字列をリストと誤認した状態、同じ回答の重複、リストの中に欠損がある状態も含めて、出力の件数を手計算と照合します。

公式情報の確認日:2026年10月28日。DataFrame.explodeとDataFrame.drop_duplicatesを参照しています。現行資料はpandas 3.1.0、以下の実行確認環境はPython 3.12.14、pandas 2.2.3です。利用中の版でも再現結果を確かめてください。

元の7行を状態付きで展開すると10行。有効タグ6件と欠損3行と形式違い1行に分かれ、タグがある元IDは4件になる。
行が増えた数と、実際に数えたい対象の件数は一致するとは限りません。

explodeの空リストとNaNを、元の状態から確かめる

七行の教材で、何を一件と数えるかを決める

教材の一行は、一件の受付記録です。idは受付の識別子、tagsは選択されたタグ、amountは受付全体の金額とします。タグが複数あっても受付自体が増えたわけではありません。この単位を決めておくと、展開後の十行を十件の受付と報告する間違いを避けられます。

O1にはredとblue、O2には空リスト、O3には元の欠損、O4にはgreen、O5にはredが二つ、O6には区切り文字列、O7には欠損とblueを入れます。O2は「選択なしと記録済み」、O3は「入力状況が不明」と教材内で定義します。同じ空欄に見えても、業務上の意味は違います。

実際の業務で空リストを未回答と定義しているなら、その定義に合わせてください。本稿の区別をそのまま会社のルールにする必要はありません。大切なのは、表示結果を見てから都合のよい意味を付けるのではなく、元の保存形式と記録の意味を先に確認することです。

import pandas as pd

df = pd.DataFrame({
    "id": [f"O{i}" for i in range(1, 8)],
    "tags": [["red", "blue"], [], None, ["green"],
             ["red", "red"], "red,blue", [None, "blue"]],
    "amount": [100, 200, 300, 400, 500, 600, 700],
})
assert df["id"].is_unique
assert len(df) == 7
print(df)

ここでidが一意であることも確認します。もともと同じidが複数行ある表では、展開後のid重複がexplodeによるものか、入力時点のものか判断しにくくなります。元表の主キーが複数列なら、その組み合わせを残してください。行番号だけに依存すると、並べ替えや読み戻しで対応が変わる場合があります。

複数回答の分類基準そのものをAIで作る場合は、複数ラベルの集計を検算する記事も参考になります。今回の目的は分類内容の良し悪しではなく、すでに入っているリストを展開したときの件数と意味を確認することです。

展開前に空リスト・欠損・形式違いを別の列へ残す

explode後にtagsがNaNの行だけを探しても、それが空リスト由来か、元の欠損由来か、リスト内の欠損由来かは判断できません。そこで状態列を展開前に追加します。リストかどうかを先に判定し、リストなら長さがゼロかを調べます。

pd.isnaを何にでも直接適用し、その結果をifへ渡す方法には注意が必要です。リストに対しては要素ごとの結果が返り、単一の真偽値として使えないことがあります。以下ではまずlistを処理し、その後はこの教材で想定した文字列か欠損だけに対象を限定します。

def source_state(value):
    if isinstance(value, list):
        return "empty_list" if len(value) == 0 else "list"
    if value is None or (isinstance(value, float) and pd.isna(value)):
        return "missing_source"
    return "scalar_needs_review"

df["source_state"] = df["tags"].map(source_state)
df["source_length"] = df["tags"].map(
    lambda value: len(value) if isinstance(value, list) else None
)
assert df["source_state"].value_counts().to_dict() == {
    "list": 4, "empty_list": 1, "missing_source": 1,
    "scalar_needs_review": 1,
}

この関数はすべてのPythonオブジェクトに対応する汎用の欠損判定ではありません。pd.NA、配列、辞書、タプルが実データに来る場合は、許可する型と例外の扱いを先に追加してください。未知の型を勝手に空リストへ変えると、形式不備の記録を「選択なし」と誤って保存してしまいます。

状態列を作った後は、元のtags列をすぐ上書きしない方が原因を追いやすくなります。原本列、整形後の列、状態列を分け、どの変換を認めたかを確認メモへ残します。入力が毎月届く資料なら、形式違いの件数が急に増えていないかも比較できます。

展開前の状態を残す理由
元の値explode後この教材での扱い
空リスト []欠損の行を一行残す選択なしと記録済み。元IDを残す
元からNone欠損の行を一行残す入力状況が不明。空リストと区別
[None, blue]欠損一行とblue一行有効要素と不明な要素を別に数える
文字列 red,blue文字列のまま一行自動分割しない。形式確認の対象

explode後の十行を、期待した行数と照合する

一列を展開する今回の入力では、非空リストは要素数の分だけ、空リストと欠損とスカラーはそれぞれ一行分だけ残ります。そのため期待行数は、二足す一足す一足す一足す二足す一足す二で十行です。空リストの要素数ゼロをそのまま足すと、実際より一行少なく数えてしまいます。

ignore_index=Trueは、結果の行番号を連番にする指定です。受付idが一意になる指定でも、重複したタグを削除する指定でもありません。元idは意図的に複製されるため、どの受付から派生した明細かを別の列で追跡します。

expected_rows = sum(
    max(len(v), 1) if isinstance(v, list) else 1
    for v in df["tags"]
)
long = df.explode("tags", ignore_index=True)
assert expected_rows == len(long) == 10
assert long["id"].nunique() == 7
assert long["tags"].isna().sum() == 3
assert long["tags"].notna().sum() == 7
print(long[["id", "tags", "source_state"]])

欠損三行は、O2の空リスト、O3の元欠損、O7の要素内欠損です。非欠損七行の中には、まだリストに変換していないO6の文字列が一行含まれます。したがってnotnaの合計七を、そのまま正しいタグの件数として採用することはできません。

受付ごとに数える場合も、sizeとcountで結果が変わります。sizeはそのグループの行数、対象列のcountは欠損でない値の数を数えるためです。O2には展開後の行が一行ありますが、有効なタグ値はありません。O7は二行のうち一行が欠損なので、二つの数え方が一致しません。

sizes = long.groupby("id").size()
counts = long.groupby("id")["tags"].count()
assert sizes["O2"] == 1 and counts["O2"] == 0
assert sizes["O7"] == 2 and counts["O7"] == 1
assert sizes.sum() == 10 and counts.sum() == 7

集計表の見出しを単に「件数」にすると、どちらを数えたか読み手には伝わりません。「展開明細行数」「非欠損タグ行数」「有効タグを持つ元ID数」のように単位を具体的に書きます。O6の形式違いはcountに一件として含まれるので、countを使うだけで入力検証が済むわけでもありません。

この確認は、出力が十行なら業務的にも正しいという意味ではありません。今回想定した入力とexplodeの挙動が一致するかを確かめる第一段階です。次に値の種類と許可したタグを見て、集計対象にできる明細と確認へ戻す行を分けます。

巨大な表では、展開前にこの期待行数を計算することも役立ちます。数万行の受付でも、一行に大量の要素があれば明細が大きくなります。まず少量のサンプルで出力を確かめ、想定する行数と利用可能なメモリーに合わせて処理方法を決めましょう。

区切り文字列を、そのまま二件のタグと数えない

O6のred,blueは、redとblueの二要素を持つPythonのリストではありません。カンマを含む一つの文字列です。explodeはこの文字列を文字ごとにもカンマごとにも分割せず、そのまま一行へ残します。想定より行数が増えない場合は、セルの表示より実際の型を確認してください。

元CSVの仕様がカンマ区切りであると確認できれば、文字列を分割してから展開できます。しかし、正規のタグ名にカンマを含む場合や、引用符付きCSVの解釈が必要な場合には、単純なsplitで別の意味になります。データの形式を確かめず一律分割するのは避けます。

この教材では、O6を形式確認の対象として保留します。リストと文字列の両方を黙って受け入れる処理へ変更しないことで、受け取るデータの仕様が揃っていない事実を残します。「エラーにならなかった」と「入力として正しい」は別の確認です。

review = long[long["source_state"] == "scalar_needs_review"]
assert review[["id", "tags"]].to_dict("records") == [
    {"id": "O6", "tags": "red,blue"}
]
valid = long[
    long["source_state"].eq("list") & long["tags"].notna()
].copy()
assert len(valid) == 6
assert set(valid["tags"]) <= {"red", "blue", "green"}

文字列からの変換を認める場合も、元の値と変換規則を残し、空文字、末尾区切り、前後空白を追加のテストへ含めます。カンマが一つあるから二件という推測ではなく、分割後の空要素やタグ一覧との一致まで確かめると、実データへ移しやすくなります。

dropnaで消してよい行と、保存すべき記録を分ける

タグ別の件数を計算する明細では、欠損タグを対象外にすることがあります。一方、受付管理では選択なしや未記録の受付も残す必要があります。同じ表を両方の目的に使い、dropnaで行を削除すると、受付が存在していたことまで追えなくなる場合があります。

dropnaを使うなら、どの列の欠損を対象にするかを指定します。表全体のどこかに欠損がある行を落とすと、タグが正常でも別の任意項目が空という理由で消える可能性があります。今回はvalidという集計用の表を別に作り、元のdfとlongを保持しています。

O7は特に確認する価値があります。リストに不明な要素が一つ含まれる一方、blueという有効なタグも持っています。この受付全体を削除するとblueまで失われ、受付全体を正常とすると不明な要素を見逃します。元行の状態と子要素の状態を二段階で見ます。

元欠損のO3を空リストへ変えても、今回のexplode後はどちらも欠損一行です。しかし「不明」を「選択なし」に変えた事実は出力から分かりにくくなります。後の集計の都合だけで欠損を補う前に、その変更が業務上の意味を変えないかを確認してください。

欠損が集計結果へどう影響するかは、groupbyで欠損キーを確認する手順にも共通します。削除や補完を先に決めるより、どの状態が何件あり、計算から何件外したかを説明できることを優先しましょう。

展開後のタグ数・人数・合計を、目的に合わせて検算する

タグ六件と回答者四人を、同じ件数として報告しない

有効な明細は六行ですが、有効タグを一つ以上持つ元idは四件です。O1、O4、O5、O7が該当します。タグを二つ選んだ受付は明細で二行になるため、明細の行数を人数や受付数として読み替えると、元表より多い数を報告してしまいます。

また「redの出現回数」と「redを選んだ受付数」も違います。O5にはredが二つ入るため、出現回数では二回、受付単位では一件です。重複回答が誤入力なのか、同じ項目を複数回選べる仕組みなのかを、データの仕様に合わせて決める必要があります。

occurrences = valid["tags"].value_counts().to_dict()
assert occurrences == {"red": 3, "blue": 2, "green": 1}
assert valid["id"].nunique() == 4

per_person = valid.drop_duplicates(subset=["id", "tags"])
people = per_person["tags"].value_counts().to_dict()
assert people == {"red": 2, "blue": 2, "green": 1}
assert len(per_person) == 5
print(occurrences, people)

この重複除去では、同じ受付の同じタグだけを一つにしています。tagsだけで重複を消すと、O1とO5のredまで一つにまとまり、redを選んだ受付数を数えられません。重複を決めるキーが違えば、同じメソッドでも結果の意味が変わります。

割合を出すなら分母も書きます。redを選んだ二件を、有効タグを持つ四件で割るのか、受付全七件で割るのかでは割合が違います。未記録と形式違いを含む全体に対して、確認済みの対象だけの割合を全体の回答率として見せないよう注意します。

金額の合計は、タグ明細で足し直す前に単位を確かめる

元表のamountは受付全体の金額です。explodeは、タグ以外の列を各明細へ複製するので、O1の100は二行、O5の500も二行、O7の700も二行に現れます。出力が整った表に見えても、この列をそのまま足すと同じ受付の金額を繰り返して計上します。

assert df["amount"].sum() == 2800
assert long["amount"].sum() == 4100
assert valid["amount"].sum() == 2300
assert df.loc[df["id"].isin(valid["id"]), "amount"].sum() == 1700
print("受付全体2800 / 展開明細4100 / 有効受付1700")

四つの数はそれぞれ別の対象を足しています。2800は全受付、4100は複製された明細、2300は有効タグごとの明細、1700は有効タグがある受付を一回ずつ数えた金額です。数値が違うだけで不具合と決めず、どの単位を足したかを説明します。

タグ別の売上が必要なら、一件の金額を複数タグへどう割り当てるかを別途決めます。各タグへ全額を載せる方法、均等に割る方法、主タグだけへ載せる方法は、同じ指標ではありません。explodeが自動的に業務上の配賦方法まで決めるわけではありません。

受付単位で一度だけ数えたいときも、無条件にidでdrop_duplicatesしてよいとは限りません。同じidに異なる金額がある場合は、どの行が正しいかの確認が必要です。日付を使って重複行を選ぶ記事と同様に、残す行の根拠を決めてから集計します。

複数列の同時展開は、同じ行の長さを揃える

タグと個数のように一対一で対応する二つのリストを展開する場合は、同じ元行でそれぞれの要素数が対応している必要があります。二列を指定したから長さが違っても組み合わせを補ってくれる、という機能ではありません。欠けた値を何で補うかも自動では決まりません。

pair = pd.DataFrame({
    "id": ["P1"], "tag": [["red", "blue"]], "qty": [[2, 5]]
})
paired = pair.explode(["tag", "qty"], ignore_index=True)
assert paired[["tag", "qty"]].values.tolist() == [["red", 2], ["blue", 5]]

bad = pd.DataFrame({"tag": [["red", "blue"]], "qty": [[2]]})
try:
    bad.explode(["tag", "qty"])
except ValueError:
    print("同じ行の要素数が揃っていません")
else:
    raise AssertionError("長さ不一致の入力を再確認してください")

sequential = pair.explode("tag").explode("qty")
assert len(sequential) == 4
assert len(paired) == 2

二列を順番にexplodeすると、対応する二組ではなく四通りの組み合わせになる例にも注目してください。最初のタグ展開でqtyのリストも複製され、その各行でqtyが展開されるためです。一括指定と順次指定は、同じ書き方の違いではなく、出力の意味が異なります。

元データが「タグ一つにつき個数一つ」の仕様なら、一括指定した対応表を検算します。すべての組み合わせが必要な用途なら順次展開が目的に合う場合もあります。行数が多い方、エラーが出ない方を選ぶのではなく、欲しい対応関係を先に紙へ書き出しましょう。

リストの順番にも意味があるならsetへ変換しないでください。公式ではsetを展開する場合の順序は一定ではないと案内されています。重複を取りたいだけなら、順序や元の対応を失わない方法を検討し、前後のIDと要素を照合します。

原本・明細・集計を分けると、原因を追いやすくなる

今回保存する表は三種類です。七行の原本は受付と入力状態を保管し、十行の明細は展開結果と保留理由を保管し、集計表は有効タグや受付数を保管します。いずれも同じ一つの件数と呼ばず、対象と除外条件を表の名前や注記へ残します。

確認メモには「原本7、展開10、欠損タグ3、形式確認1、有効タグ6、有効受付4」と書けます。有効な六行と確認対象の四行を足すと十になり、展開した明細を説明できます。欠損三件をすべて同じ理由にせず、source_stateごとの内訳も付けると原本へ戻れます。

毎回の処理では、元idの集合が維持されたか、想定行数と一致したか、許可したタグ以外がないかを先に確認します。今回は固定の教材なのでassertに具体的な数を入れています。実データでは、当月の件数が前月と同じという前提に変えず、その月の入力から期待値を作ってください。

CSVへ保存して担当者へ渡す場合は、リスト列を含む原本の保存形式にも注意します。CSVにはPythonのlist型自体を保管できないので、読み戻すと文字列になる場合があります。CSVを保存して読み戻す確認と合わせて、保存前後の型とIDを照合しましょう。

保存用の仕様を決める練習では、リストをJSON文字列へ変換する列と、元の入力状態を表す列を用意する方法があります。空リストは[]、元の欠損はnull、欠損を含むリストは[null, "blue"]と表現でき、三つを別の状態として往復確認できます。受け手がその形式を読み取れることも必要なので、作成者側だけで保存を終えず、読み戻した表を渡す相手の環境で確かめます。

元データからリストを復元したら、O2の長さはゼロ、O3は未記録、O7の長さは二で一要素が不明、と元の定義へ戻れるかを確認します。セルに同じ見た目の文字があることより、処理前後で状態の意味が維持されることが重要です。新しいファイルを受け取った月も、この三件に相当するケースを確認用の例として残せます。

チェックに失敗した場合は、原本を残して、どの段階の出力から件数が変わったかを追います。受領した七行、状態を付けた七行、展開した十行、有効な六行を順に照合すれば、展開前の読み込み、展開、対象の選別のどこを調べるかを絞れます。最終集計の数字だけを修正して合わせるのではなく、元IDから数の違いを説明する練習にしてください。

独学で質問するときも、原本七行と期待する出力を示せると相談内容が明確になります。「行数がおかしい」だけでなく「空リストは元IDを残したい」「人数はタグ重複を一件にしたい」と説明します。PythonでCSV分析を始める練習の次の課題として、この教材のタグや件数を変えて検算できます。

よくある質問と、集計を確定する前の確認

質問と回答

Q. 空リストの行はexplodeで消えますか?
A.

消えず、対象列が欠損の行になります。この教材ではO2の元IDが残ります。選択なしの受付も管理したいなら、展開前の状態列を保持してください。

Q. NaNの行だけ消せば正しいタグ数になりますか?
A.

欠損以外に形式違いの文字列が混じる場合があります。教材の非欠損は七行ですが、リストから得た有効タグは六行です。型と許可した値も確認します。

Q. ignore_index=Trueで元IDの重複も消えますか?
A.

連番になるのは結果の行番号です。元ID列や同じタグの重複は残ります。受付単位とタグ明細単位を分けて検算してください。

Q. タグの件数と回答者数が違うのは不具合ですか?
A.

複数回答なら違っていても不自然ではありません。教材ではタグ出現六件、有効タグを持つ元ID四件です。同じ人の同じタグを一件にするかも、集計目的で決めます。

Q. 二列を一列ずつexplodeしても同じ結果ですか?
A.

同じとは限りません。二要素ずつの教材では同時展開は二行、順次展開は四行になります。対応する組が欲しいか、すべての組み合わせが欲しいかを区別してください。

Q. 金額列も展開後に足してよいですか?
A.

受付全体の金額ならタグごとに複製されます。元表2800に対し全明細では4100になる教材です。受付単位で数えるか、タグへ配賦するかを先に決めてください。

最後に原本のO2・O3・O6・O7へ戻り、空リスト、元欠損、文字列、要素内欠損がそれぞれどう残ったかを確認します。正常なタグだけの例で終えず、この四つを説明できれば、欠損が増えたという見た目から処理の内訳へ進めます。

集計を渡すときは、六件という数字だけではなく「有効タグの出現六件。元ID四件。空リスト一件、元欠損一件、要素内欠損一行、形式確認一件」と添えます。処理が最後まで動くことと、読み手が何を数えたか分かることを両方の完成条件にしましょう。

pandasの文法だけでなく、読み込みから前処理、集計の検算まで順に学びたい人は、Python Winnerのデータ分析系の講座も比較先になります。公式ではPythonの基礎・応用を含むコースとマンツーマンレッスンを案内しています。今回の空リストや集計単位の相談が受講範囲に入るか、前提知識、必要な学習時間、受講総額を無料カウンセリング・受講相談で確認してください。七行の例で解決した場合は、その確認方法を手元の別教材へ応用する選択もあります。(公式情報確認日:2026年10月28日、Python Winner公式)。

広告:【PythonWinner】