時系列データの前処理は、工程の順序を間違えると後段でいくら調整しても精度が戻りません。時刻の型をそろえる前に欠損を埋めれば、埋めた場所が実際の欠測位置とずれます。全期間の平均で標準化してから分割すれば、検証区間の情報が学習側へ漏れる。この記事では、時刻軸の整理から欠損補間、外れ値と定常化、ラグや移動統計の生成、リークを避ける分割までを実装の単位で並べ、あわせて pandas 3系で書き換えが必要になった箇所を整理します。
まとめ:時系列の前処理は工程順を固定し、pandas 3系の変更点を先に潰す
先に結論を置きます。工程の順序は「時刻軸の整理、欠測の明示、外れ値の処置、定常化の判断、特徴量の生成、分割」で固定してください。この並びを崩すと、補間した値の上に外れ値判定をかけたり、未来の統計量を特徴量へ混ぜたりといった事故が静かに起きます。
とくに事故が起きやすいのは分割の周辺です。標準化の統計量を全期間から求める、ラグ生成のshift幅が予測したい先の長さに足りていない、検証区間の直前まで学習に使ってしまう。この三つは検証スコアだけが良くなり、本番で落ちる典型パターンでした。
着手の前に環境も確かめてください。pandas 3.0では月次や年次を表す M や Y といったエイリアスが削除され、既存の resample がそのままでは動きません。Copy on Write が既定になったことで、連鎖代入による列の書き換えも黙って無効化されます。前処理コードは頻度指定と代入の塊なので、影響範囲は他のどの層よりも広く出ます。
時刻軸を先に整える|タイムスタンプの型とタイムゾーンと不等間隔の直し方
前処理の第一工程は、値ではなく時刻の側です。ここが揺れたまま先へ進むと、後段の欠損判定も集約もすべて信頼できなくなります。
UTC保存とローカル変換の線引き|tz_localizeで先に決める二つの引数
実装の原則はひとつで、保存と計算はUTC、表示だけをローカルに変換します。センサーや業務システムから届く時刻列は、タイムゾーン情報が付いた文字列、ローカル時刻のまま素で書かれた文字列、エポック秒の三種類が混ざるのが常でした。読み込みの段階で utc=True を付けて一度そろえておくと、以降の結合や集約で時差の混入を疑わずに済みます。
import pandas as pd
raw = pd.read_csv("sensor.csv")
ts = pd.to_datetime(raw["measured_at"], utc=True)
s = pd.Series(raw["value"].to_numpy(), index=ts).sort_index()
jst = s.tz_convert("Asia/Tokyo")
five_min = s.resample("5min").mean()
索引をソートしていない状態で resample を呼ぶと、区間の切り出しが意図と変わります。読み込み直後の sort_index は省かないでください。重複したタイムスタンプも、この時点で扱いを決めておきます。
サマータイムで壊れる時刻|ambiguousとnonexistentで挙動を固定する
日本国内の案件だけを見ていると忘れがちですが、海外拠点の設備ログや多地域のアクセスログを扱った瞬間に夏時間の問題が来ます。時計が戻る日には同じローカル時刻が二度現れ、進む日には存在しない時刻が生まれる。pandasの tz_localize は、この二つを ambiguous と nonexistent という別々の引数で扱います。
| 引数 | 対象 | 指定できる値 | 既定値 |
|---|---|---|---|
| ambiguous | 時計が戻る日の重複 | infer・真偽値・NaT・raise | raise |
| nonexistent | 時計が進む日の欠落 | 前後への寄せ・NaT・raise | raise |
どちらも既定は raise で、該当する時刻があれば例外になります。バッチが年に二回だけ落ちる原因はたいていこれです。方針としては、重複側は判別できないので NaT にして欠測へ落とし、欠落側は shift_forward で直後の実在時刻へ寄せる組み合わせが扱いやすいところでした。時間差を指定して寄せ幅を明示することもできます。
naive = pd.to_datetime(raw["local_time"])
berlin = naive.dt.tz_localize(
"Europe/Berlin",
ambiguous="NaT",
nonexistent="shift_forward",
)
なお infer は順序から推定を試みますが、重複が対で現れる場合しか効きません。欠測混じりの実データでは例外になりやすく、明示指定のほうが安全です。
不等間隔データをそろえる|asfreqとresampleで変わる欠測の見え方
計測が5分おきの想定でも、実データは4分58秒後に来たり、20分飛んだりします。ここで asfreq と resample のどちらを使うかで、後段に見える欠測の量が変わる。asfreq は指定した頻度の格子を作って、格子点にぴったり一致する観測だけを残すため、わずかなずれも欠測として現れます。resample は区間へ入る観測をまとめるので、ずれは吸収されますが集約関数の選択が結果を左右します。
| データの性質 | 集約関数 | 理由 |
|---|---|---|
| 瞬時値の計測(温度・在庫) | mean または last | 区間代表値として妥当 |
| 累積のイベント数(売上・PV) | sum | 加算性があり分割しても保つ |
| 状態フラグ(稼働・停止) | max または mode | 平均が意味を持たない |
累積量に mean をあてると、区間内の観測本数が減った日だけ値が過小に出ます。逆に瞬時値へ sum をあてれば、計測が密な日だけ跳ね上がる。集約関数の取り違えは補間の失敗より発見が遅れるため、列ごとに辞書で明示してレビュー対象にしておくと安全です。長期保存のダウンサンプリングを保存側の機能で持つ設計は、時系列データベースの圧縮と保持設計から製品選定までを扱った記事で整理しています。
欠損値の扱い|「無い」の意味を分けてから補間方式と外挿の可否を選ぶ
時系列の欠損は、一般的な表データの欠損と扱いが違います。行そのものが存在しない期間があり、しかも埋めた値と観測値が後段で見分けられなくなるからです。
欠測と未計測とゼロの区別|補間する前に欠測フラグを列として残す
まず、値が無い理由を三つに分けます。センサーが落ちて記録が途切れた欠測、そもそも稼働していない期間の未計測、そして実際に0だった観測。この三つを同じ扱いにすると、休日の売上0を欠損として補間で埋めてしまい、需要の水準が実態より高く学習されます。
実装としては、格子を作った直後に欠測位置を別の列として保存し、そのうえで補間します。この欠測フラグは後で特徴量としても効きます。欠測が多い日は計測系に異常があった日で、目的変数の分布も違うことが珍しくありません。
grid = s.asfreq("5min")
gap_flag = grid.isna().astype("int8")
filled = grid.interpolate(
method="time",
limit=3,
limit_area="inside",
)
補間方式の選び分け|前方補完・線形・時間比例と外挿を禁じる境界
補間方式は、その値が現実にどう振る舞うかで決めます。在庫数や設定値のように次の更新まで同じ値が続く量は前方補完、温度や流量のように連続的に変化する量は時間比例の補間が素直です。等間隔でない索引に method="linear" を使うと、行番号を等間隔とみなして直線を引くので、20分空いた区間と5分の区間が同じ重みになる。時刻の間隔を尊重する method="time" を選んでください。
制限のかけ方も同じくらい効きます。limit は連続して埋める本数の上限で、これを付けないと丸一日の欠測が一本の直線で埋まります。limit_area に inside を渡すと観測に挟まれた区間だけが対象になり、系列の先頭と末尾へ外挿が伸びるのを止められる。末尾の外挿は、最新期間の予測へそのまま効いてしまう厄介な事故でした。
| データの性質 | 方式 | 連続補間の上限目安 |
|---|---|---|
| 階段状に更新される値 | ffill | 更新間隔の2倍まで |
| 連続的に変化する物理量 | time | 周期の10分の1まで |
| 強い周期を持つ需要 | 同時刻の前週値 | 1日まで |
外れ値と分布の整形|値を消さずに扱う手順と定常化を決める判断基準
外れ値の処置と定常化は、どちらも「後段のモデルが前提とする形へ寄せる」工程です。やりすぎれば拾いたい信号まで削るため、判断の根拠を残しながら進めます。
外れ値は削除より置換と記録|移動中央値とMADで境界を作る手順
時系列で行ごと削除すると、そこに穴が空いて索引の連続性が壊れます。処置は削除ではなく、境界を超えた値を移動中央値へ置き換え、置換した位置をフラグで残す形にする。境界の作り方は、平均と標準偏差ではなく中央値とMAD(中央絶対偏差)を使うのが実務的です。外れ値そのものが平均と標準偏差を押し上げ、境界が広がって検出できなくなる問題を避けられます。
med = filled.rolling(window=97, center=True, min_periods=13).median()
dev = (filled - med).abs()
mad = dev.rolling(window=97, center=True, min_periods=13).median()
score = dev.div(mad.mul(1.4826).replace(0, pd.NA))
clean = filled.mask(score.gt(5), med)
outlier_flag = score.gt(5).astype("int8")
1.4826という係数は、正規分布のときMADを標準偏差の尺度へそろえるためのものです。窓を中心合わせにしているので、この処置は学習データの整形にだけ使い、本番の推論では過去側だけを見る片側窓へ差し替えます。両者を同じコードで書き分けられるよう、窓の指定は引数へ出しておいてください。
定常性の確認手順|ADFとKPSSを併用して差分と対数変換を決める
ARIMA系のモデルは定常性を前提にしますが、勾配ブースティングやニューラル系は必ずしも要求しません。差分を取るかどうかは、使うモデルを決めてから判断します。検定はADFとKPSSを併用してください。ADFは「単位根がある」を帰無仮説に、KPSSは「定常である」を帰無仮説に置くため、片方だけでは判断がつかない領域が残ります。
from statsmodels.tsa.stattools import adfuller, kpss
x = clean.dropna()
adf_p = adfuller(x, autolag="AIC")[1]
kpss_p = kpss(x, regression="c", nlags="auto")[1]
print(adf_p, kpss_p)
| ADFの結果 | KPSSの結果 | 解釈 | 処置 |
|---|---|---|---|
| 棄却する | 棄却しない | 定常 | 差分は不要 |
| 棄却しない | 棄却する | 単位根あり | 1階差分を取る |
| 棄却する | 棄却する | トレンド定常 | トレンド項を除去 |
| 棄却しない | 棄却しない | 判断保留 | 期間を分けて再検定 |
分散が水準に比例して広がる系列、つまり売上が伸びるほど振れ幅も伸びる形では、差分より先に対数変換を検討します。0や負値を含む場合は対数が使えないため、Box-Cox変換かYeo-Johnson変換へ切り替える。どの変換を通したかは推論時に逆変換が必要なので、係数を含めて保存してください。定常化した後にどのモデルを選ぶかは、需要予測アルゴリズムの選び方と実装手順を扱った記事の領分として、本記事では扱いません。
特徴量の生成|ラグ・移動統計・カレンダー特徴を作る順序と注意点
ここからは、整えた系列を機械学習の入力へ変える工程です。時系列固有のリークは、ほぼすべてこの章で作り込まれます。
ラグと移動統計の作り方|shiftを挟んで未来の値を混ぜない書き方
予測したい先の長さ、いわゆるホライズンをhとすると、特徴量に使ってよい最短のラグはhです。3時間先を予測する設計で1時間前の値を特徴量に入れれば、推論時に手に入らない情報を使っていることになる。実装では、ラグも移動統計もすべて shift を通した系列から作るのが確実でした。
df = clean.to_frame("y")
h = 3
base = df["y"].shift(h)
for lag in [0, 1, 2, 6, 12, 24]:
df[f"lag_{h + lag}"] = df["y"].shift(h + lag)
df["roll_mean_24"] = base.rolling(24, min_periods=24).mean()
df["roll_std_24"] = base.rolling(24, min_periods=24).std()
df["gap_flag"] = gap_flag
df["dow"] = df.index.dayofweek
df["hour"] = df.index.hour
df = df.dropna()
移動統計で中心合わせの窓を指定すると窓が未来側へ伸びるため、特徴量生成では使いません。min_periods を窓幅と同じにしているのは、系列の先頭で本数の足りない不完全な平均が入るのを防ぐためです。ここを緩めると、学習データの冒頭だけ性質の違う行が混ざります。
ラグの本数は増やすほど良くなるわけではなく、周期が日単位なら直近数点と1周期前後を押さえれば大半の情報は入ります。分析手法そのものの整理は時系列分析の基礎と手法の使い分けをまとめた記事を参照してください。
カレンダーと外部データ|曜日・祝日・気象を結合する時刻の合わせ方
曜日や時刻は循環する量なので、数値のまま入れると23時と0時が最も遠い点になります。木構造のモデルなら整数のままでも分割で扱えますが、線形モデルやニューラル系ではサインとコサインの対に変換するか、カテゴリとして扱ってください。祝日は国別のカレンダーを外部から与える必要があり、日本の場合は振替休日と年末年始の企業休業日を分けて持つと精度が変わります。
気象や価格といった外部データを結合するときは、そのデータが「いつ手に入るか」を必ず確認します。学習時に実況値を使い、推論時に予報値を渡す構成にすると、本番だけ精度が落ちる。学習の段階から、推論時に入手できる版のデータで組んでおくのが原則です。
リークを避ける分割|学習と本番で同じ前処理を通すための設計手順
時系列のリークは三経路から入ります。分割の境界、スケーリングの統計量、そして特徴量生成の窓です。前の章で三つめは塞いだので、ここでは残る二つを扱います。
TimeSeriesSplitのgap|予測する先の長さだけ学習末尾を捨てる
scikit-learnの TimeSeriesSplit は、学習区間を前へ、検証区間を後ろへ置いて時間順を守る分割器です。引数は n_splits、max_train_size、test_size、gap の四つで、公式ドキュメントは gap を「各学習セットの末尾からテストセットの前に除外するサンプル数」と定義しています。test_size の既定値は、サンプル数を分割数に1を足した値で割った商です。
from sklearn.model_selection import TimeSeriesSplit
from sklearn.preprocessing import StandardScaler
X = df.drop(columns=["y"])
y = df["y"]
cv = TimeSeriesSplit(n_splits=5, test_size=24 * 14, gap=h)
for tr, te in cv.split(X):
sc = StandardScaler().fit(X.iloc[tr])
X_tr = sc.transform(X.iloc[tr])
X_te = sc.transform(X.iloc[te])
gap をホライズンと同じhに合わせている理由は、学習の最後の行が検証区間の直前の観測を見ているためです。ここを0のままにすると、実運用では存在しない「直前まで正解が見えている」状況で検証することになります。移動統計の窓幅がhより長い場合は、窓幅の分まで gap を広げてください。
スケーリングを置く位置|fold内で統計量を求め全期間平均を使わない
標準化や正規化は、必ず分割の内側で学習側だけから統計量を求めます。前処理としてデータフレーム全体に StandardScaler をかけてから分割する書き方は簡潔ですが、検証区間の平均と分散が学習へ流れ込む。上のコードで fit を学習側の索引にだけ通しているのはこのためです。
差分や対数変換も同じ扱いにします。Box-Cox変換の係数を全期間から推定すれば、そこにも未来の情報が入る。工程が増えるほど手作業では抜けが出るので、scikit-learnの Pipeline へ載せて交差検証の関数へ渡す形に寄せると、fold内で完結する保証が仕組みとして得られます。
学習と推論のずれを防ぐ|前処理を関数化して両経路で同じ実装を通す
ノートブックで書いた前処理を、推論側でAPIの担当者が書き直す。この構成が学習と推論のずれを生む最大の原因です。列名の綴り、欠測の埋め方、カテゴリの符号化順序といった細部が少しずつ食い違い、オフラインの精度が本番で再現しません。
対策は単純で、前処理を一つの関数またはPipelineオブジェクトへまとめ、学習バッチと推論経路の双方から同じ実装を呼びます。呼び出し側が異なるのは窓の向きだけになるよう、中心合わせの窓は引数で切り替えられるようにしておく。前処理後の分布を監視して、学習時と推論時のずれを検知する仕組みまで含めると、時系列データの異常検知を実装目線で扱った記事の枠組みがそのまま使えます。
pandas 3系で書き換えが要る前処理コード|頻度エイリアスと代入
2026年7月22日公開の pandas 3.0.5 を含む3系では、前処理コードへ直接効く変更が二つ入りました。既存資産を持ち込む案件では、ここを先に潰すと手戻りが減ります。
頻度エイリアスの置き換え|M・Q・Yを使う箇所を洗い出して直す
公式のリリースノートは、オフセットのエイリアス M・BM・SM・CBM・Q・BQ・Y・BY について非推奨を強制したと記しています。移行先はそれぞれ ME・BME・SME・CBME・QE・BQE・YE・BYE です。月次の集計を月末エイリアスの旧表記で書いた既存コードは、そのままでは通りません。
monthly = s.resample("ME").mean()
quarterly = s.resample("QE").sum()
yearly = s.resample("YE").max()
あわせて、日や週を小文字で書く指定も非推奨になりました。d・b・c は大文字の D・B・C へ、w や w-mon は W や W-MON へ寄せます。時・分・秒は逆に小文字の h・min・s が現行表記なので、大小の向きが頻度によって違う点に注意してください。移行時はリポジトリ全体を頻度文字列で検索し、動的に組み立てている箇所まで確かめる必要があります。
連鎖代入が効かなくなる変更|列の書き換えを一段の代入にまとめる
もう一つは Copy on Write の既定化です。公式のリリースノートは、あらゆる索引操作と新しいオブジェクトを返すメソッドが利用者から見てコピーとして振る舞うため、複数段階に分けた代入による書き換えが機能しなくなると説明しています。mode.copy_on_write オプションの設定はもはや効果を持たず、このオプション自体も pandas 4.0 で削除される予定です。
前処理では、抽出した部分集合に対して列を書き換える書き方が頻出します。条件で絞ったうえで欠損を埋める、特定期間だけ単位を換算するといった処理です。これらは元のオブジェクトへ直接代入する形へ書き直します。
frame = monthly.to_frame("value")
frame["value"] = frame["value"].ffill()
mask = frame.index.month.isin([1, 8])
frame.loc[mask, "value"] = frame.loc[mask, "value"].mul(1.05)
厄介なのは、旧来の連鎖代入が例外ではなく無変更で通る点です。警告を頼りに一斉検出する運用ではなく、書き換え後の値をテストで確かめる形に寄せてください。前処理の各工程へ最小限の単体テストを置けば、この種の変更にも版上げのたびに追随できます。
実装先の選び方|ノートブックから運用へ移すときの内製と委託の線引き
前処理は、書けることと運用に載ることが別物の領域です。最後に、実装先とライブラリ選定の現在地を整理します。
ライブラリの現在地|pandasとstatsmodelsとsktimeの版と役割
2026年8月時点のPyPI公開版は、pandas 3.0.5(2026年7月22日)、scikit-learn 1.9.0(2026年6月2日)、statsmodels 0.14.6(2025年12月5日)、sktime 1.1.0(2026年7月28日)、tsfresh 0.21.2(2026年5月31日)でした。sktimeは2026年6月11日に1.0.0へ到達しており、実験的な位置づけから運用前提の版数管理へ移っています。
| ライブラリ | 前処理での役割 | 持ち出す条件 |
|---|---|---|
| pandas | 時刻整形・欠損・集約 | 常に土台として使う |
| statsmodels | STL分解・定常性検定 | 統計的な検証が要る場合 |
| sktime | 変換と分割の共通化 | 系列が多く手順が定型のとき |
| tsfresh | 特徴量の自動生成 | 探索段階で候補を広げるとき |
tsfreshのような自動生成は候補を数百まで広げますが、生成した特徴量が推論時に計算可能かは別問題です。探索で当たりを付け、本番へ載せるものは手で定義し直す運用が現実的でした。前処理をバッチとして組む際の冪等性や再実行の設計は、データパイプラインの構成要素と冪等性から設計判断を整理した記事にまとめています。
内製で回る条件と委託を挟む場面|系列数と再現性の要件で引く境界線
内製で完結する条件は、系列が数十本まで、更新が日次以下、そして前処理の担当者が継続して在籍することの三つです。この範囲なら、pandasのスクリプトとスケジューラだけで十分に回ります。学習と推論の経路も一人の頭の中で整合が取れる。
境界を越えるのは、系列が数百から数千に増えて系列ごとに欠測パターンが違う場合、監査や再現性の要求があって前処理のどの版がどの予測を生んだか説明が要る場合、そして担当者の交代が見えている場合です。ここからは、前処理を特徴量ストアやワークフロー基盤へ載せる設計が必要になり、設計の初回だけ外部を挟む判断が費用に見合います。
当社では、計測データの取り込みから前処理、学習と推論の経路統合までをデータ分析基盤構築・MLOps構築支援として請けています。既存のノートブックを運用へ載せ替える範囲だけを切り出す進め方も可能です。
よくある質問
欠損はどこまで補間してよいですか?
連続して埋める長さの上限を、系列の周期から決めてください。日周期のデータなら数時間、5分値なら15分程度が目安です。それを超える欠測は補間せず、学習時は該当行を落とすか欠測フラグを特徴量として明示します。系列全体の2割を超えて欠測がある場合は、その系列自体を除外する判断も検討してください。
リサンプリングは平均と合計のどちらを使いますか?
値に加算性があるかで決まります。売上件数やPVのように足し合わせれば元へ戻る量は合計、温度や在庫水準のように区間の代表値を見たい量は平均か区間末尾の値です。状態フラグには最大値や最頻値をあてます。列ごとに集約関数を辞書で明示しておくと取り違えを防げます。
定常化のための差分は必ず必要ですか?
使うモデルによります。ARIMA系や状態空間モデルは定常性を前提にするため差分や対数変換が要りますが、勾配ブースティングやニューラル系は非定常のまま扱えます。判断はADF検定とKPSS検定を併用し、結果の組み合わせで差分かトレンド除去かを決めてください。差分を取った場合は、予測値を元の尺度へ戻す逆変換を推論側にも実装します。
標準化は分割の前と後のどちらで行いますか?
必ず分割の後、それも学習側のデータだけから統計量を求めます。全期間で標準化してから分割すると、検証区間の平均と分散が学習へ流れ込み、検証スコアだけが実力より高く出る。scikit-learnのPipelineへ前処理を載せて交差検証へ渡せば、fold内で完結する保証が仕組みとして得られます。
pandas 3系へ上げると前処理コードは動かなくなりますか?
頻度エイリアスを使っている箇所は例外になります。月次や年次の旧表記は削除され、月末や年末を表す新しい表記への書き換えが必要です。加えてCopy on Writeが既定になったため、複数段階に分けた列の書き換えは例外を出さずに無変更となります。後者は静かに失敗するので、版上げの際は前処理の出力を突き合わせるテストを用意してから進めてください。
関連記事
- 時系列データベースとは?RDBとの違いと圧縮・保持設計から製品選定までを実装目線で解説:前処理した系列の保存先を検討する場合に
- 時系列データの異常検知|手法の選び分けとしきい値・評価・アラート運用を実装目線で解説:整形した残差から異常を拾う実装とアラート運用
- 需要予測アルゴリズムの選び方と実装手順|時系列・機械学習・Python:前処理の後段で、どのモデルを選ぶかを判断する場合に
- データパイプラインとは?種類・構成要素と冪等性から考える設計判断を解説【2026年版】:再実行に耐えるバッチ構成へ載せ替える場合に
- 時系列とは?意味と分析手法の使い分け|ARIMAからPython実装まで:前処理の土台になる分析手法の整理