pandasデータ前処理:NaN行を効率的に削除するための徹底ガイド
データ分析の世界において、欠損値は避けて通れない問題です。現実世界のデータは、収集プロセスでのエラー、入力ミス、システムの不具合、あるいは単に情報が存在しないことなど、さまざまな理由で不完全になりがちです。これらの欠損値は、データの品質を低下させ、分析結果に偏りをもたらし、機械学習モデルの性能を著しく劣化させる可能性があります。
データ分析ライブラリとして広く利用されているpandasは、欠損値を扱うための強力なツールを提供しています。特に、欠損値をNaN
(Not a Number)として表現し、これらのNaN
を含むデータを検出、処理、あるいは削除するための多様なメソッドを備えています。
本記事では、pandas DataFrameにおける欠損値NaN
を含む「行」を削除することに焦点を当て、その理由、検出方法、そしてさまざまな削除方法について、詳細かつ包括的に解説します。特に、効率的な削除のための考慮事項や、具体的なコード例を豊富に紹介し、データ前処理の実践力を高めることを目的とします。約5000語を目標に、各トピックを掘り下げていきます。
1. はじめに:なぜ欠損値処理、特にNaN行の削除が重要なのか
データ分析プロジェクトの最初のステップの1つは、必ずデータクレンジング(データの清掃)です。データクレンジングには、重複の削除、型の変換、外れ値の処理、そして最も重要な作業の1つとして欠損値の処理が含まれます。
欠損値がデータセットに存在すると、以下のような問題が発生する可能性があります。
- 統計量の計算が不正確になる: 平均値、中央値、標準偏差などの基本的な統計量は、欠損値があると正しく計算できない場合があります。例えば、単純な平均計算では欠損値を含むデータポイントを無視するか、エラーを引き起こします。
- 可視化が歪む: 欠損値を含むデータをプロットすると、意図しない結果になったり、エラーが発生したりすることがあります。
- 機械学習モデルが処理できない: 多くの機械学習アルゴリズムは、入力データに欠損値が含まれていると正しく動作しません。モデルによっては訓練や予測の段階でエラーを発生させたり、性能が著しく低下したりします。
- 分析結果の信頼性が低下する: 欠損値が多い、あるいは欠損値の分布に偏りがある場合、そのデータに基づいて得られた分析結果や結論の信頼性が損なわれます。
欠損値の処理方法には、大きく分けて「削除 (Dropping)」と「補完 (Imputation)」があります。削除は、欠損値を含むデータポイント(行または列)を取り除く方法です。補完は、欠損値を推定値や代替値(平均値、中央値、予測値など)で埋める方法です。
本記事で焦点を当てる「NaN行の削除」は、欠損値処理の最もシンプルで直接的な方法の一つです。特に、欠損値がごく一部の行に限定されている場合や、欠損値が存在する行が分析にとって重要ではないと判断できる場合に有効な手段となり得ます。しかし、安易な削除は貴重な情報を失うことにつながるため、その影響を十分に理解した上で行う必要があります。
2. NaN(Not a Number)とは:pandasにおける欠損値の表現
pandasでは、欠損値を表現するために主にNaN
(Not a Number)を使用します。これはNumPyライブラリから導入された特殊な浮動小数点値です。したがって、整数型の列にNaNが含まれると、その列全体のデータ型が浮動小数点型(float64
など)に自動的に変換されるという重要な特性があります。pandas 0.24以降では、NaNをネイティブにサポートする新しいデータ型(例: Nullable Integer Int64
)も導入されていますが、広く使用されているのは依然としてNumPy由来のNaN
です。
NaN
は、数値計算において「未定義」や「表現不能」を示すために使われます。例えば、0による除算の結果などがNaN
になることがあります。pandasにおいては、データが存在しない、取得できなかった、あるいは無効であるといった状況を表すために利用されます。
NaN
の興味深い特性として、「いかなる値(NaN自身を含む)とも等しくない」という性質があります。つまり、NaN == NaN
は常に False
と評価されます。このため、欠損値を検出する際には、単純な等号比較ではなく、専用のメソッドを使用する必要があります。
3. なぜNaN行を削除するのか(そしてその影響)
先述の通り、NaNを含む行の削除は欠損値処理の基本的な手段です。削除を選択する主な理由は以下の通りです。
- シンプルさ: 欠損値補完に比べて実装が非常に簡単です。
- 分析の簡素化: 多くの統計手法や機械学習アルゴリズムは、欠損値がないデータセットを前提としています。NaN行を削除することで、これらの手法やアルゴリズムをそのまま適用できるようになります。
- 特定のアルゴリズムの要件: 一部の機械学習モデル(例: 線形回帰、サポートベクターマシンなど)は、入力データに欠損値が含まれることを許容しません。これらのモデルを使用するためには、NaNを含むデータを処理する必要があります。
しかし、NaN行の削除には以下のようなデメリットや注意点があります。
- 情報損失: 行を削除するということは、その行に含まれる他の列の有効なデータも一緒に捨ててしまうということです。これは、貴重な分析情報を失うことにつながります。
- データの偏り: 欠損値の発生がランダムでない(例えば、特定の属性を持つデータポイントに欠損値が多い)場合、NaN行の削除によってデータセットの分布に偏りが生じ、分析結果が歪む可能性があります。これを非ランダム欠損 (Non-Random Missing, NMAR) と呼びます。欠損が完全にランダムな場合(完全ランダム欠損, Missing Completely at Random, MCAR)や、他の既知の変数によって欠損確率が説明できる場合(ランダム欠損, Missing at Random, MAR)に比べて、NMARの場合は削除によるバイアスの影響が大きくなります。
- データセットサイズの縮小: 多数の行に欠損値が含まれる場合、削除によってデータセットのサイズが大幅に縮小し、統計的な検出力(分析が真の関係性を検出できる確率)が低下する可能性があります。
したがって、NaN行の削除を行う前には、まず欠損値の量やパターンをしっかりと分析し、削除が分析目的や後続の処理に与える影響を慎重に評価することが不可欠です。
4. pandasにおけるNaNの検出方法
NaNを含む行を削除する前に、データセット内にどれだけ、どのような形でNaNが存在するのかを把握することが重要です。pandasはNaNを検出するためのいくつかの便利なメソッドを提供しています。
4.1. isnull()
/ isna()
メソッド
最も基本的な欠損値検出メソッドは isnull()
です。エイリアスとして isna()
も用意されており、機能は全く同じです。これらのメソッドは、DataFrameの各要素がNaNであるかどうかに応じて、ブール値 (True
またはFalse
) を要素とする同じ形状のDataFrameを返します。
“`python
import pandas as pd
import numpy as np
サンプルDataFrameの作成
data = {‘A’: [1, 2, np.nan, 4, 5],
‘B’: [6, np.nan, 8, 9, 10],
‘C’: [11, 12, 13, np.nan, 15],
‘D’: [np.nan, np.nan, np.nan, np.nan, np.nan],
‘E’: [16, 17, 18, 19, 20]}
df = pd.DataFrame(data)
print(“元のDataFrame:”)
print(df)
print(“\nisnull() または isna() の結果:”)
print(df.isnull())
print(df.isna()) # isna()も同じ結果
“`
出力:
“`
元のDataFrame:
A B C D E
0 1.0 6.0 11.0 NaN 16
1 2.0 NaN 12.0 NaN 17
2 NaN 8.0 13.0 NaN 18
3 4.0 9.0 NaN NaN 19
4 5.0 10.0 15.0 NaN 20
isnull() または isna() の結果:
A B C D E
0 False False False True False
1 False True False True False
2 True False False True False
3 False False True True False
4 False False False True False
“`
4.2. notnull()
/ notna()
メソッド
isnull()
/ isna()
の逆の操作を行うのが notnull()
/ notna()
メソッドです。これらは要素がNaNでない場合にTrue
を返します。
“`python
print(“\nnotnull() または notna() の結果:”)
print(df.notnull())
print(df.notna()) # notna()も同じ結果
“`
出力:
notnull() または notna() の結果:
A B C D E
0 True True True False True
1 True False True False True
2 False True True False True
3 True True False False True
4 True True True False True
4.3. NaNの合計数をカウントする
isnull()
または isna()
の結果はブール値のDataFrameなので、これに対してNumPyやpandasの集計メソッドを適用することで、NaNの数を知ることができます。
- 列ごとのNaN数:
isnull().sum()
を使用します。True
は数値計算では1として扱われるため、列ごとにTrue
(NaN)の合計数が得られます。
python
print("\n列ごとのNaN数:")
print(df.isnull().sum())
出力:
列ごとのNaN数:
A 1
B 1
C 1
D 5
E 0
dtype: int64
- データフレーム全体のNaN数:
isnull().sum().sum()
のように、sum()
を二重に適用します。最初のsum()
で列ごとの合計が得られ、2番目のsum()
でその合計をさらに合計します。
python
print("\nデータフレーム全体のNaN数:")
print(df.isnull().sum().sum())
出力:
データフレーム全体のNaN数:
9
4.4. info()
メソッドによる欠損値の把握
info()
メソッドは、DataFrameの各列のデータ型、非欠損値の数、メモリ使用量などをまとめて表示してくれます。非欠損値の数を見ることで、欠損値の存在とその数をおおまかに把握できます。
python
print("\ndf.info():")
df.info()
出力:
“`
RangeIndex: 5 entries, 0 to 4
Data columns (total 5 columns):
# Column Non-Null Count Dtype
0 A 4 non-null float64
1 B 4 non-null float64
2 C 4 non-null float64
3 D 0 non-null float64
4 E 5 non-null int64
dtypes: float64(4), int64(1)
memory usage: 328.0 bytes
“`
info()
の出力にある “Non-Null Count” は非欠損値の数を示しています。データの行数(RangeIndex: 5 entries)と比較することで、欠損値の数が分かります。例えば、列Aは5行中4行が非欠損なので、1つの欠損値があることがわかります。列Dは5行中0行が非欠損なので、5つ全てが欠損値であることがわかります。
これらの検出方法を駆使することで、データセットの欠損値の状況を詳細に把握し、どのような欠損値処理が適切かを判断するための基礎情報を得ることができます。
5. NaN行の削除方法:dropna()
メソッドの活用
pandasでNaNを含む行を削除する最も一般的で効率的な方法は、DataFrameやSeriesオブジェクトの dropna()
メソッドを使用することです。このメソッドは非常に柔軟で、さまざまな条件に基づいて行(または列)を削除できます。
dropna()
メソッドの主要なパラメータは以下の通りです。
axis
: どの軸に対して削除を行うかを指定します。0
または'index'
: 行を削除します(デフォルト)。1
または'columns'
: 列を削除します。
how
: どの条件で行(または列)を削除するかを指定します。'any'
: 1つでもNaNが含まれている場合に削除します(デフォルト)。'all'
: 全ての要素がNaNである場合にのみ削除します。
thresh
: 非NaN値が指定した数以上ある行(または列)のみを残します。このパラメータを指定すると、how
パラメータは無視されます。subset
: 特定の列(またはインデックス)に限定してNaNチェックを行い、削除の対象とします。axis
が0(行削除)の場合は列名のリストを、axis
が1(列削除)の場合はインデックス名のリストを指定します。inplace
: 削除を実行した結果を元のDataFrameに反映させるか(True
)、新しいDataFrameとして返すか(False
)を指定します。デフォルトはFalse
です。
以下に、それぞれのパラメータの使い方と挙動を詳しく見ていきます。
サンプルDataFrameを再度使用します。
“`python
import pandas as pd
import numpy as np
data = {‘A’: [1, 2, np.nan, 4, 5],
‘B’: [6, np.nan, 8, 9, 10],
‘C’: [11, 12, 13, np.nan, 15],
‘D’: [np.nan, np.nan, np.nan, np.nan, np.nan],
‘E’: [16, 17, 18, 19, 20]}
df = pd.DataFrame(data)
print(“元のDataFrame:”)
print(df)
“`
元のDataFrame:
A B C D E
0 1.0 6.0 11.0 NaN 16
1 2.0 NaN 12.0 NaN 17
2 NaN 8.0 13.0 NaN 18
3 4.0 9.0 NaN NaN 19
4 5.0 10.0 15.0 NaN 20
5.1. 基本的な削除 (axis='index'
, how='any'
)
デフォルト設定です。行 (axis=0
) に対して、1つでも (how='any'
) NaNが含まれている行を削除します。
“`python
df_dropped_any = df.dropna(axis=’index’, how=’any’)
df_dropped_any = df.dropna() # axis=0, how=’any’はデフォルトなので省略可能
print(“\ndropna(how=’any’) でNaNを含む行を削除:”)
print(df_dropped_any)
“`
出力:
dropna(how='any') でNaNを含む行を削除:
A B C D E
4 5.0 10.0 15.0 NaN 20
元のDataFrameでは、インデックス0, 1, 2, 3の行はそれぞれ列C, B, A, CにNaNを含んでおり、列Dは全ての行でNaNを含んでいます。インデックス4の行は列D以外にはNaNがありません。how='any'
は1つでもNaNがあれば削除するため、インデックス4以外の全ての行が削除されました。列Dは全ての行でNaNを含んでいるため、どの行も列DにおいてはNaNを含んでしまいます。結果として、列D以外の全ての列でNaNを含まない唯一の行であるインデックス4のみが残りました。
5.2. 全ての要素がNaNの行を削除 (how='all'
)
行 (axis=0
) に対して、全ての (how='all'
) 要素がNaNである場合にのみその行を削除します。
“`python
df_dropped_all = df.dropna(axis=’index’, how=’all’)
print(“\ndropna(how=’all’) で全ての要素がNaNの行を削除:”)
print(df_dropped_all)
“`
出力:
dropna(how='all') で全ての要素がNaNの行を削除:
A B C D E
0 1.0 6.0 11.0 NaN 16
1 2.0 NaN 12.0 NaN 17
2 NaN 8.0 13.0 NaN 18
3 4.0 9.0 NaN NaN 19
4 5.0 10.0 15.0 NaN 20
このサンプルDataFrameには、全ての要素がNaNである行は存在しません(列Dは全てNaNですが、行としてはどの行も列Eには有効な値が入っています)。したがって、どの行も削除されず、元のDataFrameと同じ結果になります。
では、全ての要素がNaNである行を含むDataFrameを作成して試してみましょう。
“`python
data_with_all_nan_row = {‘A’: [1, 2, np.nan, 4, 5, np.nan],
‘B’: [6, np.nan, 8, 9, 10, np.nan],
‘C’: [11, 12, 13, np.nan, 15, np.nan],
‘D’: [np.nan, np.nan, np.nan, np.nan, np.nan, np.nan],
‘E’: [16, 17, 18, 19, 20, np.nan]}
df_all_nan_row = pd.DataFrame(data_with_all_nan_row)
print(“\n全ての要素がNaNの行を含むDataFrame:”)
print(df_all_nan_row)
df_dropped_all_from_new = df_all_nan_row.dropna(axis=’index’, how=’all’)
print(“\ndropna(how=’all’) で全ての要素がNaNの行を削除 (新しいDataFrame):”)
print(df_dropped_all_from_new)
“`
出力:
“`
全ての要素がNaNの行を含むDataFrame:
A B C D E
0 1.0 6.0 11.0 NaN 16.0
1 2.0 NaN 12.0 NaN 17.0
2 NaN 8.0 13.0 NaN 18.0
3 4.0 9.0 NaN NaN 19.0
4 5.0 10.0 15.0 NaN 20.0
5 NaN NaN NaN NaN NaN
dropna(how=’all’) で全ての要素がNaNの行を削除 (新しいDataFrame):
A B C D E
0 1.0 6.0 11.0 NaN 16.0
1 2.0 NaN 12.0 NaN 17.0
2 NaN 8.0 13.0 NaN 18.0
3 4.0 9.0 NaN NaN 19.0
4 5.0 10.0 15.0 NaN 20.0
“`
新しいDataFrameではインデックス5の行が全ての要素でNaNを含んでいたため、dropna(how='all')
によってその行だけが削除されました。
5.3. 特定の列に基づいて削除 (subset
)
subset
パラメータを使用すると、特定の列にNaNが含まれているかどうかに基づいて行を削除できます。これは、分析において特定の重要な列に欠損値がないことが必須である場合に非常に役立ちます。
例: 列Aまたは列BのいずれかにNaNを含む行を削除したい場合。(how='any'
と組み合わせる)
“`python
元のdfを使用
print(“元のDataFrame:”)
print(df)
df_dropped_subset_AB_any = df.dropna(subset=[‘A’, ‘B’], how=’any’)
print(“\ndropna(subset=[‘A’, ‘B’], how=’any’) でAまたはBにNaNを含む行を削除:”)
print(df_dropped_subset_AB_any)
“`
出力:
“`
元のDataFrame:
A B C D E
0 1.0 6.0 11.0 NaN 16
1 2.0 NaN 12.0 NaN 17
2 NaN 8.0 13.0 NaN 18
3 4.0 9.0 NaN NaN 19
4 5.0 10.0 15.0 NaN 20
dropna(subset=[‘A’, ‘B’], how=’any’) でAまたはBにNaNを含む行を削除:
A B C D E
0 1.0 6.0 11.0 NaN 16
3 4.0 9.0 NaN NaN 19
4 5.0 10.0 15.0 NaN 20
“`
インデックス0: A=1.0, B=6.0 (NaNなし) -> 残る
インデックス1: A=2.0, B=NaN (BにNaNあり) -> 削除
インデックス2: A=NaN, B=8.0 (AにNaNあり) -> 削除
インデックス3: A=4.0, B=9.0 (NaNなし) -> 残る
インデックス4: A=5.0, B=10.0 (NaNなし) -> 残る
結果として、インデックス1と2の行が削除されました。列DのNaNはチェック対象外です。
例: 列Aと列Bの両方がNaNである行を削除したい場合。(how='all'
と組み合わせる)
“`python
元のdfを使用
print(“元のDataFrame:”)
print(df)
df_dropped_subset_AB_all = df.dropna(subset=[‘A’, ‘B’], how=’all’)
print(“\ndropna(subset=[‘A’, ‘B’], how=’all’) でAとBの両方がNaNの行を削除:”)
print(df_dropped_subset_AB_all)
“`
出力:
“`
元のDataFrame:
A B C D E
0 1.0 6.0 11.0 NaN 16
1 2.0 NaN 12.0 NaN 17
2 NaN 8.0 13.0 NaN 18
3 4.0 9.0 NaN NaN 19
4 5.0 10.0 15.0 NaN 20
dropna(subset=[‘A’, ‘B’], how=’all’) でAとBの両方がNaNの行を削除:
A B C D E
0 1.0 6.0 11.0 NaN 16
1 2.0 NaN 12.0 NaN 17
2 NaN 8.0 13.0 NaN 18
3 4.0 9.0 NaN NaN 19
4 5.0 10.0 15.0 NaN 20
“`
このサンプルDataFrameには、列Aと列Bの両方がNaNである行は存在しません。したがって、どの行も削除されず、元のDataFrameと同じ結果になります。
5.4. しきい値に基づく削除 (thresh
)
thresh
パラメータを使用すると、「非NaN値が指定した数以上含まれている行のみを残す」という条件で削除できます。これは、ほとんどのデータが欠損しているような「スカスカ」の行を削除したいが、少しの欠損なら許容したい場合に便利です。thresh=N
と指定すると、少なくともN個の非NaN値を持つ行が保持されます。
例: 少なくとも3つの非NaN値を持つ行を残す。
“`python
元のdfを使用
print(“元のDataFrame:”)
print(df)
df_dropped_thresh = df.dropna(axis=’index’, thresh=3)
print(“\ndropna(thresh=3) で少なくとも3つの非NaN値を持つ行を残す:”)
print(df_dropped_thresh)
“`
出力:
“`
元のDataFrame:
A B C D E
0 1.0 6.0 11.0 NaN 16
1 2.0 NaN 12.0 NaN 17
2 NaN 8.0 13.0 NaN 18
3 4.0 9.0 NaN NaN 19
4 5.0 10.0 15.0 NaN 20
dropna(thresh=3) で少なくとも3つの非NaN値を持つ行を残す:
A B C D E
0 1.0 6.0 11.0 NaN 16
1 2.0 NaN 12.0 NaN 17
2 NaN 8.0 13.0 NaN 18
3 4.0 9.0 NaN NaN 19
4 5.0 10.0 15.0 NaN 20
“`
各行の非NaN値の数をカウントしてみましょう(列Dは全てNaNなので、その行の非NaN値カウントは列A, B, C, Eの非NaN値の合計になります):
インデックス0: A, B, C, E -> 4つ非NaN
インデックス1: A, C, E -> 3つ非NaN
インデックス2: B, C, E -> 3つ非NaN
インデックス3: A, B, E -> 3つ非NaN
インデックス4: A, B, C, E -> 4つ非NaN
全ての行が少なくとも3つの非NaN値を持っているため、どの行も削除されませんでした。
例2: 少なくとも4つの非NaN値を持つ行を残す。
“`python
元のdfを使用
print(“元のDataFrame:”)
print(df)
df_dropped_thresh_4 = df.dropna(axis=’index’, thresh=4)
print(“\ndropna(thresh=4) で少なくとも4つの非NaN値を持つ行を残す:”)
print(df_dropped_thresh_4)
“`
出力:
“`
元のDataFrame:
A B C D E
0 1.0 6.0 11.0 NaN 16
1 2.0 NaN 12.0 NaN 17
2 NaN 8.0 13.0 NaN 18
3 4.0 9.0 NaN NaN 19
4 5.0 10.0 15.0 NaN 20
dropna(thresh=4) で少なくとも4つの非NaN値を持つ行を残す:
A B C D E
0 1.0 6.0 11.0 NaN 16
4 5.0 10.0 15.0 NaN 20
“`
インデックス0と4は4つの非NaN値を持つため残りました。インデックス1, 2, 3は3つの非NaN値しか持たないため削除されました。
5.5. inplace
パラメータ
デフォルトでは dropna()
は新しいDataFrameを返します。元のDataFrameを変更したい場合は inplace=True
を指定します。大規模データでメモリを節約したい場合などに有用ですが、元のデータを失う可能性があるため注意が必要です。
“`python
元のdfを再作成 (変更されないように)
data = {‘A’: [1, 2, np.nan, 4, 5],
‘B’: [6, np.nan, 8, 9, 10],
‘C’: [11, 12, 13, np.nan, 15],
‘D’: [np.nan, np.nan, np.nan, np.nan, np.nan],
‘E’: [16, 17, 18, 19, 20]}
df_inplace = pd.DataFrame(data)
print(“inplace前のDataFrame:”)
print(df_inplace)
df_inplace.dropna(axis=’index’, how=’any’, inplace=True)
print(“\ndropna(inplace=True) 実行後のDataFrame:”)
print(df_inplace)
“`
出力:
“`
inplace前のDataFrame:
A B C D E
0 1.0 6.0 11.0 NaN 16
1 2.0 NaN 12.0 NaN 17
2 NaN 8.0 13.0 NaN 18
3 4.0 9.0 NaN NaN 19
4 5.0 10.0 15.0 NaN 20
dropna(inplace=True) 実行後のDataFrame:
A B C D E
4 5.0 10.0 15.0 NaN 20
“`
inplace=True
を指定すると、メソッドは None
を返し、元のDataFrame (df_inplace
) が変更されます。
6. NaN行の削除方法:ブールインデックス参照による方法
dropna()
メソッドは非常に便利ですが、特定の複雑な条件でNaNを含む行を選択的に残したり削除したりしたい場合には、ブールインデックス参照(Boolean Indexing)がより柔軟な方法を提供します。
ブールインデックス参照は、DataFrameにTrue/Falseのシーケンス(SeriesまたはDataFrame)を渡すことで、対応する位置がTrueである行(または列)を選択する機能です。NaNを検出する isnull()
や notnull()
メソッドはブール値を返すため、これらを活用することでNaN行を選択的に扱うことができます。
「NaNを含まない行のみを選択する」という操作は、結果的にNaN行を削除したことと同じになります。
6.1. 特定の列にNaNがない行を選択する
例えば、列AにNaNがない行のみを残したい(列AにNaNがある行を削除したい)場合、df['A'].notna()
で列Aの各要素がNaNでないかどうかのブールSeriesを取得し、これでDataFrameをフィルタリングします。
“`python
元のdfを使用
print(“元のDataFrame:”)
print(df)
列AにNaNがない行を選択
df_notna_A = df[df[‘A’].notna()]
print(“\n列AにNaNがない行を選択:”)
print(df_notna_A)
“`
出力:
“`
元のDataFrame:
A B C D E
0 1.0 6.0 11.0 NaN 16
1 2.0 NaN 12.0 NaN 17
2 NaN 8.0 13.0 NaN 18
3 4.0 9.0 NaN NaN 19
4 5.0 10.0 15.0 NaN 20
列AにNaNがない行を選択:
A B C D E
0 1.0 6.0 11.0 NaN 16
1 2.0 NaN 12.0 NaN 17
3 4.0 9.0 NaN NaN 19
4 5.0 10.0 15.0 NaN 20
“`
列AがNaNであるインデックス2の行が削除されました。
6.2. 複数の列の条件を組み合わせる
ブールインデックス参照の強力な点は、複数の条件を論理演算子 (&
(AND), |
(OR), ~
(NOT)) で組み合わせられることです。
例: 列Aにも列BにもNaNがない行を選択する(列Aまたは列BにNaNがある行を削除)。これは dropna(subset=['A', 'B'], how='any')
と同じ結果になります。
“`python
元のdfを使用
print(“元のDataFrame:”)
print(df)
列AがNaNでなく、かつ列BがNaNでない行を選択
condition = df[‘A’].notna() & df[‘B’].notna()
df_notna_AB_both = df[condition]
print(“\n列AにもBにもNaNがない行を選択:”)
print(df_notna_AB_both)
“`
出力:
“`
元のDataFrame:
A B C D E
0 1.0 6.0 11.0 NaN 16
1 2.0 NaN 12.0 NaN 17
2 NaN 8.0 13.0 NaN 18
3 4.0 9.0 NaN NaN 19
4 5.0 10.0 15.0 NaN 20
列AにもBにもNaNがない行を選択:
A B C D E
0 1.0 6.0 11.0 NaN 16
3 4.0 9.0 NaN NaN 19
4 5.0 10.0 15.0 NaN 20
“`
インデックス0, 3, 4の行は列Aと列Bの両方に有効な値があるため残りました。インデックス1 (BがNaN) と2 (AがNaN) は削除されました。
例: 列Aまたは列Bの少なくとも一方にNaNがない行を選択する(列Aと列Bの両方がNaNである行を削除)。これは dropna(subset=['A', 'B'], how='all')
と同じ結果になります。
“`python
元のdfを使用
print(“元のDataFrame:”)
print(df)
列AがNaNでない、または列BがNaNでない行を選択
condition = df[‘A’].notna() | df[‘B’].notna()
df_notna_AB_either = df[condition]
print(“\n列AまたはBの少なくとも一方にNaNがない行を選択:”)
print(df_notna_AB_either)
“`
出力:
“`
元のDataFrame:
A B C D E
0 1.0 6.0 11.0 NaN 16
1 2.0 NaN 12.0 NaN 17
2 NaN 8.0 13.0 NaN 18
3 4.0 9.0 NaN NaN 19
4 5.0 10.0 15.0 NaN 20
列AまたはBの少なくとも一方にNaNがない行を選択:
A B C D E
0 1.0 6.0 11.0 NaN 16
1 2.0 NaN 12.0 NaN 17
2 NaN 8.0 13.0 NaN 18
3 4.0 9.0 NaN NaN 19
4 5.0 10.0 15.0 NaN 20
“`
列Aと列Bの両方がNaNである行はこのDataFrameには存在しないため、全ての行が残りました。
例: 列AにNaNがあり、かつ列BにはNaNがない行を選択する。
“`python
元のdfを使用
print(“元のDataFrame:”)
print(df)
列AがNaNであり、かつ列BがNaNでない行を選択
condition = df[‘A’].isna() & df[‘B’].notna()
df_isna_A_notna_B = df[condition]
print(“\n列AにNaNがあり、かつ列BにはNaNがない行を選択:”)
print(df_isna_A_notna_B)
“`
出力:
“`
元のDataFrame:
A B C D E
0 1.0 6.0 11.0 NaN 16
1 2.0 NaN 12.0 NaN 17
2 NaN 8.0 13.0 NaN 18
3 4.0 9.0 NaN NaN 19
4 5.0 10.0 15.0 NaN 20
列AにNaNがあり、かつ列BにはNaNがない行を選択:
A B C D E
2 NaN 8.0 13.0 NaN 18
“`
インデックス2の行は列AがNaN (True)、列BがNaNでない (True) なので、条件を満たして選択されました。
6.3. ブールインデックス参照の利点と欠点
利点:
- 柔軟性: 任意の複雑な論理条件でフィルタリングできます。
- 可読性: 条件式が直感的で分かりやすい場合があります。
欠点:
- パフォーマンス: 特に大規模なデータセットでは、C拡張で最適化されている
dropna()
メソッドの方が一般的に高速な場合があります。ブールSeriesを作成し、それを使ってDataFrameをフィルタリングするプロセスは、内部的に新しいDataFrameのコピーを作成することが多いため、メモリ使用量が増える可能性もあります(SettingWithCopyWarningの原因になることもあります)。 - 記述量: 単純な「NaNを含む行を全て削除」のようなケースでは、
dropna()
に比べてコードの記述量が多くなります。
特定の列にNaNがあるかどうかをチェックして削除するだけなら dropna(subset=...)
が最も簡潔で効率的です。しかし、複数の列のNaN状態を複雑に組み合わせて条件を作りたい場合は、ブールインデックス参照が適しています。
7. 効率的なNaN行削除のための考慮事項
データセットのサイズが大きくなるにつれて、処理の効率性が重要になります。NaN行削除も例外ではありません。
7.1. inplace=True
vs 新しいDataFrameの作成
dropna()
メソッドの inplace
パラメータは、メモリ使用量とパフォーマンスに影響を与えます。
inplace=True
: 元のDataFrameを直接変更します。新しいDataFrameを作成しないため、メモリ使用量を抑えることができます。ただし、元のデータが失われるため、後で元のデータに戻りたい場合は事前にコピーを取っておく必要があります。また、pandasの一部のバージョンや操作によっては、予期しない挙動(例: SettingWithCopyWarning)を引き起こす可能性があります。inplace=False
(デフォルト): NaN行を削除した新しいDataFrameを返します。元のDataFrameは変更されません。これにより、元のデータを保持しつつ、処理結果を別の変数に代入できます。コードの安全性や予測可能性は高まりますが、新しいDataFrameのためのメモリが必要になります。
大規模データセットでメモリが制約となる場合は inplace=True
が有効ですが、そうでなければデフォルトの inplace=False
を使用し、新しい変数に結果を代入する方が一般的には推奨されます。
7.2. dropna()
とブールインデックス参照のパフォーマンス
前述のように、単純なNaN行削除においては、C拡張で実装されている dropna()
がブールインデックス参照よりも高速であることが多いです。特に、how='any'
やhow='all'
、subset
のような標準的な条件の場合は dropna()
を優先的に検討すると良いでしょう。
ただし、ブールインデックス参照は論理演算子を自由に組み合わせられるため、より複雑な条件を直接的に表現できます。例えば、「列AがNaN ではない かつ (列BがNaN である または 列CがNaN ではない) 」のような条件でフィルタリングしたい場合、dropna()
のパラメータだけでは表現できません。このような場合は、ブールインデックス参照を使用することになります。パフォーマンスが最優先される場面では、両方の方法で実装してみて、実際にかかる時間を計測(プロファイリング)してみるのが最も確実です。
7.3. 処理対象の列を絞る
DataFrameの列数が非常に多い場合、全ての列に対してNaNチェックを行うのは非効率なことがあります。もし特定の重要な列のNaNだけを考慮して行を削除したいのであれば、dropna()
の subset
パラメータを使うか、あるいは事前に必要な列だけを選択した上で削除処理を行う方が効率的です。
“`python
元のdfを使用
print(“元のDataFrame:”)
print(df)
列A, B, Cのみを対象に、1つでもNaNを含む行を削除
df_subset_efficient = df.dropna(subset=[‘A’, ‘B’, ‘C’], how=’any’)
print(“\ndropna(subset=[‘A’, ‘B’, ‘C’], how=’any’) でA,B,CのいずれかにNaNを含む行を削除:”)
print(df_subset_efficient)
“`
出力:
“`
元のDataFrame:
A B C D E
0 1.0 6.0 11.0 NaN 16
1 2.0 NaN 12.0 NaN 17
2 NaN 8.0 13.0 NaN 18
3 4.0 9.0 NaN NaN 19
4 5.0 10.0 15.0 NaN 20
dropna(subset=[‘A’, ‘B’, ‘C’], how=’any’) でA,B,CのいずれかにNaNを含む行を削除:
A B C D E
0 1.0 6.0 11.0 NaN 16
4 5.0 10.0 15.0 NaN 20
“`
インデックス0: A, B, C 全て非NaN -> 残る
インデックス1: BがNaN -> 削除
インデックス2: AがNaN -> 削除
インデックス3: CがNaN -> 削除
インデックス4: A, B, C 全て非NaN -> 残る
この結果は dropna(subset=['A', 'B', 'C'], how='any')
の実行例です。列DにNaNが含まれていても、subsetに指定されていないため削除の判断には影響しません。これにより、不要な列のチェックをスキップし、処理を効率化できます。
7.4. 大規模データにおける考慮
メモリに乗らないほど巨大なデータセットを扱う場合、pandas DataFrame全体を一度にメモリに読み込んで処理することが難しくなります。このような場合は、データを小さな塊(チャンク)に分割して読み込み、チャンクごとに処理を行う方法(例えば pd.read_csv(..., chunksize=...)
)が考えられます。
チャンクごとにNaN行を削除する場合、各チャンクに対して dropna()
を適用し、処理済みのチャンクを結合していくという流れになります。ただし、チャンクの境界をまたぐような複雑な欠損値パターンを考慮する必要がある場合は、さらに工夫が必要になることもあります。しかし、単純なNaN行削除であれば、チャンク処理も有効な手段となり得ます。
8. 削除以外の欠損値処理方法(簡単な紹介)
NaN行の削除は簡単な方法ですが、常に最善の選択肢とは限りません。情報損失やバイアスのリスクを回避するため、あるいは単純にデータが少なくなってしまうのを避けたい場合、欠損値の補完 (Imputation) が検討されます。
補完とは、欠損値を何らかの値で置き換える処理です。pandasでは fillna()
メソッドを使用してさまざまな補完が可能です。
- 定数で補完:
df.fillna(value)
(例: 0, 特定の文字列など) - 統計量で補完:
- 平均値:
df.fillna(df.mean())
- 中央値:
df.fillna(df.median())
- 最頻値:
df.fillna(df.mode().iloc[0])
(mode()は複数の最頻値を返す可能性があるため、iloc[0]で最初の最頻値を取得することが多い)
- 平均値:
- 前後値で補完:
- 前方の非欠損値で補完 (Forward fill):
df.fillna(method='ffill')
またはdf.fillna(method='pad')
- 後方の非欠損値で補完 (Backward fill):
df.fillna(method='bfill')
またはdf.fillna(method='backfill')
- 前方の非欠損値で補完 (Forward fill):
- 回帰分析などモデルベースの補完: scikit-learnなどのライブラリと組み合わせて、他の列の値から欠損値を予測して埋める方法。
補完は削除よりも情報損失を抑えられますが、補完された値が実際の値と異なれば、分析に新たなバイアスを導入する可能性もあります。どの補完方法が適切かは、欠損値の性質(なぜ欠損しているのか)、データの分布、そして分析の目的に依存します。
本記事はNaN行削除に焦点を当てていますが、欠損値処理全体を考える際には、削除と補完の両方を理解し、データと分析課題に応じて適切な方法を選択することが重要です。
9. 実践的なアドバイス
NaN行の削除は、データ前処理パイプラインの一部です。効果的かつ効率的に行うための実践的なアドバイスをいくつかご紹介します。
- まず欠損値の分析から始める: いきなり削除するのではなく、まず
isnull().sum()
やinfo()
を使って、どの列にどのくらいの割合でNaNがあるのかを把握しましょう。欠損値のパターン(特定の条件で発生しやすいかなど)も調査できるとなお良いです。df[df.isnull().any(axis=1)]
のようにして、NaNを含む行だけを抽出して確認するのも有効です。 - 削除の影響を評価する: 削除によってデータセットのサイズがどれだけ小さくなるか、特定のサブグループのデータが過剰に削除されないかなどを事前にシミュレーションまたは確認しましょう。
- 重要な列を特定する: 分析にとって不可欠な列を特定し、これらの列にNaNがある場合にのみ行を削除する(
dropna(subset=[...])
)など、削除の条件をデータと分析目的に合わせてカスタマイズしましょう。 - ドキュメントを参照する: pandasのバージョンアップによってメソッドの挙動や推奨されるプラクティスが変わることがあります。常に最新のpandasドキュメントを参照する癖をつけましょう。
- コードの可読性を意識する: 効率性も重要ですが、特に共同開発や将来のメンテナンスを考えると、コードの分かりやすさも大切です。複雑なブールインデックス参照を使う場合は、条件式を分割したり、コメントをつけたりするなど、可読性を高める工夫をしましょう。
10. まとめ
pandas DataFrameにおけるNaNを含む行の削除は、データ前処理における基本的かつ重要なステップです。本記事では、その必要性、NaNの検出方法、そして主に dropna()
メソッドとブールインデックス参照を用いた具体的な削除方法を詳細に解説しました。
dropna()
メソッドは、axis
,how
,subset
,thresh
といったパラメータを組み合わせることで、多様な条件に基づいたNaN行の削除を効率的に実行できます。- ブールインデックス参照 (
df[df[column].notna()]
など) は、より複雑な論理条件でNaN行を選択的に削除(または保持)したい場合に高い柔軟性を提供します。 - 処理の効率性を考慮する際には、
inplace
パラメータの使い分け、データサイズ、そしてdropna()
とブールインデックス参照の特性を理解することが重要です。 - NaN行の削除は情報損失のリスクを伴うため、常に欠損値の分析を行い、削除の影響を慎重に評価した上で、データと分析目的に最も適した方法を選択する必要があります。多くの場合、補完 (Imputation) も含めた総合的な欠損値処理戦略の一部として、削除を検討することになります。
これらの知識を活用することで、pandasを使ったデータ前処理において、欠損値であるNaNを含む行を適切かつ効率的に処理できるようになるでしょう。データ分析の精度と信頼性を高めるために、欠損値処理は常に丁寧に行うべき工程であることを忘れずに、実践に取り組んでください。