pandas: 欠損値割合を指定して列を削除する

close up shot of a panda bear on a tree branch

python-pandasを用いた前処理を実施する際に,欠損率XX%を超える列を削除する方法について。

結論

自前で処理を作成するパターンと,pandas.DataFrame.dropnaメソッドの”thresh”を設定するパターンと2通りが存在する。

自作関数パターン

下記の”drop_na_threshold”関数が一例。引数”df”はpandas.DataFrameオブジェクト,”threshold”は列削除基準の欠損値割合。戻り値は欠損値割合が”threshold”以上となる列が削除されたpandas.DataFrame。

処理としては単純に,[特定列の欠損値件数/DataFrame行数]で求まる欠損値割合が”threshold”以上になる列名のみを取得し,その列を”drop”メソッドで指定して削除しているだけ。

# 変数”df”は何らかのpandas.DataFrameオブジェクト
def drop_na_threshold(df, threshold=0.1):
    dflen = df.shape[0]  # データフレームの行数
    
    # [各列の欠損値個数/データフレーム列数] で求めた欠損値割合が閾値以上となる列名を取得する
    col_is_drop = [c for c in df.columns if df[c].isnull().sum() / df.shape[0] >= threshold]
    
    # 各行の欠損値割合が閾値以上となる列を削除したデータフレームを返す
    return df.drop(col_is_drop, axis=1)

例えば下記のように利用することで,指定欠損率割合=30%以上の列が削除されたpandas.DataFrameを得ることができる。

# 変数”df”は何らかのpandas.DataFrameオブジェクト

threshold = 0.3
df_dropped = drop_na_threshold(df, threshold)

pandas.DataFrame.dropnaメソッドパターン

自作関数の作成後に気付いたが,pandas.DataFrame.dropnaメソッドの”thresh”を指定するだけでよい。

”thresh”に値を指定すると,「欠損値ではない値が指定値以上存在する列」が返される。指定しているのは欠損値割合ではなく,あくまでも欠損していない要素の個数である点に注意が必要。下記の例のように,”thresh=2000”と指定した場合には,各列において欠損していない要素が2,000個以上存在する列のみからなるpandas.DataFrameが得られる。

# 変数”df”は何らかのpandas.DataFrameオブジェクト

thresh = 2000
df_dropped = df.dropna(axis=1, thresh=thresh)

仮に非欠損要素数ではなく,欠損値割合を指定したい場合は下記のような書き方をする。

4行目処理において,指定欠損割合における[DataFrame行数 – 指定欠損割合*DataFrame行数],つまり非欠損要素数を求めておき,その結果を”drop”メソッドのthreshに渡す。

# 変数”df”は何らかのpandas.DataFrameオブジェクト

thresh_ratio = 0.3
num_not_na_rows = df.shape[0] - int(df.shape[0] * thresh_ratio)
df_dropped = df.dropna(axis=1, thresh=num_not_na_rows)

動作確認プログラム

上記処理内容の実行・確認は下記コードより

自作関数パターン

import numpy as np
import pandas as pd

# 自作関数
def drop_na_threshold(df, threshold=0.1):
    dflen = df.shape[0]  # データフレームの行数
    
    # [各行の欠損値個数/データフレームの行数] で求めた欠損値割合が閾値以上となる列名を取得する
    col_is_drop = [c for c in df.columns if df[c].isnull().sum() / df.shape[0] >= threshold]
    
    # 各行の欠損値割合が閾値以上となる列を削除したデータフレームを返す
    return df.drop(col_is_drop, axis=1)

# サンプルデータの作成
rows = 10000
columns = 10
np.random.seed(0)
data = np.random.random((rows, columns))  # 0から1の範囲の値を有する10,000行10列の配列

# 行ごとに異なる閾値で欠損値nanに置き換える
for i, threash in enumerate([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]):
    data[:, i] = np.where(data[:, i] < threash, np.nan, data[:, i])  # 値が閾値より小さいものを欠損値nanに置き換える
    
# DataFrame作成
df = pd.DataFrame(data) 
print('列削除前のDataFrame')
display(df)

# 欠損値割合表示
print('各列欠損値個数\n', df.isnull().sum())

# 指定欠損割合に応じて列を削除
threshold = 0.3
df_dropped = drop_na_threshold(df, threshold)
print('列削除後のDataFrame')
display(df_dropped)
print(f"各列について欠損値行数が {threshold*10} 割以上になる列が削除された")

pandas.DataFrame.dropnaパターン

import numpy as np
import pandas as pd

# 自作関数
def drop_na_threshold(df, threshold=0.1):
    dflen = df.shape[0]  # データフレームの行数
    
    # [各行の欠損値個数/データフレームの行数] で求めた欠損値割合が閾値以上となる列名を取得する
    col_is_drop = [c for c in df.columns if df[c].isnull().sum() / df.shape[0] >= threshold]
    
    # 各行の欠損値割合が閾値以上となる列を削除したデータフレームを返す
    return df.drop(col_is_drop, axis=1)

# サンプルデータの作成
rows = 10000
columns = 10
np.random.seed(0)
data = np.random.random((rows, columns))  # 0から1の範囲の値を有する10,000行10列の配列

# 行ごとに異なる閾値で欠損値nanに置き換える
for i, threash in enumerate([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]):
    data[:, i] = np.where(data[:, i] < threash, np.nan, data[:, i])  # 値が閾値より小さいものを欠損値nanに置き換える
    
# DataFrame作成
df = pd.DataFrame(data) 
print('列削除前のDataFrame')
display(df)

# 欠損値割合表示
print('各列欠損値個数\n', df.isnull().sum())

# 指定欠損割合に応じて列を削除
thresh_ratio = 0.3
num_not_na_rows = df.shape[0] - int(df.shape[0] * thresh_ratio)
df_dropped = df.dropna(axis=1, thresh=num_not_na_rows) 
print('列削除後のDataFrame')
display(df_dropped)
print(f"各列について欠損値行数が {threshold*10} 割以上になる列が削除された")

コメント