pandasで欠損値NaNを前後の値から補間するinterpolate

Modified: | Tags: Python, pandas

pandas.DataFrame, pandas.Seriesの欠損値NaNを前後の値から補間するにはinterpolate()メソッドを使う。

欠損値NaNを削除したり特定の値で穴埋めする場合はdropna(), fillna()を使う。以下の記事を参照。

本記事のサンプルコードのpandasのバージョンは以下の通り。バージョンによって仕様が異なる可能性があるので注意。説明のため、NumPyも使う。

import pandas as pd
import numpy as np

print(pd.__version__)
# 2.0.3

interpolate()の基本的な使い方

以下のpandas.DataFrameを例とする。

df = pd.DataFrame({'col1': [0, np.nan, np.nan, 3, 4],
                   'col2': [np.nan, 1, 2, np.nan, np.nan],
                   'col3': [4, np.nan, np.nan, 7, 10]})
print(df)
#    col1  col2  col3
# 0   0.0   NaN   4.0
# 1   NaN   1.0   NaN
# 2   NaN   2.0   NaN
# 3   3.0   NaN   7.0
# 4   4.0   NaN  10.0

デフォルトでは各列に対して線形補間を行う。下端の欠損値には同じ値が繰り返される。上端の欠損値はそのまま。

print(df.interpolate())
#    col1  col2  col3
# 0   0.0   NaN   4.0
# 1   1.0   1.0   5.0
# 2   2.0   2.0   6.0
# 3   3.0   2.0   7.0
# 4   4.0   2.0  10.0

以下、引数の設定について説明する。基本的にはpandas.Seriesでも同じ。

第一引数methodで指定する補間方法については後述。

行 or 列を指定: 引数axis

引数axis=1とすると各行に対して補間される。右端の欠損値には同じ値が繰り返される。左端の欠損値はそのまま。

print(df.interpolate(axis=1))
#    col1  col2  col3
# 0   0.0   2.0   4.0
# 1   NaN   1.0   1.0
# 2   NaN   2.0   2.0
# 3   3.0   5.0   7.0
# 4   4.0   7.0  10.0

補間する連続欠損値の最大数を指定: 引数limit

欠損値が連続している場合、最大でいくつの欠損値を補間するかを引数limitで指定できる。デフォルトはNoneで、連続する欠損値すべてが補間される。

print(df.interpolate(limit=1))
#    col1  col2  col3
# 0   0.0   NaN   4.0
# 1   1.0   1.0   5.0
# 2   NaN   2.0   NaN
# 3   3.0   2.0   7.0
# 4   4.0   NaN  10.0

補間方向を指定: 引数limit_direction

補間方向は引数limit_direction'forward', 'backward', 'both'のいずれかを指定する。

print(df.interpolate(limit=1, limit_direction='forward'))
#    col1  col2  col3
# 0   0.0   NaN   4.0
# 1   1.0   1.0   5.0
# 2   NaN   2.0   NaN
# 3   3.0   2.0   7.0
# 4   4.0   NaN  10.0

print(df.interpolate(limit=1, limit_direction='backward'))
#    col1  col2  col3
# 0   0.0   1.0   4.0
# 1   NaN   1.0   NaN
# 2   2.0   2.0   6.0
# 3   3.0   NaN   7.0
# 4   4.0   NaN  10.0

print(df.interpolate(limit=1, limit_direction='both'))
#    col1  col2  col3
# 0   0.0   1.0   4.0
# 1   1.0   1.0   5.0
# 2   2.0   2.0   6.0
# 3   3.0   2.0   7.0
# 4   4.0   NaN  10.0

上述のように、デフォルトでは上端(または左端)の欠損値はそのままとなるが、limit_direction='both'とすると両方とも補間される。

print(df.interpolate(limit_direction='both'))
#    col1  col2  col3
# 0   0.0   1.0   4.0
# 1   1.0   1.0   5.0
# 2   2.0   2.0   6.0
# 3   3.0   2.0   7.0
# 4   4.0   2.0  10.0

内挿のみ or 外挿のみ or 両方を指定: 引数limit_area

補間対象領域は引数limit_areaで指定する。

'inside'だと内挿のみ、'outside'だと外挿のみ、None(デフォルト)だと両方が対象となる。外挿については上述のlimit_directionで前方(上側・左側)、後方(下側・右側)、両方を指定できる。

print(df.interpolate(limit_area='inside'))
#    col1  col2  col3
# 0   0.0   NaN   4.0
# 1   1.0   1.0   5.0
# 2   2.0   2.0   6.0
# 3   3.0   NaN   7.0
# 4   4.0   NaN  10.0

print(df.interpolate(limit_area='outside'))
#    col1  col2  col3
# 0   0.0   NaN   4.0
# 1   NaN   1.0   NaN
# 2   NaN   2.0   NaN
# 3   3.0   2.0   7.0
# 4   4.0   2.0  10.0

print(df.interpolate(limit_area='outside', limit_direction='both'))
#    col1  col2  col3
# 0   0.0   1.0   4.0
# 1   NaN   1.0   NaN
# 2   NaN   2.0   NaN
# 3   3.0   2.0   7.0
# 4   4.0   2.0  10.0

なお、便宜上「外挿」という言葉を使っているが、上の結果からも分かるように、線形補間(デフォルト)では外側の値は端の値の繰り返しとなり線形外挿はされない。後述のスプライン補間では外側の値も繰り返しではなく外挿された値となる。

オブジェクト自体を更新するかを指定: 引数inplace

ほかの多くのメソッドと同様、引数inplaceでオブジェクト自体を更新するかどうかを指定できる。

df.interpolate(inplace=True)
print(df)
#    col1  col2  col3
# 0   0.0   NaN   4.0
# 1   1.0   1.0   5.0
# 2   2.0   2.0   6.0
# 3   3.0   2.0   7.0
# 4   4.0   2.0  10.0

補間方法: 引数method

補間方法は第一引数methodに指定する。デフォルトはmethod='linear'で線形補間。

線形補間: linear, index, values

method='linear'(デフォルト)ではインデックス列は考慮されないが、method='index'またはmethod='values'とするとインデックス列を考慮して補間される。インデックス列をY軸、対象の列をX軸として線形補間するようなイメージ。

s = pd.Series([0, np.nan, np.nan, 3],
              index=[0, 4, 6, 8])
print(s)
# 0    0.0
# 4    NaN
# 6    NaN
# 8    3.0
# dtype: float64

print(s.interpolate())
# 0    0.0
# 4    1.0
# 6    2.0
# 8    3.0
# dtype: float64

print(s.interpolate('index'))
# 0    0.00
# 4    1.50
# 6    2.25
# 8    3.00
# dtype: float64

デフォルトのmethod='linear'はインデックス列が文字列でもよいが、method='index'またはmethod='values'だとエラーとなる。

s.index = list('abcd')
print(s)
# a    0.0
# b    NaN
# c    NaN
# d    3.0
# dtype: float64

print(s.interpolate())
# a    0.0
# b    1.0
# c    2.0
# d    3.0
# dtype: float64

# print(s.interpolate('index'))
# TypeError: Cannot cast array data from dtype('O') to dtype('float64') according to the rule 'safe'

前後の値をそのまま使用: ffill, pad, bfill, backfill

method='ffill'またはmethod='pad'だと前のNaNではない値、method='bfill'またはmethod='backfill'だと後ろのNaNではない値で埋められる。

s = pd.Series([np.nan, 1, np.nan, 2, np.nan])
print(s)
# 0    NaN
# 1    1.0
# 2    NaN
# 3    2.0
# 4    NaN
# dtype: float64

print(s.interpolate('ffill'))
# 0    NaN
# 1    1.0
# 2    1.0
# 3    2.0
# 4    2.0
# dtype: float64

print(s.interpolate('bfill'))
# 0    1.0
# 1    1.0
# 2    2.0
# 3    2.0
# 4    NaN
# dtype: float64

method=''ffill', 'pad'のときはlimit_direction='forward'method=''bfill', 'backfill'のときはlimit_direction='backward'でなければならない。

# s.interpolate('ffill', limit_direction='both')
# ValueError: `limit_direction` must be 'forward' for method `ffill`

# s.interpolate('bfill', limit_direction='both')
# ValueError: `limit_direction` must be 'backward' for method `bfill`

fillna()メソッドで引数methodを指定しても同様の処理が可能。

print(s.fillna(method='ffill'))
# 0    NaN
# 1    1.0
# 2    1.0
# 3    2.0
# 4    2.0
# dtype: float64

print(s.fillna(method='bfill'))
# 0    1.0
# 1    1.0
# 2    2.0
# 3    2.0
# 4    NaN
# dtype: float64

スプライン補間: spline

method='spline'とするとスプライン補間。同時に引数orderに次数を指定する必要がある。

s = pd.Series([0, 10, np.nan, np.nan, 4, np.nan],
              index=[0, 2, 5, 6, 8, 12])
print(s)
# 0      0.0
# 2     10.0
# 5      NaN
# 6      NaN
# 8      4.0
# 12     NaN
# dtype: float64

print(s.interpolate('spline', order=2))
# 0      0.00
# 2     10.00
# 5     13.75
# 6     12.00
# 8      4.00
# 12   -30.00
# dtype: float64

スプライン補間は常にインデックス列を考慮して補間される。インデックスが変わると補間結果も変わる。

s.index = range(6)
print(s)
# 0     0.0
# 1    10.0
# 2     NaN
# 3     NaN
# 4     4.0
# 5     NaN
# dtype: float64

print(s.interpolate('spline', order=2))
# 0     0.0
# 1    10.0
# 2    14.0
# 3    12.0
# 4     4.0
# 5   -10.0
# dtype: float64

したがって、スプライン補間する場合はインデックス列が数値である必要がある。文字列だとエラー。

s.index = list('abcdef')
print(s)
# a     0.0
# b    10.0
# c     NaN
# d     NaN
# e     4.0
# f     NaN
# dtype: float64

# print(s.interpolate('spline', order=2))
# ValueError: Index column must be numeric or datetime type when using spline method other than linear.
# Try setting a numeric or datetime index column before interpolating.

その他

引数methodに指定できる補間方法としては、そのほか、'nearest', 'zero', 'slinear', 'quadratic', 'cubic', 'barycentric', 'krogh', 'polynomial', 'piecewise_polynomial', 'from_derivatives', 'pchip', 'akima'がある。

公式ドキュメントにもあるように、上述のスプライン補間('spline')も含めて、これらはSciPyの関数に渡される。

いずれの場合も上述のスプライン補間と同様にインデックスが数値である必要がある。

時系列データの補間

時系列データについては専用の補間方法としてmethod='time'が用意されている。method='time'の場合、インデックス列の日時に合わせて線形補間される。

df_nan = pd.DataFrame({'value': [1, np.nan, np.nan, np.nan, 31]},
                      index=pd.to_datetime(['2018-01-01', '2018-01-02', '2018-01-15', '2018-01-20', '2018-01-31']))

print(df_nan)
#             value
# 2018-01-01    1.0
# 2018-01-02    NaN
# 2018-01-15    NaN
# 2018-01-20    NaN
# 2018-01-31   31.0

print(df_nan.interpolate())
#             value
# 2018-01-01    1.0
# 2018-01-02    8.5
# 2018-01-15   16.0
# 2018-01-20   23.5
# 2018-01-31   31.0

print(df_nan.interpolate('time'))
#             value
# 2018-01-01    1.0
# 2018-01-02    2.0
# 2018-01-15   15.0
# 2018-01-20   20.0
# 2018-01-31   31.0

時系列データのリサンプリングについては以下の記事を参照。

データ型dtypeがobjectの場合(文字列など)

例えば、要素として文字列を含む列のデータ型dtypeobjectとなる。

s_object = pd.Series(['A', np.nan, 'C'])
print(s_object)
# 0      A
# 1    NaN
# 2      C
# dtype: object

object列はデフォルトのmethod='linear'などでは補間できずNaNのまま。ffill, pad, bfill, backfillといった前後の値をそのまま使う方法の場合は埋められる。

print(s_object.interpolate())
# 0      A
# 1    NaN
# 2      C
# dtype: object

print(s_object.interpolate('ffill'))
# 0    A
# 1    A
# 2    C
# dtype: object

要素が数値でもobject列の場合は同様。

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

print(s_object_num.interpolate())
# 0      0
# 1    NaN
# 2      2
# dtype: object

print(s_object_num.interpolate('ffill'))
# 0    0
# 1    0
# 2    2
# dtype: int64

astype()floatに変換すれば線形補間などが可能。NaNを含んでいるとintには変換できないので注意。

print(s_object_num.astype(float).interpolate())
# 0    0.0
# 1    1.0
# 2    2.0
# dtype: float64

# print(s_object_num.astype(int))
# ValueError: cannot convert float NaN to integer

関連カテゴリー

関連記事