note.nkmk.me

pandasで欠損値NaNを除外(削除)・置換(穴埋め)・抽出

Date: 2017-11-16 / Modified: 2018-06-12 / tags: Python, pandas

例えばcsvファイルをpandasで読み込んだとき、要素が空白だったりすると欠損値NaN(Not a Number)だと見なされる。

欠損値を除外(削除)するにはdropna()メソッド、欠損値を他の値に置換(穴埋め)するにはfillna()メソッドを使う。

また、欠損値を含む行や列を抽出したい場合は、要素が欠損値かどうかを判定するisnull()メソッドを使う。

例として空白を含むcsvファイルをread_csvで読み込んで使用する。NumPyとmathは説明のためにインポートしているだけなので、処理自体には必要ない。

import pandas as pd
import numpy as np
import math

df = pd.read_csv('data/src/sample_pandas_normal_nan.csv')
print(df)
#       name   age state  point  other
# 0    Alice  24.0    NY    NaN    NaN
# 1      NaN   NaN   NaN    NaN    NaN
# 2  Charlie   NaN    CA    NaN    NaN
# 3     Dave  68.0    TX   70.0    NaN
# 4    Ellen   NaN    CA   88.0    NaN
# 5    Frank  30.0   NaN    NaN    NaN

サンプルのcsvファイルはコチラ。

ここでは以下の内容について説明する。

  • pandasにおける欠損値NaNの型
  • pandasにおいて欠損値NaNとして扱われる値
  • 欠損値を除外(削除)する
    • すべての値が欠損値である行・列を削除する
    • 欠損値が一つでも含まれる行・列を削除する
    • 欠損値の数に応じて行・列を削除する
    • 特定の行・列に欠損値がある列・行を削除する
    • pandas.Seriesの場合
  • 欠損値を置換(穴埋め)する
    • 共通の値で一律に置換する
    • 列ごとに異なる値で置換する
    • 列ごとに平均値、中央値、最頻値などで置換する
    • 前後の値で置換する
    • 最大連続置換回数を指定
    • pandas.Seriesの場合
  • 欠損値を抽出する
    • 特定の行・列に欠損値がある列・行を抽出する
    • 欠損値が一つでも含まれる行・列を抽出する

削除や置換、抽出ではなく、行・列に欠損値が含まれているかを判定したり欠損値の個数をカウントしたい場合は以下の記事を参照。

また、単純な置換ではなく前後の値から補間する場合は以下の記事を参照。

スポンサーリンク

pandasにおける欠損値NaNの型

pandasにおいて、列に一つでも欠損値NaNが含まれていると、ほかの値がすべて整数intでもその列のdtypeは浮動小数点として処理される。文字列などPythonの組み込み型を格納するobject型の列はそのまま。

df = pd.read_csv('data/src/sample_pandas_normal_nan.csv')
print(df)
#       name   age state  point  other
# 0    Alice  24.0    NY    NaN    NaN
# 1      NaN   NaN   NaN    NaN    NaN
# 2  Charlie   NaN    CA    NaN    NaN
# 3     Dave  68.0    TX   70.0    NaN
# 4    Ellen   NaN    CA   88.0    NaN
# 5    Frank  30.0   NaN    NaN    NaN

print(df.dtypes)
# name      object
# age      float64
# state     object
# point    float64
# other    float64
# dtype: object

コードを書く上では特に気にする必要はないが、object型の列の欠損値は組み込み型floatで、浮動小数点型の列の欠損値はNumPyのnumpy.float64となる(ビット数を表す末尾の数字は環境によって異なる場合がある)。

print(df.at[1, 'name'])
print(type(df.at[1, 'name']))
# nan
# <class 'float'>

print(df.at[0, 'point'])
print(type(df.at[0, 'point']))
# nan
# <class 'numpy.float64'>

欠損値であるかどうかの判定にはpandas.isnull()を使う。numpy.isnan(), math.isnan()でもいい(それぞれNumPy, mathのインポートが必要)。

print(pd.isnull(df.at[0, 'point']))
print(np.isnan(df.at[0, 'point']))
print(math.isnan(df.at[0, 'point']))
# True
# True
# True

np.nanmath.nan==で比較してもFalseを返すので注意。

print(df.at[0, 'point'] == np.nan)
# False

要素ごとの判定ではなく、行・列に欠損値が含まれているかを判定したい場合は以下の記事を参照。

pandasにおいて欠損値NaNとして扱われる値

pandasにおいては、None, np.nan, math.nanおよびpd.np.nanが欠損値NaNとして扱われ、以降で説明するdropna()fillna()の対象となる。なお、pd.np.nanはpandasをインポートしていればNumPyをインポートしていなくても使用可能。

Nonenumpy.float64nanに変換される。

s_nan = pd.Series([None, np.nan, math.nan, pd.np.nan])
print(s_nan)
# 0   NaN
# 1   NaN
# 2   NaN
# 3   NaN
# dtype: float64

print(s_nan[0])
print(type(s_nan[0]))
# nan
# <class 'numpy.float64'>

print(s_nan.isnull())
# 0    True
# 1    True
# 2    True
# 3    True
# dtype: bool

上述のように、欠損値が含まれていると整数int型の値が浮動小数点float型にキャストされる。

s_nan_int = pd.Series([None, pd.np.nan, 0, 1])
print(s_nan_int)
# 0    NaN
# 1    NaN
# 2    0.0
# 3    1.0
# dtype: float64

print(s_nan_int.isnull())
# 0     True
# 1     True
# 2    False
# 3    False
# dtype: bool

文字列str型の値が含まれていると、そのpandas.Series(およびpandas.DataFrameの列)データ型はobjectとなる。Nonenumpy.float64nanに変換されずNoneのままとなるが、dropna()fillna()の対象となるので実用上は特に気にする必要はない。

s_nan_str = pd.Series([None, pd.np.nan, 'NaN', 'nan'])
print(s_nan_str)
# 0    None
# 1     NaN
# 2     NaN
# 3     nan
# dtype: object

print(s_nan_str[0])
print(type(s_nan_str[0]))
# None
# <class 'NoneType'>

print(s_nan_str.isnull())
# 0     True
# 1     True
# 2    False
# 3    False
# dtype: bool

文字列'NaN'などのように欠損値として扱いたい値がある場合はreplace()メソッドで欠損値に置き換えればよい。

s_nan_str_replace = s_nan_str.replace({'NaN': pd.np.nan, 'nan': pd.np.nan})
print(s_nan_str_replace)
# 0   NaN
# 1   NaN
# 2   NaN
# 3   NaN
# dtype: float64

print(s_nan_str_replace.isnull())
# 0    True
# 1    True
# 2    True
# 3    True
# dtype: bool

なお、上の例でcsvファイルの空白の値が欠損値として読み込まれたように、read_csv()などのファイルを読み込む関数では、空文字列(空白)や文字列'NaN', 'null'などがデフォルトで欠損値として扱われる。詳細は以下の記事を参照。

欠損値を除外(削除)する

欠損値を除外(削除)するにはメソッドdropna()を使う。

デフォルトでは新しいオブジェクトを返して元のオブジェクトは変更されないが、引数inplace=Trueを指定すると元のオブジェクト自体が変更される。

先に読み込んだpandas.DataFrameを例とする。

print(df)
#       name   age state  point  other
# 0    Alice  24.0    NY    NaN    NaN
# 1      NaN   NaN   NaN    NaN    NaN
# 2  Charlie   NaN    CA    NaN    NaN
# 3     Dave  68.0    TX   70.0    NaN
# 4    Ellen   NaN    CA   88.0    NaN
# 5    Frank  30.0   NaN    NaN    NaN

すべての値が欠損値である行・列を削除する

引数how='all'を指定するとすべての値が欠損値の行が削除される。

print(df.dropna(how='all'))
#       name   age state  point  other
# 0    Alice  24.0    NY    NaN    NaN
# 2  Charlie   NaN    CA    NaN    NaN
# 3     Dave  68.0    TX   70.0    NaN
# 4    Ellen   NaN    CA   88.0    NaN
# 5    Frank  30.0   NaN    NaN    NaN

さらにaxis=1とするとすべての値が欠損値の列が削除される。

print(df.dropna(how='all', axis=1))
#       name   age state  point
# 0    Alice  24.0    NY    NaN
# 1      NaN   NaN   NaN    NaN
# 2  Charlie   NaN    CA    NaN
# 3     Dave  68.0    TX   70.0
# 4    Ellen   NaN    CA   88.0
# 5    Frank  30.0   NaN    NaN

axis=[0, 1]とするとすべての値が欠損値の行と列が削除されるが、バージョン0.23.0から引数axisのリストやタプルでの指定はDeprecated(非推奨)となった。

行・列両方に適用したい場合はdropna()を繰り返し適用すればOK。

print(df.dropna(how='all').dropna(how='all', axis=1))
#       name   age state  point
# 0    Alice  24.0    NY    NaN
# 2  Charlie   NaN    CA    NaN
# 3     Dave  68.0    TX   70.0
# 4    Ellen   NaN    CA   88.0
# 5    Frank  30.0   NaN    NaN

欠損値が一つでも含まれる行・列を削除する

すべての値が欠損値である行と列を削除したデータを例とする。

df2 = df.dropna(how='all').dropna(how='all', axis=1)
print(df2)
#       name   age state  point
# 0    Alice  24.0    NY    NaN
# 2  Charlie   NaN    CA    NaN
# 3     Dave  68.0    TX   70.0
# 4    Ellen   NaN    CA   88.0
# 5    Frank  30.0   NaN    NaN

引数how='any'を指定すると、欠損値が一つでも含まれる行が削除される。デフォルトがhow='any'なので、何も指定しないとこの動作になる。

print(df2.dropna(how='any'))
#    name   age state  point
# 3  Dave  68.0    TX   70.0

print(df2.dropna())
#    name   age state  point
# 3  Dave  68.0    TX   70.0

axis=1とすると列に適用。欠損値が一つでも含まれる列が削除される。

print(df2.dropna(how='any', axis=1))
#       name
# 0    Alice
# 2  Charlie
# 3     Dave
# 4    Ellen
# 5    Frank

欠損値の数に応じて行・列を削除する

引数threshに個数を指定すると含まれる欠損値の数に応じて行・列を削除できる。

例えばthresh=3とすると欠損値が3個以上含まれている行が削除される。

print(df.dropna(thresh=3))
#     name   age state  point  other
# 0  Alice  24.0    NY    NaN    NaN
# 3   Dave  68.0    TX   70.0    NaN
# 4  Ellen   NaN    CA   88.0    NaN

axis=1とすると列に適用。

print(df.dropna(thresh=3, axis=1))
#       name   age state
# 0    Alice  24.0    NY
# 1      NaN   NaN   NaN
# 2  Charlie   NaN    CA
# 3     Dave  68.0    TX
# 4    Ellen   NaN    CA
# 5    Frank  30.0   NaN

特定の行・列に欠損値がある列・行を削除する

特定の行・列を基準に削除したい場合は、引数subsetに対象としたい行ラベル・列ラベルをリストで指定する。リストである必要があるので、対象が一つでもsubset=['name']のように指定する。

デフォルトではsubsetで指定した列に欠損値がある行を削除する。

print(df.dropna(subset=['age']))
#     name   age state  point  other
# 0  Alice  24.0    NY    NaN    NaN
# 3   Dave  68.0    TX   70.0    NaN
# 5  Frank  30.0   NaN    NaN    NaN

複数列を指定した場合、デフォルトでは指定した列のいずれかが欠損値である行をすべて削除する。

print(df.dropna(subset=['age', 'state']))
#     name   age state  point  other
# 0  Alice  24.0    NY    NaN    NaN
# 3   Dave  68.0    TX   70.0    NaN

引数how='all'とすると、指定した列すべてが欠損値である行のみを削除する。

print(df.dropna(subset=['age', 'state'], how='all'))
#       name   age state  point  other
# 0    Alice  24.0    NY    NaN    NaN
# 2  Charlie   NaN    CA    NaN    NaN
# 3     Dave  68.0    TX   70.0    NaN
# 4    Ellen   NaN    CA   88.0    NaN
# 5    Frank  30.0   NaN    NaN    NaN

axis=1とすると、subsetで指定した行に欠損値がある列を削除する。引数howも使える。

print(df.dropna(subset=[0, 4], axis=1))
#       name state
# 0    Alice    NY
# 1      NaN   NaN
# 2  Charlie    CA
# 3     Dave    TX
# 4    Ellen    CA
# 5    Frank   NaN

print(df.dropna(subset=[0, 4], axis=1, how='all'))
#       name   age state  point
# 0    Alice  24.0    NY    NaN
# 1      NaN   NaN   NaN    NaN
# 2  Charlie   NaN    CA    NaN
# 3     Dave  68.0    TX   70.0
# 4    Ellen   NaN    CA   88.0
# 5    Frank  30.0   NaN    NaN

pandas.Seriesの場合

データが一次元のpandas.Seriesの場合、シンプルにdropna()を呼ぶだけでいい。欠損値が削除される。

s = df['age']
print(s)
# 0    24.0
# 1     NaN
# 2     NaN
# 3    68.0
# 4     NaN
# 5    30.0
# Name: age, dtype: float64

print(s.dropna())
# 0    24.0
# 3    68.0
# 5    30.0
# Name: age, dtype: float64

欠損値を置換(穴埋め)する

メソッドfillna()を使うと欠損値を任意の値で置き換えられる。

デフォルトでは新しいオブジェクトを返して元のオブジェクトは変更されないが、引数inplace=Trueを指定すると元のオブジェクト自体が変更される。

先に読み込んだpandas.DataFrameを例とする。

print(df)
#       name   age state  point  other
# 0    Alice  24.0    NY    NaN    NaN
# 1      NaN   NaN   NaN    NaN    NaN
# 2  Charlie   NaN    CA    NaN    NaN
# 3     Dave  68.0    TX   70.0    NaN
# 4    Ellen   NaN    CA   88.0    NaN
# 5    Frank  30.0   NaN    NaN    NaN

なお、単純な置換ではなく前後の値から補間する場合はinterpolate()を使う。以下の記事を参照。

共通の値で一律に置換する

引数に置き換えたい値を指定するとすべての欠損値NaNがその値で置き換わる。

print(df.fillna(0))
#       name   age state  point  other
# 0    Alice  24.0    NY    0.0    0.0
# 1        0   0.0     0    0.0    0.0
# 2  Charlie   0.0    CA    0.0    0.0
# 3     Dave  68.0    TX   70.0    0.0
# 4    Ellen   0.0    CA   88.0    0.0
# 5    Frank  30.0     0    0.0    0.0

列ごとに異なる値で置換する

引数に辞書を指定すると、列ごとに異なる値が代入される。辞書のkeyを列ラベル(列名)、valueを置き換えたい値とする。指定されていない列は欠損値NaNのまま。

print(df.fillna({'name': 'XXX', 'age': 20, 'point': 0}))
#       name   age state  point  other
# 0    Alice  24.0    NY    0.0    NaN
# 1      XXX  20.0   NaN    0.0    NaN
# 2  Charlie  20.0    CA    0.0    NaN
# 3     Dave  68.0    TX   70.0    NaN
# 4    Ellen  20.0    CA   88.0    NaN
# 5    Frank  30.0   NaN    0.0    NaN

辞書だけでなくpandas.Seriesも指定可能。pandas.Seriesのラベルと一致する列ラベル(列名)の列の欠損値がpandas.Seriesの値で置換される。pandas.Seriesのラベルと対応しない列は欠損値のまま。

s_for_fill = pd.Series(['ZZZ', 100], index=['name', 'age'])
print(s_for_fill)
# name    ZZZ
# age     100
# dtype: object

print(df.fillna(s_for_fill))
#       name    age state  point  other
# 0    Alice   24.0    NY    NaN    NaN
# 1      ZZZ  100.0   NaN    NaN    NaN
# 2  Charlie  100.0    CA    NaN    NaN
# 3     Dave   68.0    TX   70.0    NaN
# 4    Ellen  100.0    CA   88.0    NaN
# 5    Frank   30.0   NaN    NaN    NaN

列ごとに平均値、中央値、最頻値などで置換する

列ごとの平均値はmean()メソッドで算出できる。結果はpandas.Series。欠損値は除外して算出される。

print(df.mean())
# age      40.666667
# point    79.000000
# other          NaN
# dtype: float64

このpandas.Seriesfillna()の引数に指定すると、上述のように、対応する列の欠損値が平均値で置換される。

print(df.fillna(df.mean()))
#       name        age state  point  other
# 0    Alice  24.000000    NY   79.0    NaN
# 1      NaN  40.666667   NaN   79.0    NaN
# 2  Charlie  40.666667    CA   79.0    NaN
# 3     Dave  68.000000    TX   70.0    NaN
# 4    Ellen  40.666667    CA   88.0    NaN
# 5    Frank  30.000000   NaN   79.0    NaN

同様に、中央値で置き換えたい場合はmedian()メソッドを使う。偶数個の場合は中央二つの値の平均値が中央値となる。

print(df.fillna(df.median()))
#       name   age state  point  other
# 0    Alice  24.0    NY   79.0    NaN
# 1      NaN  30.0   NaN   79.0    NaN
# 2  Charlie  30.0    CA   79.0    NaN
# 3     Dave  68.0    TX   70.0    NaN
# 4    Ellen  30.0    CA   88.0    NaN
# 5    Frank  30.0   NaN   79.0    NaN

最頻値はmode()メソッド。mode()pandas.DataFrameを返すのでiloc[0]で先頭行をpandas.Seriesとして取得している。

print(df.fillna(df.mode().iloc[0]))
#       name   age state  point  other
# 0    Alice  24.0    NY   70.0    NaN
# 1    Alice  24.0    CA   70.0    NaN
# 2  Charlie  24.0    CA   70.0    NaN
# 3     Dave  68.0    TX   70.0    NaN
# 4    Ellen  24.0    CA   88.0    NaN
# 5    Frank  30.0    CA   70.0    NaN

この例では特に問題ないが、mean()などのメソッドはデフォルトでは数値列だけでなくほかの型の列に対しても処理を試みるので思いもよらない値を返す場合がある。

引数numeric_only=Trueとすると対象が数値列に限定される。なお、その場合もbool型の列はTrue=1, False=0として処理対象となる。

前後の値で置換する

引数methodを使うと指定した値ではなく前後の値で置換できる。

method='ffill'とすると前の値で置き換えられ、method='bfill'とすると後ろの値で置き換えられる。時系列データのときに便利。

print(df.fillna(method='ffill'))
#       name   age state  point  other
# 0    Alice  24.0    NY    NaN    NaN
# 1    Alice  24.0    NY    NaN    NaN
# 2  Charlie  24.0    CA    NaN    NaN
# 3     Dave  68.0    TX   70.0    NaN
# 4    Ellen  68.0    CA   88.0    NaN
# 5    Frank  30.0    CA   88.0    NaN

print(df.fillna(method='bfill'))
#       name   age state  point  other
# 0    Alice  24.0    NY   70.0    NaN
# 1  Charlie  68.0    CA   70.0    NaN
# 2  Charlie  68.0    CA   70.0    NaN
# 3     Dave  68.0    TX   70.0    NaN
# 4    Ellen  30.0    CA   88.0    NaN
# 5    Frank  30.0   NaN    NaN    NaN

最大連続置換回数を指定

引数limitで、最大何回まで連続して置換するかを指定できる。

print(df.fillna(method='bfill', limit=1))
#       name   age state  point  other
# 0    Alice  24.0    NY    NaN    NaN
# 1  Charlie   NaN    CA    NaN    NaN
# 2  Charlie  68.0    CA   70.0    NaN
# 3     Dave  68.0    TX   70.0    NaN
# 4    Ellen  30.0    CA   88.0    NaN
# 5    Frank  30.0   NaN    NaN    NaN

pandas.Seriesの場合

pandas.Seriesの場合もこれまでの例と同様に処理が可能。

s = df['age']
print(s)
# 0    24.0
# 1     NaN
# 2     NaN
# 3    68.0
# 4     NaN
# 5    30.0
# Name: age, dtype: float64

print(s.fillna(100))
# 0     24.0
# 1    100.0
# 2    100.0
# 3     68.0
# 4    100.0
# 5     30.0
# Name: age, dtype: float64

print(s.fillna({1: 100, 4: 0}))
# 0     24.0
# 1    100.0
# 2      NaN
# 3     68.0
# 4      0.0
# 5     30.0
# Name: age, dtype: float64

print(s.fillna(method='bfill', limit=1))
# 0    24.0
# 1     NaN
# 2    68.0
# 3    68.0
# 4    30.0
# 5    30.0
# Name: age, dtype: float64

欠損値を抽出する

特定の行・列に欠損値がある列・行を抽出する

特定の列に欠損値が含まれている行を選択して確認したい場合、列のisnull()メソッドで欠損値がTrue、それ以外がFalseとなるpandas.Seriesを取得し、ブールインデックス参照で抽出する。

print(df)
#       name   age state  point  other
# 0    Alice  24.0    NY    NaN    NaN
# 1      NaN   NaN   NaN    NaN    NaN
# 2  Charlie   NaN    CA    NaN    NaN
# 3     Dave  68.0    TX   70.0    NaN
# 4    Ellen   NaN    CA   88.0    NaN
# 5    Frank  30.0   NaN    NaN    NaN

print(df['point'].isnull())
# 0     True
# 1     True
# 2     True
# 3    False
# 4    False
# 5     True
# Name: point, dtype: bool

print(df[df['point'].isnull()])
#       name   age state  point  other
# 0    Alice  24.0    NY    NaN    NaN
# 1      NaN   NaN   NaN    NaN    NaN
# 2  Charlie   NaN    CA    NaN    NaN
# 5    Frank  30.0   NaN    NaN    NaN

特定の行に欠損値が含まれている列を選択する場合も考え方は同じ。行名(行ラベル)で選択する場合はloc[]、位置で選択する場合はiloc[]を使う。

print(df.iloc[2].isnull())
# name     False
# age       True
# state    False
# point     True
# other     True
# Name: 2, dtype: bool

print(df.loc[:, df.iloc[2].isnull()])
#     age  point  other
# 0  24.0    NaN    NaN
# 1   NaN    NaN    NaN
# 2   NaN    NaN    NaN
# 3  68.0   70.0    NaN
# 4   NaN   88.0    NaN
# 5  30.0    NaN    NaN

欠損値が一つでも含まれる行・列を抽出する

特定の行・列に対する判定ではなく、欠損値が一つでも含まれる行・列を抽出する場合。

すべての値が欠損値である行と列を削除したデータを例とする。

df2 = df.dropna(how='all').dropna(how='all', axis=1)
print(df2)
#       name   age state  point
# 0    Alice  24.0    NY    NaN
# 2  Charlie   NaN    CA    NaN
# 3     Dave  68.0    TX   70.0
# 4    Ellen   NaN    CA   88.0
# 5    Frank  30.0   NaN    NaN

pandas.DataFrameisnull()メソッドは各要素が欠損値かどうかを判定しTrue, Falseとするpandas.DataFrameを返す。

print(df2.isnull())
#     name    age  state  point
# 0  False  False  False   True
# 2  False   True  False   True
# 3  False  False  False  False
# 4  False   True  False  False
# 5  False  False   True   True

これに対してany()メソッドを適用しブールインデックス参照で抽出する。

anyメソッドは行または列に一つでもTrueが含まれているとTrueを返す。引数axis=1とすると行に対する処理となる。

print(df2.isnull().any(axis=1))
# 0     True
# 2     True
# 3    False
# 4     True
# 5     True
# dtype: bool

print(df2[df2.isnull().any(axis=1)])
#       name   age state  point
# 0    Alice  24.0    NY    NaN
# 2  Charlie   NaN    CA    NaN
# 4    Ellen   NaN    CA   88.0
# 5    Frank  30.0   NaN    NaN

列を抽出する場合も同様。any()の引数axis=0とすると列に対する処理となる。デフォルトがaxis=0なので省略可能。

print(df2.isnull().any())
# name     False
# age       True
# state     True
# point     True
# dtype: bool

print(df2.loc[:, df2.isnull().any()])
#     age state  point
# 0  24.0    NY    NaN
# 2   NaN    CA    NaN
# 3  68.0    TX   70.0
# 4   NaN    CA   88.0
# 5  30.0   NaN    NaN
スポンサーリンク
シェア
このエントリーをはてなブックマークに追加

関連カテゴリー

関連記事