【タイムシリーズ応用】resample・rolling・ewmで時系列分析をマスター
レッスン16では日付時刻型を学びました。本レッスンでは実践的なタイムシリーズ分析に踏み込みます。resampleは日次データを月次サマリーに変換し、shift/diffはラグ特徴量を構築し、3つのウィンドウ関数(rolling/expanding/ewm)はトレンドとボラティリティを明確にします。これは定量分析やML特徴量エンジニアリングの核となる基礎です。
⚠️ 注意: 以下のコードを実行するには、ローカルのPython環境が必要です。
1. 本レッスンで学ぶこと
- ❶ resample — リサンプリング
- ❷ shift / diff / pct_change
- ❸ rolling — 移動ウィンドウ
- ❹ expanding — 累積ウィンドウ
- ❺ ewm — 指数加重
2. アリスのコーヒーショップ月次レポート
(1) 課題:日次データは粒度が細かすぎる — 月次集計が必要
アリスは90日分の日次売上データを持っており、上司は月次レポートを求めています:
PYTHON
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'sales': np.random.poisson(50, 90) * 10,
'customers': np.random.poisson(30, 90)
}, index=pd.date_range('2024-01-01', periods=90))
# 日次データ → 月次サマリーが必要
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。pandasのバージョンにより実際の値が若干異なる場合があります。
(2) 解決策:resampleで1行集計
▶ サンプル:resample月次集計(難易度 ⭐)
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。pandasのバージョンにより実際の値が若干異なる場合があります。
PYTHON
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'sales': np.random.poisson(50, 90) * 10,
'customers': np.random.poisson(30, 90)
}, index=pd.date_range('2024-01-01', periods=90))
# 日次 → 月次にリサンプリング
monthly = df.resample('M').agg({
'sales': 'sum',
'customers': 'mean'
})
print(monthly)
# sales customers
# 2024-01-31 14680 29.55
# 2024-02-29 13520 31.10
# 2024-03-31 15430 30.45
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。pandasのバージョンにより実際の値が若干異なる場合があります。
3. resample — リサンプリング
(1) ダウンサンプリングとアップサンプリング
▶ サンプル:resampleダウンサンプリング/アップサンプリング(難易度 ⭐⭐)
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。pandasのバージョンにより実際の値が若干異なる場合があります。
PYTHON
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'price': np.random.uniform(100, 200, 60).round(2)
}, index=pd.date_range('2024-01-01', periods=60))
# ダウンサンプリング:日次 → 週次
weekly = df.resample('W').agg({
'price': ['first', 'max', 'min', 'last'] # OHLC(四本値)
})
print("週次OHLC:")
print(weekly.head())
# ダウンサンプリング:日次 → 四半期
quarterly = df.resample('Q').mean()
# アップサンプリング:日次 → 時間単位(補完方法が必要)
hourly = df.resample('h').ffill() # 前方補完
# 警告:行数が大幅に増えます!
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。pandasのバージョンにより実際の値が若干異なる場合があります。
(2) resampleとgroupbyの比較
| 機能 | resample | groupby |
|---|---|---|
| グループ化の基準 | 時間頻度 | 任意のカラム |
| 対象 | DatetimeIndex | 任意のDataFrame |
| アップサンプリング | ✅ 欠損値を補完 | ❌ |
| 頻度エイリアス | D/W/M/Q/H | — |
| 本質 | 時間次元に沿ったgroupby | 汎用的なグループ化 |
4. shift / diff / pct_change
(1) ラグと差分
▶ サンプル:shift/diff/pct_change(難易度 ⭐⭐)
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。pandasのバージョンにより実際の値が若干異なる場合があります。
PYTHON
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'sales': np.random.poisson(50, 10) * 10
}, index=pd.date_range('2024-01-01', periods=10))
# shift:データを前後に移動
df['sales_prev'] = df['sales'].shift(1) # 前日の売上
df['sales_next'] = df['sales'].shift(-1) # 翌日の売上
# diff:前値との差分
df['sales_diff'] = df['sales'].diff() # 売上 - 前日売上
# pct_change:変化率
df['sales_pct'] = df['sales'].pct_change() * 100 # 変化率(%)
print(df.round(1))
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。pandasのバージョンにより実際の値が若干異なる場合があります。
💡 ヒント: shiftはMLタイムシリーズ特徴量エンジニアリングの基石です。
df['lag_1'] = df['value'].shift(1) は前日の値で当日を予測します。diffはトレンドの変化量を抽出し、pct_changeは成長率を抽出します。
5. rolling — 移動ウィンドウ
(1) 移動平均
▶ サンプル:rolling移動平均(難易度 ⭐⭐)
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。pandasのバージョンにより実際の値が若干異なる場合があります。
PYTHON
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'temperature': np.round(np.random.normal(20, 5, 30), 1)
}, index=pd.date_range('2024-01-01', periods=30))
# 7日移動平均
df['ma_7'] = df['temperature'].rolling(window=7).mean()
# min_periods付き(部分ウィンドウを許可)
df['ma_7_flex'] = df['temperature'].rolling(window=7, min_periods=3).mean()
# 複数集計
# Pandas 2.x:rolling.agg()は複数カラムのDataFrameを返すため、単一カラムに直接代入できません。joinを使用してください
rolling_agg = df['temperature'].rolling(7).agg(['mean', 'std', 'min', 'max'])
rolling_agg.columns = [f'temp_{stat}' for stat in ['mean', 'std', 'min', 'max']]
df = df.join(rolling_agg)
# 中央揃えrolling(将来の値を含む — 平滑化用であり予測用ではありません)
df['ma_7_center'] = df['temperature'].rolling(7, center=True).mean()
print(df[['temperature', 'ma_7', 'ma_7_center']].head(10))
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。pandasのバージョンにより実際の値が若干異なる場合があります。
(2) 時間ベースのウィンドウ
▶ サンプル:時間ベースのrollingウィンドウ(難易度 ⭐⭐)
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。pandasのバージョンにより実際の値が若干異なる場合があります。
PYTHON
import pandas as pd
import numpy as np
# 不規則なタイムシリーズ — 行ベースのウィンドウは不適切
df = pd.DataFrame({
'value': [10, 20, 15, 30, 25, 40, 35, 50, 45, 60]
}, index=pd.to_datetime([
'2024-01-01', '2024-01-02', '2024-01-05', # 03〜04が欠落
'2024-01-06', '2024-01-07', '2024-01-15', # 08〜14が欠落
'2024-01-16', '2024-01-20', '2024-01-21', '2024-01-25'
]))
# 行ベースのウィンドウ(不規則データには不適切)
print("行ベース 3:")
print(df['value'].rolling(3).mean())
# 時間ベースのウィンドウ(正しい — 暦日7日間)
print("\n時間ベース 7D:")
print(df['value'].rolling('7D').mean())
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。pandasのバージョンにより実際の値が若干異なる場合があります。
6. expanding — 累積ウィンドウ
▶ サンプル:expanding累積統計(難易度 ⭐)
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。pandasのバージョンにより実際の値が若干異なる場合があります。
PYTHON
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'sales': np.random.poisson(50, 10) * 10
})
# expanding:現在位置までの全値
df['cumsum'] = df['sales'].expanding().sum()
df['cummean'] = df['sales'].expanding().mean()
df['cummax'] = df['sales'].expanding().max()
# min_periods付き
df['cummean_3'] = df['sales'].expanding(min_periods=3).mean()
print(df)
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。pandasのバージョンにより実際の値が若干異なる場合があります。
(1) rolling vs expanding vs ewm の比較
| 機能 | rolling | expanding | ewm |
|---|---|---|---|
| ウィンドウ | 固定サイズ | 常に拡大 | 無限(重みが減衰) |
| 重み | 均等 | 均等 | 直近の値に高い重み |
| ユースケース | 局所的トレンド | 累積統計 | 平滑化トレンド |
| パラメータ | window | min_periods | span/com/halflife |
7. ewm — 指数加重
▶ サンプル:ewm指数平滑化(難易度 ⭐⭐)
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。pandasのバージョンにより実際の値が若干異なる場合があります。
PYTHON
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'price': np.cumsum(np.random.randn(30)) + 100
}, index=pd.date_range('2024-01-01', periods=30))
# 指数加重移動平均
df['ewm_5'] = df['price'].ewm(span=5).mean()
df['ewm_10'] = df['price'].ewm(span=10).mean()
df['ewm_20'] = df['price'].ewm(span=20).mean()
# 単純移動平均との比較
df['sma_5'] = df['price'].rolling(5).mean()
print(df[['price', 'sma_5', 'ewm_5']].head(10).round(2))
# ewmパラメータ(いずれか1つを指定):
# span=5 → 5期間SMAに類似するが直近重視
# com=4 → 重心(com = span-1)
# halflife=3 → 3期間ごとに重みが半減
# alpha=0.3 → 平滑化係数(0<α≤1)
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。pandasのバージョンにより実際の値が若干異なる場合があります。
8. 総合例:日次株価ローソク足分析
(5) ▶ rollingウィンドウが時間軸に沿ってスライドする様子
graph TB
A[タイムシリーズデータ] --> B[rollingウィンドウ]
B --> C[ウィンドウが時間軸に沿ってスライド]
C --> D[ウィンドウ1:t1〜tN]
C --> E[ウィンドウ2:t2〜t(N+1)]
C --> F[ウィンドウ3:t3〜t(N+2)]
D --> G[mean / std / max / min]
E --> G
F --> G
G --> H[平滑化トレンド / ボラティリティ]
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。pandasのバージョンにより実際の値が若干異なる場合があります。
▶ サンプル:タイムシリーズ分析パイプライン全体(難易度 ⭐⭐⭐)
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。pandasのバージョンにより実際の値が若干異なる場合があります。
PYTHON
import pandas as pd
import numpy as np
# ============================================
# 総合例:株価日次データ
# resample → rolling → diff → ewm
# ============================================
# 1. 株価日次データの生成(60営業日)
np.random.seed(42)
dates = pd.bdate_range('2024-01-01', periods=60)
price = 100 + np.cumsum(np.random.randn(60) * 2)
df = pd.DataFrame({
'open': price + np.random.randn(60) * 0.5,
'high': price + np.abs(np.random.randn(60)) * 1.5,
'low': price - np.abs(np.random.randn(60)) * 1.5,
'close': price + np.random.randn(60) * 0.5,
'volume': np.random.randint(100000, 500000, 60)
}, index=dates)
# 2. リサンプリング:日次 → 週次
weekly = df.resample('W-FRI').agg({
'open': 'first', 'high': 'max', 'low': 'min',
'close': 'last', 'volume': 'sum'
})
print("=== 週次OHLCV ===")
print(weekly.head(4).round(2))
# 3. rolling:20日移動平均
df['ma_20'] = df['close'].rolling(20).mean()
df['vol_20'] = df['close'].rolling(20).std()
# 4. 日次リターン
df['returns'] = df['close'].pct_change()
df['log_returns'] = np.log(df['close'] / df['close'].shift(1))
# 5. EWM:指数平滑化
df['ewm_12'] = df['close'].ewm(span=12).mean()
df['ewm_26'] = df['close'].ewm(span=26).mean()
df['macd'] = df['ewm_12'] - df['ewm_26']
# 6. サマリー
print(f"\n=== 統計情報 ===")
print(f"期間:{df.index[0].date()} 〜 {df.index[-1].date()}")
print(f"トータルリターン:{((df['close'].iloc[-1] / df['close'].iloc[0] - 1) * 100):.1f}%")
print(f"ボラティリティ(20日):{df['vol_20'].iloc[-1]:.2f}")
print(f"最大ドローダウン:{((df['close'] / df['close'].cummax() - 1).min() * 100):.1f}%")
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。pandasのバージョンにより実際の値が若干異なる場合があります。
❓ よくある質問
Q resampleとgroupbyの違いは何ですか?
A resampleは本質的に時間次元に沿ったgroupbyです。頻度(日次/週次/月次/四半期)でグループ化し、DatetimeIndex/PeriodIndexでのみ動作します。groupbyは任意のカラムでグループ化し、時間に限定されません。resampleはアップサンプリング(低頻度 → 高頻度は補完が必要)を独自にサポートしており、groupbyではできません。目安:時間ベースの集計にはresample、それ以外にはgroupbyを使用してください。
Q rollingのウィンドウサイズはどのように選べばよいですか?
A シナリオに依存します。短期トレンドにはウィンドウ5〜10、中期には20〜30(月次)、長期には50〜200(四半期/年次)を使用します。金融では20/50/200日移動平均が標準です。ウィンドウが小さすぎるとノイズが多く、大きすぎるとラグが過大になります。推奨:まずデータの頻度を確認し、ウィンドウが1〜3つの「自然サイクル」をカバーするように設定してください。
Q shiftとdiffの違いは何ですか?
A shiftは何も計算せずにデータを移動します。diffは差分を計算します。
s.diff() = s - s.shift(1) です。shiftはラグ特徴量の構築(ML)に使用し、diffは変化量の抽出(トレンド分析)に使用します。pct_changeは変化率を計算します:s.pct_change() = (s - s.shift(1)) / s.shift(1)。Q expandingとcumsumの違いは何ですか?
A expandingはcumsumの汎用版です。任意の集計関数(mean/std/max/min)をサポートしますが、cumsumは累積和のみを計算します。
s.expanding().sum() は s.cumsum() と同等です。expandingはより柔軟ですがやや遅く、日常利用ではcumsumの方が簡潔です。Q ewmとは何ですか?
A EWM(Exponentially Weighted Moving)は指数加重移動平均です。直近のデータに高い重みを与え、古いデータは指数的に減衰します。span=5は5期間SMAに似た挙動をしますが、直近の値により高い重みを付けます。SMAより変化への反応が速く、テクニカル分析(MACD)やオンライン学習の基礎となります。
Q resampleでアップサンプリングした後の値をどう補完しますか?
A アップサンプリング(低頻度 → 高頻度)では多くのNaN値が発生します。補完オプション:ffill()(前方補完、前値を使用)、bfill()(後方補完)、interpolate()(補間)、asfreq()(NaNを保持)。ffillが推奨です(シンプルかつ保守的)。タイムシリーズ分析にはinterpolateを使用してください(より滑らかな結果が得られます)。
Q center=Trueは予測に使えますか?
A 絶対に使えません!center=Trueはウィンドウが「未来を参照」する(後続の値を含む)ため、過去データの平滑化にのみ適しており、予測には使用できません。リアルタイムや予測シナリオでは、必ずcenter=False(デフォルト)を使用してください。これは過去の値のみを参照します。このミスは定量分析において深刻なデータリーケージを引き起こします。
📖 まとめ
- resampleは時間頻度で集計します。「時間次元に沿ったgroupby」です
- shiftはデータを移動してラグ特徴量を構築し、diff/pct_changeは差分と変化率を計算します
- rollingは固定サイズの移動ウィンドウで計算します(移動平均/標準偏差/極値)
- 時間ベースのrolling('7D')は不規則なタイムシリーズに最適です
- expandingは累積ウィンドウ(先頭から現在位置まで)、ewmは指数加重を適用します(直近の値に高い重み)
- 選択基準:局所的トレンド → rolling、累積統計 → expanding、平滑化 → ewm
📝 練習問題
- 基礎(難易度 ⭐):30日分の日次売上DataFrameを作成してください。resample('W')で週次データに集計し、rolling(7)で7日移動平均を計算してください。
- 中級(難易度 ⭐⭐):60日分の株価終値を作成してください。shift(1)ラグ → diff()日次差分 → pct_change()リターン → rolling(20)ボラティリティ → ewm(span=12)平滑化を計算してください。
- チャレンジ(難易度 ⭐⭐⭐):90日分の日次売上をシミュレートしてください。resample月次/四半期サマリー → rolling 7/14/30日移動平均の比較 → expanding累積平均 → ewm平滑化 → 最大ドローダウンの計算を完成させてください。



