実データには欠損値(NaN)がつきものです。センサーの故障、通信エラー、データ収集の失敗など、さまざまな理由でデータが欠落します。pandas には欠損値を補間するための多様な方法が用意されています。
欠損値の確認
まず、欠損値を含むサンプルデータを作成します。
import pandas as pd
import numpy as np
# 欠損値を含むデータ
df = pd.DataFrame({
'date': pd.date_range('2024-01-01', periods=10, freq='D'),
'value': [100, np.nan, np.nan, 103, 105, np.nan, 108, 110, np.nan, 115]
}).set_index('date')
print(df)
# value
# date
# 2024-01-01 100.0
# 2024-01-02 NaN
# 2024-01-03 NaN
# 2024-01-04 103.0
# 2024-01-05 105.0
# 2024-01-06 NaN
# 2024-01-07 108.0
# 2024-01-08 110.0
# 2024-01-09 NaN
# 2024-01-10 115.0
欠損値の数や位置を確認するには、いくつかの方法があります。
# 欠損値の数
print(df.isna().sum())
# value 4
# 欠損値がある行
print(df[df['value'].isna()])
# 欠損値の割合
print(df.isna().mean())
# value 0.4
前方補間(ffill)と後方補間(bfill)
最もシンプルな補間方法は、直前または直後の有効な値で埋める方法です。
# 前方補間:直前の値で埋める
df['ffill'] = df['value'].ffill()
# 後方補間:直後の値で埋める
df['bfill'] = df['value'].bfill()
print(df)
# value ffill bfill
# date
# 2024-01-01 100.0 100.0 100.0
# 2024-01-02 NaN 100.0 103.0
# 2024-01-03 NaN 100.0 103.0
# 2024-01-04 103.0 103.0 103.0
# ...
連続する欠損値が多い場合、ffill だと同じ値が長く続いてしまいます。limit パラメータで補間する最大数を制限できます。
# 最大1つまで前方補間
df['ffill_limit'] = df['value'].ffill(limit=1)
print(df)
# value ffill_limit
# date
# 2024-01-01 100.0 100.0
# 2024-01-02 NaN 100.0 # 補間される
# 2024-01-03 NaN NaN # 制限超過で補間されない
# 2024-01-04 103.0 103.0
# ...
時系列データで「最新の既知の値を使う」場合に適している。例:センサー故障時に直前の値を使う。
データを遡って処理する場合に使う。リアルタイム処理では使えない(未来の値を参照するため)。
線形補間(interpolate)
前後の値から線形に補間する方法です。より自然な補間結果が得られます。
# 線形補間
df['linear'] = df['value'].interpolate(method='linear')
print(df[['value', 'ffill', 'linear']])
# value ffill linear
# date
# 2024-01-01 100.0 100.0 100.0
# 2024-01-02 NaN 100.0 101.0 # (100+103)/2 に近い値
# 2024-01-03 NaN 100.0 102.0
# 2024-01-04 103.0 103.0 103.0
# ...
interpolate には多くの補間方法があります。
| method | 説明 |
|---|---|
| linear | 線形補間(デフォルト) |
| time | 時間間隔を考慮した線形補間 |
| index | インデックス値を考慮した補間 |
| pad / ffill | 前方補間 |
| nearest | 最も近い値で補間 |
| polynomial | 多項式補間 |
| spline | スプライン補間 |
時間ベースの補間
不規則な間隔の時系列データでは、method='time' を使うと時間間隔を考慮した補間ができます。
# 不規則な間隔のデータ
irregular = pd.DataFrame({
'value': [100, np.nan, 200]
}, index=pd.to_datetime(['2024-01-01', '2024-01-02', '2024-01-11']))
# 線形補間(インデックス位置ベース)
irregular['linear'] = irregular['value'].interpolate(method='linear')
# 時間補間(時間間隔ベース)
irregular['time'] = irregular['value'].interpolate(method='time')
print(irregular)
# value linear time
# 2024-01-01 100.0 100.0 100.0
# 2024-01-02 NaN 150.0 110.0 # 時間では1/10の位置
# 2024-01-11 200.0 200.0 200.0
method='linear' はインデックスの位置(0, 1, 2)で補間するため中間値 150 になりますが、method='time' は時間間隔を考慮して 110 に近い値になります。
スプライン補間
滑らかな曲線で補間したい場合はスプライン補間が有効です。
# スプライン補間(3次)
df['spline'] = df['value'].interpolate(method='spline', order=3)
# 多項式補間(2次)
df['poly'] = df['value'].interpolate(method='polynomial', order=2)
スプライン補間を使うには scipy がインストールされている必要があります。order パラメータで多項式の次数を指定します。
シンプルで高速。ほとんどの場合はこれで十分。
滑らかな曲線が必要な場合に使う。ただしデータの端で不安定になることがある。
特定の値で埋める
補間ではなく、固定値で埋めたい場合は fillna を使います。
# 0で埋める
df['zero'] = df['value'].fillna(0)
# 平均値で埋める
df['mean'] = df['value'].fillna(df['value'].mean())
# 中央値で埋める
df['median'] = df['value'].fillna(df['value'].median())
グループごとの平均値で埋めることもできます。
# カテゴリ付きデータ
df = pd.DataFrame({
'category': ['A', 'A', 'A', 'B', 'B', 'B'],
'value': [10, np.nan, 12, 100, np.nan, 110]
})
# グループ別の平均値で埋める
df['filled'] = df.groupby('category')['value'].transform(
lambda x: x.fillna(x.mean())
)
print(df)
# category value filled
# 0 A 10.0 10.0
# 1 A NaN 11.0 # Aグループの平均
# 2 A 12.0 12.0
# 3 B 100.0 100.0
# 4 B NaN 105.0 # Bグループの平均
# 5 B 110.0 110.0
欠損値を削除する
補間が適切でない場合、欠損値を含む行を削除する選択肢もあります。
# 欠損値を含む行を削除
df_clean = df.dropna()
# 特定の列に欠損値がある行を削除
df_clean = df.dropna(subset=['value'])
# 全ての列が欠損の行のみ削除
df_clean = df.dropna(how='all')
# 閾値以上のデータがある行を残す
df_clean = df.dropna(thresh=2) # 2列以上に値があれば残す
補間方法の選び方
補間方法の選択は、データの性質と用途によって異なります。
センサーデータ
ffill または線形補間
直前の値を使うか、前後から推測
金融データ
ffill(直前の価格を使用)
ゼロや平均値での補間は不適切
科学データ
スプライン補間
滑らかな連続性が必要
欠損値の補間は分析結果に大きく影響します。データの欠損パターン(ランダムか、特定の条件で発生するか)を理解し、適切な補間方法を選ぶことが重要です。また、補間後のデータが元のデータの特性(分布、相関など)を大きく変えていないか確認することも大切です。