pandas dropna()の使い方|欠損値の削除・subset・howを解説

pandasでCSVやExcelを読み込んだあと、dropna()を実行したら、予想以上に行が減って困ったことはありませんか。

dropna()は欠損値の意味を判断してくれるメソッドではありません。引数を指定しない場合は、どれか1列でも欠損している行を削除します。そのため、売上分析には必要な行が、任意項目の未入力だけで消えることがあります。

先に結論を示すと、次の順番で使うのが安全です。

  1. 欠損値の場所・件数・対象行を確認する
  2. 分析に必須の列を決める
  3. 通常はsubsetで削除の判定列を絞る
  4. 削除後の行数・欠損数・残ったIDを検算する

この記事では、注文データを使い、Google Colabで欠損行の確認から削除、検算、次の集計までを順番に実行します。最後には、「なぜこの行を削除したのか」を自分のデータで説明できる状態を目指します。

この記事でわかること

  • dropna()の既定動作と、必要な行まで消える理由
  • subsethowthreshaxisの選び方
  • howthreshを同時に指定できない理由
  • 削除前後の行数・欠損数・注文IDを確認する方法
  • 元のDataFrameへ反映する方法とignore_index=Trueの注意点
  • 空文字列が削除されないときの確認方法
  • dropna()fillna()の使い分け

dropna()は、データを読み込んで状態を確認したあと、集計や可視化へ進む前に行う前処理の一つです。前処理全体の順番を先に知りたい場合は、Pandasデータ前処理の基本も参考にしてください。

先に結論:全列ではなく「分析に必須の列」で削除を判断する

引数なしのdf.dropna()は、1つでも欠損値がある行を削除します。しかし、任意入力の「地域」が空でも、金額が分かれば売上集計に使える場合があります。

最初に、目的に合う指定を選びましょう。

目的 選ぶ指定 削除される条件 残り得る欠損 注意点
どの列にも欠損がない行だけ残す 引数なし 1列でも欠損 なし 必要な行まで消えやすい
必須列が欠損した行だけ削除 subset 指定列が欠損 指定外の列 実務ではまず検討する
全項目が欠損した行だけ削除 how='all' すべての列が欠損 一部の欠損 空の追加行などに使う
有効な値が少なすぎる行を削除 thresh 非欠損値が指定数未満 条件内の欠損 「欠損数」ではなく「非欠損数」
欠損を含む列を削除 axis=1 列に欠損がある 残った列にはなし 列全体が消えるため慎重に使う

欠損行を残す必要がある場合は、削除せずfillna()で補完する選択肢があります。削除と補完の違いは後半で比較します。

注文データを用意して、削除前の状態を確認する

実行目的

注文ID・顧客ID・商品・金額・地域を持つDataFrameを作り、どの列と行に欠損があるか確認します。金額は売上集計の必須列、地域と顧客IDは今回の集計では任意列とします。

np.nanNoneはpandasが欠損値として認識します。一方、A106の地域に入れた空文字列''は欠損値として認識されません。この違いも後で確認します。

import pandas as pd
import numpy as np

df = pd.DataFrame({
    "order_id": ["A101", "A102", "A103", "A104", "A105", "A106", "A107", "A108"],
    "customer_id": ["C01", "C02", "C03", "C04", "C05", "C06", None, "C08"],
    "product": ["PC", "Chair", "PC", "Chair", "Monitor", "PC", "Chair", "Monitor"],
    "amount": [12000, 8000, np.nan, 5000, 15000, 9000, 7000, np.nan],
    "region": ["関東", "関西", "関東", None, "中部", "", "関西", "関東"],
})

print(df.to_string(index=True))
order_id customer_id product amount region
0 A101 C01 PC 12000.0 関東
1 A102 C02 Chair 8000.0 関西
2 A103 C03 PC NaN 関東
3 A104 C04 Chair 5000.0 None
4 A105 C05 Monitor 15000.0 中部
5 A106 C06 PC 9000.0
6 A107 None Chair 7000.0 関西
7 A108 C08 Monitor NaN 関東

出力では、A103・A108のamount、A104のregion、A107のcustomer_idが欠損しています。A106のregionは見た目が空でも空文字列なので、NaNとは表示されません。

次に、列ごとの欠損数と、欠損を1つでも含む行を確認します。削除前に対象行を表示しておくと、必要な注文まで消していないか判断できます。欠損値の確認方法を詳しく知りたい場合は、pandasで欠損値を確認する方法を参照してください。

print("列ごとの欠損数")
print(df.isna().sum().to_string())

print("\n欠損を1つでも含む行")
missing_rows = df[df.isna().any(axis=1)]
print(missing_rows.to_string(index=False))
欠損数
order_id 0
customer_id 1
product 0
amount 2
region 1
order_id customer_id product amount region
2 A103 C03 PC NaN 関東
3 A104 C04 Chair 5000.0 None
6 A107 None Chair 7000.0 関西
7 A108 C08 Monitor NaN 関東

注目するのは、amountの欠損が2件であることと、欠損を含む注文IDがA103・A104・A107・A108であることです。

ただし、4件すべてを削除する必要はありません。今回の売上集計に必須なのはamountなので、削除候補はA103・A108の2件です。A104は地域が未入力、A107は顧客IDが未入力ですが、金額と商品があるため商品別売上には使えます。

引数なしのdropna()では必要な行まで削除される

実行目的

まず既定動作を確認します。引数なしでは、すべての列が判定対象です。

df_default = df.dropna()

print(df_default.to_string(index=False))
print(f"\n処理前: {len(df)}行")
print(f"処理後: {len(df_default)}行")
print("削除された注文ID:", sorted(set(df["order_id"]) - set(df_default["order_id"])))
order_id customer_id product amount region
0 A101 C01 PC 12000.0 関東
1 A102 C02 Chair 8000.0 関西
4 A105 C05 Monitor 15000.0 中部
5 A106 C06 PC 9000.0
処理前: 8行
処理後: 4行
削除された注文ID: ['A103', 'A104', 'A107', 'A108']

処理後は8行から4行に減り、A103・A104・A107・A108が削除されました。A104とA107は金額があるため、商品別売上には利用できる行です。それでも消えたのは、dropna()が分析目的を知らず、全列の欠損を同じように判定するからです。

また、地域が空文字列のA106は残っています。コードが正常終了し、欠損行を削除できても、削除条件が分析目的に合っているとは限りません。

subsetで削除の基準にする列を絞る

実行目的

商品別売上に必須のamountだけを判定対象にします。subsetへ指定していない列に欠損があっても、その行は削除されません。

df_amount = df.dropna(subset=["amount"])

print(df_amount.to_string(index=False))
print(f"\n処理前: {len(df)}行")
print(f"処理後: {len(df_amount)}行")
print("削除された注文ID:", sorted(set(df["order_id"]) - set(df_amount["order_id"])))
order_id customer_id product amount region
0 A101 C01 PC 12000.0 関東
1 A102 C02 Chair 8000.0 関西
3 A104 C04 Chair 5000.0 None
4 A105 C05 Monitor 15000.0 中部
5 A106 C06 PC 9000.0
6 A107 None Chair 7000.0 関西
処理前: 8行
処理後: 6行
削除された注文ID: ['A103', 'A108']

今度はA103・A108だけが削除され、8行から6行になりました。A104のregionとA107のcustomer_idには欠損が残っていますが、これは失敗ではありません。subset=['amount']は、金額が欠損した行だけを削除する指定だからです。

複数の必須列を基準にする場合は、subsetへ列名を複数指定します。たとえば、商品名と金額の両方が必要ならproductamountを判定対象にします。列名を間違えるとKeyErrorになるため、自分のデータでは実際の列名を確認してください。

削除前後は行数・欠損数・残ったIDを検算する

dropna()の結果は、DataFrameを眺めるだけでなく、同じ基準で数値確認します。

確認項目 処理前 subset=['amount']で処理後 読み方
行数 8 6 金額欠損2件だけ減った
amountの欠損数 2 0 必須列の欠損は解消した
残った注文ID A101〜A108 A101・A102・A104・A105・A106・A107 A103・A108だけ削除した
regionの欠損数 1 1 subset外なので残ってよい
customer_idの欠損数 1 1 今回の集計では任意列

次のコードで、削除件数と必須列の欠損数を機械的に確認します。

removed_ids = sorted(set(df["order_id"]) - set(df_amount["order_id"]))

check = pd.Series({
    "処理前の行数": len(df),
    "処理後の行数": len(df_amount),
    "削除行数": len(df) - len(df_amount),
    "処理後のamount欠損数": int(df_amount["amount"].isna().sum()),
    "処理後のregion欠損数": int(df_amount["region"].isna().sum()),
})

print(check.to_string())
print("削除された注文ID:", removed_ids)
処理前の行数 8
処理後の行数 6
削除行数 2
処理後のamount欠損数 0
処理後のregion欠損数 1
削除された注文ID: ['A103', 'A108']

「削除行数が2」「処理後のamount欠損数が0」「削除IDがA103・A108」の3点が一致しています。regionの欠損が1件残るのは、今回の目的では許容した結果です。

実データでは、削除候補が特定の地域・顧客層・期間へ偏っていないかも確認してください。偏った欠損行をまとめて削除すると、コードは正しくても集計結果が実態からずれる可能性があります。

how・thresh・axisは目的が合う場合だけ使う

how=’all’:すべて欠損した行だけを削除する

how='all'は、全項目が欠損している行だけを削除します。CSVの末尾に空行相当のレコードが入った場合などに使えます。一部に値がある行は残ります。

empty_row = pd.DataFrame([{
    "order_id": None,
    "customer_id": None,
    "product": None,
    "amount": np.nan,
    "region": None,
}])

df_with_empty_row = pd.concat([df, empty_row], ignore_index=True)
df_how_all = df_with_empty_row.dropna(how="all")

print(f"処理前: {len(df_with_empty_row)}行")
print(f"処理後: {len(df_how_all)}行")
print(df_how_all.tail(3).to_string(index=False))
処理前: 9行
処理後: 8行
order_id customer_id product amount region
5 A106 C06 PC 9000.0
6 A107 None Chair 7000.0 関西
7 A108 C08 Monitor NaN 関東

9行目の全項目が欠損した行だけが削除され、元の8行は残ります。how='any'は欠損が1つでもあれば削除、how='all'はすべて欠損した場合だけ削除、と読み分けます。

thresh:非欠損値が一定数以上ある行を残す

thresh=4は「欠損が4個まで」という意味ではありません。欠損していない値が4個以上ある行を残す指定です。5列のうち2列しか値がない疎な行を追加して確認します。

sparse_row = pd.DataFrame([{
    "order_id": "A109",
    "customer_id": None,
    "product": "PC",
    "amount": np.nan,
    "region": None,
}])

df_with_sparse_row = pd.concat([df, sparse_row], ignore_index=True)
df_thresh = df_with_sparse_row.dropna(thresh=4)

print("A109の非欠損値数:", int(df_with_sparse_row.iloc[-1].notna().sum()))
print("thresh=4の処理後にA109があるか:", "A109" in df_thresh["order_id"].values)
print("残った注文ID:", df_thresh["order_id"].tolist())
A109の非欠損値数: 2
thresh=4の処理後にA109があるか: False
残った注文ID: ['A101', 'A102', 'A103', 'A104', 'A105', 'A106', 'A107', 'A108']

A109の非欠損値はorder_idproductの2個だけなので、thresh=4では削除されます。

howthreshは同時に指定できません。 「欠損が1つでもあるか・すべて欠損か」で選ぶならhow、「有効な値が何個以上あるか」で選ぶならthreshを使い、どちらか一方だけを指定してください。

axis=1:欠損を含む列を削除する

axis=1は行ではなく列を削除します。今回のデータに引数なしの条件を適用すると、欠損が1つでもあるcustomer_idamountregionが列ごと消えます。

df_drop_columns = df.dropna(axis=1)

print("処理前の列:", df.columns.tolist())
print("処理後の列:", df_drop_columns.columns.tolist())
print(df_drop_columns.head(3).to_string(index=False))
処理前の列: ['order_id', 'customer_id', 'product', 'amount', 'region']
処理後の列: ['order_id', 'product']
order_id product
0 A101 PC
1 A102 Chair
2 A103 PC

残るのは欠損のないorder_idproductだけです。金額列まで失うため、今回の売上分析には使えません。列削除は、列自体が不要であると確認できた場合に限定しましょう。

元のDataFrameへの反映とignore_indexの選び方

dropna()は既定では新しいDataFrameを返し、元のdfを変更しません。処理前後を比べやすくするには、まずdf_amountのような別変数へ保存する方法が安全です。同じ変数へ反映するなら、df = df.dropna(...)のように再代入します。

inplace=Trueでも元のDataFrameを直接変更できますが、戻り値はNoneです。初心者のうちは、処理前後を確認できるように別変数または再代入を使うと、意図しない削除へ気づきやすくなります。

削除後のインデックスを0から振り直したい場合は、pandas 2.0.0以降ではignore_index=Trueを指定できます。古い環境でこの引数が使えない場合は、dropna()の結果へreset_index(drop=True)を続けます。

df_amount_reset = df.dropna(subset=["amount"], ignore_index=True)

print(df_amount_reset.to_string(index=True))
order_id customer_id product amount region
0 A101 C01 PC 12000.0 関東
1 A102 C02 Chair 8000.0 関西
2 A104 C04 Chair 5000.0 None
3 A105 C05 Monitor 15000.0 中部
4 A106 C06 PC 9000.0
5 A107 None Chair 7000.0 関西

インデックスは0〜5の連番になりました。一方、元の行ラベルは失われます。注文の識別にはorder_idを使い、インデックスを注文IDだと誤解しないでください。

元のインデックスを残しておきたい場合はignore_index=Trueを指定しません。インデックスを列として残したい、列名を付けて整理したい場合は、pandas reset_index()の使い方も確認してください。

空文字列がdropna()で消えないときは表記を確認する

起きる現象

欠損行を削除したのに、A106のregionのような空欄が残ります。

原因

空文字列''は文字列であり、pandasの欠損値ではありません。前後に空白だけが入った文字列も、そのままでは欠損値として数えられません。

確認方法と修正方法

文字列の前後空白を除き、空文字列を欠損値へ統一してからdropna()を使います。元のDataFrameを壊さないようにコピーして確認します。

df_normalized = df.copy()
df_normalized["region"] = df_normalized["region"].str.strip().replace("", pd.NA)

print("修正前のregion欠損数:", int(df["region"].isna().sum()))
print("修正後のregion欠損数:", int(df_normalized["region"].isna().sum()))
print("修正後にregionが欠損している注文ID:",
      df_normalized.loc[df_normalized["region"].isna(), "order_id"].tolist())
修正前のregion欠損数: 1
修正後のregion欠損数: 2
修正後にregionが欠損している注文ID: ['A104', 'A106']

修正前はA104の1件だけでしたが、修正後はA104とA106の2件が欠損として確認できます。その後に地域を必須列として削除するか、「不明」として補完するかを分析目的から選びます。

実データでは空文字列のほか、'-''不明''N/A'など独自の欠損表記が混ざることがあります。それらを機械的に欠損へ変換する前に、業務上の意味を確認してください。前後空白の削除方法はpandas str.strip()の使い方で詳しく確認できます。

dropna()とfillna()は「行を失ってよいか」で選ぶ

判断すること dropna()を選ぶ場面 fillna()を選ぶ場面
行を失ってよいか 必須値がなく分析に使えない 行自体は残す必要がある
欠損の意味 記録不成立・対象外と判断できる 未回答・未測定など意味がある
集計への影響 母数や対象件数が減る 補完値によって平均などが変わる
実行後の確認 削除件数・残存ID・偏り 補完件数・補完値・分布の変化

今回のamount欠損は、売上金額を集計できないため削除対象としました。一方、regionが未入力でも全国の売上合計には使えるため、行を残しました。

0や平均値で埋めると行数は保てますが、「売上不明」を「売上0」とみなすなど、意味が変わる可能性があります。補完方法の選び方は、pandas fillna()の使い方で確認してください。

よくある失敗を現象から確認・修正する

行が想定以上に減った

  • 原因:引数なしで全列を判定対象にした可能性がある
  • 確認方法:列別の欠損数と、df[df.isna().any(axis=1)]で削除候補を確認する
  • 修正方法:分析に必須の列だけをsubsetへ指定する
  • 修正後の確認:削除行数、必須列の欠損数、残ったIDが想定と一致するか確認する

dropna()を書いたのに元のdfが変わらない

  • 原因:既定では新しいDataFrameを返し、元のdfは変更しない
  • 確認方法:結果を変数へ代入しているか確認する
  • 修正方法df_clean = df.dropna(...)またはdf = df.dropna(...)の形で保存する
  • 修正後の確認:保存先のDataFrameの行数と欠損数を確認する

subsetでKeyErrorが出た

  • 原因:指定した列名が存在しない、または列名に前後空白・表記違いがある
  • 確認方法df.columnsで実際の列名を確認する
  • 修正方法:存在する列名へ直し、必要なら列名の前後空白を除く
  • 修正後の確認:指定列だけを基準に、想定した行が削除されたか確認する

howとthreshを一緒に指定してエラーになった

  • 原因:現在のpandasではhowthreshを同時に使えない
  • 確認方法:「欠損の有無」と「非欠損値数」のどちらで判定したいか確認する
  • 修正方法:目的に合う片方だけを指定する
  • 修正後の確認:残った行の非欠損値数または欠損状態を確認する

空欄が残った

  • 原因:空文字列や空白だけの文字列は欠損値として認識されない
  • 確認方法:値の表記と文字列長を確認する
  • 修正方法:前後空白を除き、空文字列をpd.NAへ統一する
  • 修正後の確認isna().sum()と対象IDを再確認する

自分のDataFrameへ置き換える前のチェックリスト

  • df.columnssubsetへ指定する列名を確認したか
  • isna().sum()で列ごとの欠損数を確認したか
  • 空文字列、空白、-不明など独自表記の意味を確認したか
  • 分析に必須の列と、欠損していても行を残せる列を分けたか
  • 削除候補のIDを処理前に確認したか
  • 削除候補が特定の地域・顧客・期間へ偏っていないか
  • 処理後の行数、必須列の欠損数、残ったIDを確認したか
  • 元のインデックスを残すか、ignore_index=Trueで連番にするか決めたか
  • 元のDataFrameを残す必要がある場合、別変数へ保存したか

欠損値を削除したデータを集計へつなげる

最後に、amountの欠損を削除したdf_amountを商品別に集計します。削除後も金額があるA104・A107を残したため、これらの売上も集計へ反映されます。

実行目的

商品別の注文件数と売上合計を求め、前処理後のデータが次の分析へ使えることを確認します。

sales_by_product = (
    df_amount.groupby("product", as_index=False)
    .agg(注文件数=("order_id", "count"), 売上合計=("amount", "sum"))
    .sort_values("product")
    .reset_index(drop=True)
)

print(sales_by_product.to_string(index=False))
print("\n集計対象件数:", int(sales_by_product["注文件数"].sum()))
print("売上合計:", int(sales_by_product["売上合計"].sum()))
product 注文件数 売上合計
0 Chair 3 20000.0
1 Monitor 1 15000.0
2 PC 2 21000.0
集計対象件数: 6
売上合計: 56000

集計対象は6件、売上合計は56,000です。商品別ではPCが21,000、Chairが20,000、Monitorが15,000になります。

引数なしで欠損行をすべて削除していた場合、地域欠損のA104と顧客ID欠損のA107も集計から外れ、売上を12,000少なく数えてしまいます。どの列を基準に削除するかが、次の分析結果を変えることを確認してください。

複数列・複数集計の方法は、pandas groupby・aggの使い方で詳しく解説しています。

まとめ

  • dropna()は既定では、欠損値を1つでも含む行を削除する
  • 削除前に欠損の場所・意味・対象IDを確認する
  • 通常は分析に必須の列だけをsubsetへ指定する
  • how='all'はすべて欠損した行、threshは非欠損値数を基準にする
  • howthreshは同時に指定できない
  • axis=1は列全体を削除するため、必要な列まで失わないか確認する
  • 空文字列は欠損値ではないため、表記を整えてから処理する
  • 削除後は行数・欠損数・残ったID・集計結果を検算する
  • 行を残す必要がある場合は、dropna()ではなくfillna()を検討する

dropna()は「欠損があるから消す」ための命令ではありません。何を分析するかを決め、必要な列を基準に削除し、結果を再確認してから集計や可視化へ進みましょう。

関連記事

▲ ページトップへ戻る

howとthreshは同時に指定できますか?

できません。欠損が1つでもあるか・すべて欠損かで判断するならhow、非欠損値の最低個数で判断するならthreshを選び、片方だけを指定します。

空文字列がdropna()で消えないのはなぜですか?

空文字列''は文字列であり、欠損値ではないためです。業務上の意味を確認してから前後空白を除き、空文字列をpd.NAなどへ統一すると、dropna()の判定対象になります。

dropna()のあとにインデックスを振り直すにはどうしますか?

現行のpandasではignore_index=Trueを指定すると、結果のインデックスを0からの連番にできます。ただし元の行ラベルは失われるため、注文IDなどの識別列を残してください。より細かく整理する場合はreset_index()を使います。

dropna()とfillna()はどちらを選べばよいですか?

必須値がなく、その行を分析に使えないならdropna()を検討します。行を残す必要があるならfillna()を検討します。ただし、0や平均値による補完でデータの意味や分布が変わるため、補完値の根拠も確認してください。

コメント

タイトルとURLをコピーしました