ここまでの第5〜7回では、Titanic データのような 順序を持たないデータ の補完を扱ってきました。
第5回:単変量補完① — 定数・任意値での補完(pandas と SimpleImputer)
https://www.salesanalytics.co.jp/datascience/datascience339/第6回:単変量補完② — 平均・中央値・最頻値での補完
https://www.salesanalytics.co.jp/datascience/datascience340/第7回:単変量補完③ — ランダムサンプル補完で分布を保つ
https://www.salesanalytics.co.jp/datascience/datascience341/
今回は少し趣を変えて、時系列データ(time series data) の欠損値補完を取り上げます。
時系列データとは、株価・気温・売上のように 時間の順序に沿って並んだデータ のことです。
「昨日の値と今日の値は近い」「全体に増加トレンドがある」といった 時間的な構造 を持つため、これまでの手法(平均値補完など)をそのまま使うと、その構造を壊してしまいます。
時系列特有の補完手法である LOCF・NOCB・線形補間 を、サンプルデータを使って一つずつ可視化しながら学んでいきましょう。
Contents
なぜ時系列データは特別なのか
これまで扱ってきた手法と、時系列データの違いを整理しておきます。
| 観点 | 通常のデータ(Titanic等) | 時系列データ |
|---|---|---|
| 行の順序 | 意味を持たない(並べ替え可) | 時間順で意味を持つ(並べ替え不可) |
| 隣接する値の関係 | 特に関係なし | 隣の時点と強く相関する |
| 補完の発想 | 列全体の代表値や分布を使う | 前後の時点の値 |
たとえば、ある日の気温が欠損していたら、「全期間の平均気温」で埋めるより「前日の気温」や「前後の気温の中間」で埋めるほうが、はるかに自然です。
これが時系列補完の基本的な考え方です。
サンプルデータの作成
時系列の欠損補完を学ぶため、週次の売上データを人工的に作成します。
以下、コードです。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# 2023年の週次の日付インデックスを作成
date_range = pd.date_range(
start='2023-01-01',
end='2023-12-31',
freq='W'
)
# 再現性のため乱数の種を固定
np.random.seed(42)
# 12000〜15000のランダムな売上データを生成
sales = np.random.uniform(
12000,
15000,
size=len(date_range)
).round(2)
# データフレームにまとめ、日付をインデックスに設定
ts = pd.DataFrame({
'Date': date_range,
'Sales': sales
})
ts.set_index('Date', inplace=True)
print(f"データ件数: {len(ts)}件")
print(ts.head())
pd.date_range(start, end, freq='W'):指定期間の 週次(Weekly) の日付を生成しますnp.random.uniform(12000, 15000, ...):12000〜15000の一様乱数で売上を生成ts.set_index('Date', inplace=True):日付列をインデックスにすることで「時系列データ」になります
以下、実行結果です。
データ件数: 53件
Sales
Date
2023-01-01 13123.62
2023-01-08 14852.14
2023-01-15 14195.98
2023-01-22 13795.98
2023-01-29 12468.06
2023年の各週の売上が並んだ時系列データができあがりました。
このデータに、ランダムな位置で欠損を作ります。
以下、コードです。
ts_missing = ts.copy()
# ランダムに10箇所を欠損させる
np.random.seed(42)
n_missing = 10
missing_idx = np.random.choice(
len(ts_missing),
size=n_missing,
replace=False
)
ts_missing.iloc[missing_idx, 0] = np.nan
print(f"欠損数: {ts_missing['Sales'].isnull().sum()}件")
print("\n末尾11週の様子:")
print(ts_missing['Sales'].tail(11))
np.random.choice(..., replace=False):重複なしで10箇所のインデックスを選ぶts_missing.iloc[missing_idx, 0] = np.nan:選んだ位置を欠損にする
以下、実行結果です。
欠損数: 10件 末尾11週の様子: Date 2023-10-22 12103.17 2023-10-29 NaN 2023-11-05 12776.34 2023-11-12 13987.57 2023-11-19 12935.13 2023-11-26 NaN 2023-12-03 13640.13 2023-12-10 12554.56 2023-12-17 NaN 2023-12-24 14325.40 2023-12-31 14818.50 Name: Sales, dtype: float64
実行すると、10箇所が NaN になった時系列データができます。
末尾11週を見ると、いくつかの週で売上が欠損していることが確認できます。
ダメな方法:ゼロ埋め・平均埋め(比較用)
まずは、これまでの回で学んだ「時系列を無視した方法」を試して、その問題点を可視化します。
一つ目は、欠損をゼロで埋めいく方法です。
以下、コードです。
# 欠損付きデータ
ts_zero = ts.copy()
ts_zero.iloc[missing_idx, 0] = np.nan
# 欠損値をゼロで埋める
ts_zero['Sales'] = ts_zero['Sales'].fillna(0)
# グラフで可視化
fig, ax = plt.subplots(figsize=(11, 4))
# 元のデータのプロット
ax.plot(
ts.index,
ts['Sales'],
color='dodgerblue',
label='Original',
linewidth=1.5
)
# 欠損値をゼロで埋めた場合のプロット
ax.plot(
ts_zero.index,
ts_zero['Sales'],
color='darkorange',
label='Filled with 0',
linestyle='--',
linewidth=1.5
)
ax.set_title('Fill NaN with 0 (Bad Example)')
ax.set_xlabel('Date')
ax.set_ylabel('Sales ($)')
ax.legend()
plt.tight_layout()
plt.show()
fillna(0):欠損をゼロで埋めるax.plot(...):折れ線グラフで元データと補完後を重ねて描画
以下、実行結果です。

欠損箇所だけ グラフが0まで急降下する 不自然なギザギザが現れます。
売上が12000〜15000の範囲なのに、欠損週だけ0になるのは明らかにおかしいですね。
これは時系列データに対して 絶対にやってはいけない補完 の典型例です。
二つ目は、欠損を平均で埋めいく方法です。
以下、コードです。
# 欠損付きデータ
ts_mean = ts.copy()
ts_mean.iloc[missing_idx, 0] = np.nan
# 全体の平均値を計算
avg_sales = ts_mean['Sales'].mean()
# 欠損値を平均値で埋める
ts_mean['Sales'] = ts_mean['Sales'].fillna(avg_sales)
# グラフで可視化
fig, ax = plt.subplots(figsize=(11, 4))
# 元のデータのプロット
ax.plot(
ts.index,
ts['Sales'],
color='dodgerblue',
label='Original',
linewidth=1.5
)
# 欠損値を平均値で埋めた場合のプロット
ax.plot(
ts_mean.index,
ts_mean['Sales'],
color='darkorange',
label='Filled with Mean',
linestyle='--',
linewidth=1.5
)
ax.set_title('Fill NaN with Mean')
ax.set_xlabel('Date')
ax.set_ylabel('Sales ($)')
ax.legend()
plt.tight_layout()
plt.show()
以下、実行結果です。

欠損箇所が すべて同じ高さ(平均値) に揃ってしまい、その週だけ平らになります。
ゼロ埋めよりはマシですが、前後の値とのつながりが不自然です。
時系列データには、前後の値を活かす補完が必要 だということがよくわかります。
LOCF(Last Observation Carried Forward)
LOCF は「直前の観測値をそのまま引き継ぐ」方法です。
「Last Observation Carried Forward(最後の観測値を前方に運ぶ)」の略で、欠損が出たら、その直前の値で埋めます。
pandas では ffill()(forward fill)メソッドで実装します。
以下、コードです。
# 欠損付きデータ
ts_locf = ts.copy()
ts_locf.iloc[missing_idx, 0] = np.nan
# LOCFで欠損値を補完
ts_locf['Sales'] = ts_locf['Sales'].ffill()
# グラフで可視化
fig, ax = plt.subplots(figsize=(11, 4))
# 元のデータのプロット
ax.plot(
ts.index,
ts['Sales'],
color='dodgerblue',
label='Original',
linewidth=1.5
)
# LOCFで補完したデータのプロット
ax.plot(
ts_locf.index,
ts_locf['Sales'],
color='darkorange',
label='LOCF (ffill)',
linestyle='--',
linewidth=1.5
)
ax.set_title('LOCF - Fill NaN with ffill')
ax.set_xlabel('Date')
ax.set_ylabel('Sales ($)')
ax.legend()
plt.tight_layout()
plt.show()
# 補完後の値を確認
print(ts_locf['Sales'].tail(11))
ffill():直前の有効な値で欠損を埋める(forward fill)- グラフでは、欠損箇所が 直前の値の高さで横ばい になる
以下、実行結果です。

Date 2023-10-22 12103.17 2023-10-29 12103.17 2023-11-05 12776.34 2023-11-12 13987.57 2023-11-19 12935.13 2023-11-26 12935.13 2023-12-03 13640.13 2023-12-10 12554.56 2023-12-17 12554.56 2023-12-24 14325.40 2023-12-31 14818.50 Name: Sales, dtype: float64
欠損箇所が直前の値のまま 階段状(水平に伸びる) になります。
値が急に変わらず、直前の状態が続くと仮定する手法です。
NOCB(Next Observation Carried Backward)
NOCB は LOCF の逆で、「直後の観測値を後方に引き継ぐ」方法です。
「Next Observation Carried Backward(次の観測値を後方に運ぶ)」の略で、欠損が出たら、その直後の値で埋めます。
pandas では bfill()(backward fill)メソッドで実装します。
以下、コードです。
ts_nocb = ts.copy()
ts_nocb.iloc[missing_idx, 0] = np.nan
fig, ax = plt.subplots(figsize=(11, 4))
ax.plot(ts.index, ts['Sales'], color='dodgerblue',
label='Original', linewidth=1.5)
ax.plot(ts_nocb.index, ts_nocb['Sales'].bfill(),
color='darkorange', label='NOCB (bfill)', linewidth=1.5)
ax.set_title('NOCB - Fill NaN with bfill')
ax.set_xlabel('Date')
ax.set_ylabel('Sales ($)')
ax.legend()
plt.tight_layout()
plt.show()
ts_nocb['Sales'] = ts_nocb['Sales'].bfill()
print(ts_nocb['Sales'].tail(11))
bfill():直後の有効な値で欠損を埋める(backward fill)- グラフでは、欠損箇所が 直後の値の高さ になる
以下、実行結果です。

Date 2023-10-22 12103.17 2023-10-29 12776.34 2023-11-05 12776.34 2023-11-12 13987.57 2023-11-19 12935.13 2023-11-26 13640.13 2023-12-03 13640.13 2023-12-10 12554.56 2023-12-17 14325.40 2023-12-24 14325.40 2023-12-31 14818.50 Name: Sales, dtype: float64
LOCF とは逆向きの階段状になります。
線形補間(Linear Interpolation)
線形補間 は、欠損の 前後の値を直線で結び、その直線上の値で欠損を埋める方法です。
LOCF・NOCB のような階段状ではなく、滑らかにつながる のが特徴です。
pandas では interpolate(method='linear') で実装します。
以下、コードです。
# 欠損付きデータ
ts_interp = ts.copy()
ts_interp.iloc[missing_idx, 0] = np.nan
# 線形補間で欠損値を補完
ts_interp['Sales'] = ts_interp['Sales'].interpolate(method='linear')
# グラフで可視化
fig, ax = plt.subplots(figsize=(11, 4))
# 元のデータのプロット
ax.plot(
ts.index,
ts['Sales'],
color='dodgerblue',
label='Original',
linewidth=1.5
)
# 線形補間で補完したデータのプロット
ax.plot(
ts_interp.index,
ts_interp['Sales'],
color='darkorange',
label='Linear Interpolation',
linestyle='--',
linewidth=1.5
)
ax.set_title('Fill NaN with Linear Interpolation')
ax.set_xlabel('Date')
ax.set_ylabel('Sales ($)')
ax.legend()
plt.tight_layout()
plt.show()
# 補完後の値を確認
print(ts_interp['Sales'].tail(11))
interpolate(method='linear'):前後の値を直線で結んで補完- 連続して欠損がある場合も、両端の値を結ぶ直線上に等間隔で配置される
以下、実行結果です。

Date 2023-10-22 12103.170 2023-10-29 12439.755 2023-11-05 12776.340 2023-11-12 13987.570 2023-11-19 12935.130 2023-11-26 13287.630 2023-12-03 13640.130 2023-12-10 12554.560 2023-12-17 13439.980 2023-12-24 14325.400 2023-12-31 14818.500 Name: Sales, dtype: float64
欠損箇所が前後の値を結ぶ なだらかな線 で埋められます。
3手法の中では最も自然な見た目になることが多く、トレンドや連続性のあるデータに適しています。
4手法を一覧で比較する
最後に、LOCF・NOCB・線形補間・平均埋めを 1枚のグラフに並べて 比較しましょう。
それぞれの挙動の違いが一目でわかります。
以下、コードです。
# 欠損付きデータ
ts_cmp = ts.copy()
ts_cmp.iloc[missing_idx, 0] = np.nan
methods = {
'Mean Fill': ts_cmp['Sales'].fillna(ts_cmp['Sales'].mean()),
'LOCF (ffill)': ts_cmp['Sales'].ffill(),
'NOCB (bfill)': ts_cmp['Sales'].bfill(),
'Linear Interp.': ts_cmp['Sales'].interpolate(method='linear'),
}
# 1枚のグラフに全ての補完方法をまとめて可視化
fig, ax = plt.subplots(figsize=(11, 6))
# 元データ
ax.plot(
ts.index,
ts['Sales'],
color='gray',
label='Original',
linewidth=1.0,
alpha=0.9
)
# 各補完方法を同じグラフに重ねて表示
colors = {
'Mean Fill': 'tab:orange',
'LOCF (ffill)': 'tab:green',
'NOCB (bfill)': 'tab:red',
'Linear Interp.': 'tab:blue',
}
for name, filled in methods.items():
ax.plot(
ts_cmp.index,
filled,
label=name,
color=colors[name],
linewidth=1.5,
linestyle='--',
alpha=0.85
)
# 欠損箇所を強調表示
missing_dates = ts_cmp.index[ts_cmp['Sales'].isna()]
ax.scatter(
missing_dates,
ts.loc[missing_dates, 'Sales'],
color='red',
label='Missing Positions',
s=60,
marker='x',
zorder=10
)
ax.set_title('Comparison of Missing Value Imputation Methods')
ax.set_xlabel('Date')
ax.set_ylabel('Sales ($)')
ax.grid(True, alpha=0.3)
ax.legend(
loc='best',
bbox_to_anchor=(1, 1),
)
plt.show()
実行すると、4つのグラフが並びます。
- Mean Fill:欠損箇所だけ平均値の高さに飛ぶ(前後と不連続)
- LOCF:直前の値で水平に伸びる(階段状)
- NOCB:直後の値で水平に伸びる(逆向きの階段状)
- Linear Interp.:前後を直線で結び、最も滑らか
以下、実行結果です。

この比較から、時系列データには線形補間や LOCF/NOCB が適しており、平均埋めは不向き だということが視覚的に理解できます。
トレンドのあるデータでの注意点
ここまでのサンプルデータは、ランダムに変動するだけで明確なトレンド(増加・減少傾向)がありませんでした。
しかし、右肩上がりのトレンドがあるデータ では、手法選びがより重要になります。
以下、コードです。
# 増加トレンドのある時系列を作成
np.random.seed(0)
trend = np.linspace(10000, 20000, 52) # 1万から2万へ増加
noise = np.random.normal(0, 500, 52)
trend_sales = trend + noise
date_idx = pd.date_range(start='2023-01-01', periods=52, freq='W')
ts_trend = pd.DataFrame({'Sales': trend_sales}, index=date_idx)
# 中間に連続した欠損を作る
ts_trend.iloc[15:30, 0] = np.nan
# グラフで可視化
fig, ax = plt.subplots(figsize=(11, 4))
# 元のデータのプロット
ax.plot(
ts_trend.index,
trend + noise,
color='lightgray',
label='True (hidden)',
linewidth=2
)
# LOCFで補完したデータのプロット
ax.plot(
ts_trend.index,
ts_trend['Sales'].ffill(),
color='darkorange',
label='LOCF',
linestyle='--',
linewidth=1.5
)
# 線形補間で補完したデータのプロット
ax.plot(
ts_trend.index,
ts_trend['Sales'].interpolate(method='linear'),
color='seagreen',
label='Linear Interp.',
linestyle='--',
linewidth=1.5
)
ax.set_title('Imputation on Trending Data')
ax.set_xlabel('Date')
ax.set_ylabel('Sales ($)')
ax.legend()
plt.tight_layout()
plt.show()
np.linspace(10000, 20000, 52):1万から2万へ直線的に増加するトレンドを作成- 15〜30週目に連続した欠損を作り、LOCF と線形補間の違いを見る
以下、実行結果です。

増加トレンドのある区間で、次のようになっています。
- LOCF(オレンジ):欠損区間で値が 横ばいになり、トレンドから取り残される。実際の値(灰色)より低い位置で停滞してしまう
- 線形補間(緑):欠損区間でも トレンドに沿って上昇 し、実際の値に近い
このように、トレンドのあるデータでは LOCF/NOCB がトレンドを無視してバイアスを生む ことがあります。
一方、線形補間は前後を結ぶためトレンドをある程度追従できます。
まとめ
今回のポイントを振り返りましょう。
- 時系列データ は時間順という構造を持つため、平均値補完などをそのまま使うと不自然になる
- ゼロ埋め・平均埋め は時系列構造を壊すので避けるべき
- LOCF(
ffill()):直前の値で埋める。未来情報を使わないので予測モデル向き
- NOCB(
bfill()):直後の値で埋める。未来情報を使うのでデータ漏洩に注意
- 線形補間(
interpolate(method='linear')):前後を直線で結ぶ。最も滑らかで連続性のあるデータ向き
- トレンドのあるデータ では LOCF/NOCB がバイアスを生むため、線形補間が有利なことが多い
- 万能の手法はなく、データの性質に応じて選び、可視化して確認する ことが重要

