404 Not Found

404 Not Found


nginx

【タイムシリーズ応用】resample・rolling・ewmで時系列分析をマスター

レッスン16では日付時刻型を学びました。本レッスンでは実践的なタイムシリーズ分析に踏み込みます。resampleは日次データを月次サマリーに変換し、shift/diffはラグ特徴量を構築し、3つのウィンドウ関数(rolling/expanding/ewm)はトレンドとボラティリティを明確にします。これは定量分析やML特徴量エンジニアリングの核となる基礎です。

⚠️ 注意: 以下のコードを実行するには、ローカルのPython環境が必要です。

1. 本レッスンで学ぶこと



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ウィンドウが時間軸に沿ってスライドする様子

100%
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(デフォルト)を使用してください。これは過去の値のみを参照します。このミスは定量分析において深刻なデータリーケージを引き起こします。

📖 まとめ


📝 練習問題

  1. 基礎(難易度 ⭐):30日分の日次売上DataFrameを作成してください。resample('W')で週次データに集計し、rolling(7)で7日移動平均を計算してください。
  2. 中級(難易度 ⭐⭐):60日分の株価終値を作成してください。shift(1)ラグ → diff()日次差分 → pct_change()リターン → rolling(20)ボラティリティ → ewm(span=12)平滑化を計算してください。
  3. チャレンジ(難易度 ⭐⭐⭐):90日分の日次売上をシミュレートしてください。resample月次/四半期サマリー → rolling 7/14/30日移動平均の比較 → expanding累積平均 → ewm平滑化 → 最大ドローダウンの計算を完成させてください。

← 前のレッスン:日付と時刻 · 次のレッスン:MultiIndex →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%