K_DM Book
用語集
対象者 標準
対象者別の入口 →

対象者の設定はこのブラウザの中にだけ保存されます。

言語 JA

リサンプリングで週次トレンドを抽出

入門

04.5.4

リサンプリングで週次トレンドを抽出

最終更新 2020-01-21 読了時間 2 分
まとめ
リサンプリングとは、日次のデータを「月曜で締める 1 週間」のような決まった幅の箱に入れ直し、箱ごとに平均や合計を 1 つの値にまとめる操作です。pandas.Series.resample("W-MON").mean() の 1 行で 210 日分の点が約 30 個の週次平均に変わり、標準偏差 4 の日々のノイズは平均によって約 1.5(\(4/\sqrt{7}\))まで小さくなります。このページでは、1 日 0.2 ずつ増えるトレンドに周期 14 日の波とノイズを重ねた人工データを作り、日次の細い線と週次平均の太い線を重ねて、どこまでが「ならされる」のかを確かめます。読み終えると、集約の幅を周期の整数倍にすると周期そのものが消えるという使い分けまで判断できるようになります。

直感 #

毎日の来店客数を 210 日分グラフにすると、曜日や天気で上下する細かいギザギザに目が奪われ、「半年でお客は増えたのか」が読めません。そこで「火曜から月曜までの 7 日分を平均して、その月曜に 1 点だけ打つ」ことにします。7 日の中にあるプラスとマイナスのノイズは打ち消し合い、点は 30 個ほどに減って、ゆっくり右上がりの流れだけが残ります。これがリサンプリングです。移動平均と違って窓が重ならず点が間引かれるので、「週ごとの数字」として会議資料にそのまま載せられる形になります。

何が起きているか(ステップで見る) #

コードを読む前に、この手法が図の上で何をしているかを追ってみましょう。「再生」を押すか、スライダーを動かすと 1 手順ずつ進みます。

見るポイント:窓の中の平均を窓の中央に打つだけ。リサンプリングはこの窓を 7 日ごとに飛ばして重ならないように置いた版で、点の数が 1/7 になる。窓を 7 日から 14 日に広げるほど滑らかだが、山や谷が遅れて現れる。

式で見ると何がどれだけ減るか #

週 \(k\) の平均は、その週に属する 7 日の値 \(y_t\) の単純平均です。

$$ \bar{y}_k = \frac{1}{7} \sum_{t \in \text{week}_k} y_t $$

このページのデータは \(y_t = 200 + 0.2t + 10\sin(2\pi t/14) + \varepsilon_t\)(\(\varepsilon_t \sim N(0, 4^2)\))なので、7 日平均の各項は次のように変わります。トレンド \(0.2t\) は週の中央の値になるだけで形は変わりません。独立なノイズは 7 個を平均するので標準偏差が \(4/\sqrt{7} \approx 1.5\) に縮みます。周期 14 日の波は 7 日平均では半周期分しか入らないため、振幅が \(10 \times \sin(\pi/2) / (7 \sin(\pi/14)) \approx 6.4\) に減るものの消えません。もし resample("2W-MON") で 14 日ごとに平均すれば、ちょうど 1 周期分が箱に入って波は打ち消し合い、トレンドだけが残ります。

Python で日次と週次平均を重ねる #

次のコードは、上の式どおりの日次データ 210 点を作り、resample("W-MON").mean() で週次平均に集約して、薄い日次の線の上に太い週次の線を丸マーカー付きで重ねます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

rng = np.random.default_rng(6)
dates = pd.date_range("2021-04-01", periods=210, freq="D")
base = 200 + 0.2 * np.arange(len(dates))
seasonal = 10 * np.sin(2 * np.pi * np.arange(len(dates)) / 14)
noise = rng.normal(0, 4, size=len(dates))
daily = pd.Series(base + seasonal + noise, index=dates)

weekly_mean = daily.resample("W-MON").mean()

fig, ax = plt.subplots(figsize=(7, 4))
ax.plot(daily.index, daily, color="#cbd5f5", linewidth=0.9, label="日次")
ax.plot(weekly_mean.index, weekly_mean, color="#2563eb", linewidth=2, marker="o", label="週次平均")
ax.set_title("日次データを週次平均でならす")
ax.set_xlabel("日付")
ax.set_ylabel("値")
ax.legend()
ax.grid(alpha=0.3)

fig.tight_layout()
fig.savefig("static/images/timeseries/resample_weekly.svg")

リサンプリングで週次トレンドを抽出の図

実行結果の読み方 #

薄い青の日次の線は 200 付近から 240 付近(200 + 0.2 × 209 ≈ 242)まで上がりながら、周期 14 日の波と標準偏差 4 のノイズで激しく上下します。太い青の週次平均はおよそ 30 個の丸印を結んだ線で、ノイズが約 1.5 に減ったため右上がりのトレンドがはっきり読めます。ただし丸印は 2 週ごとに高くなったり低くなったりを繰り返しているはずです。これは周期 14 日の波が振幅約 6.4 で残っているためで、週次平均は「周期 7 日より短い揺れ」しか消せないことを示しています。

使いどころと落とし穴 #

リサンプリングの幅は、消したい揺れの周期と、意思決定の単位の両方から決めます。曜日のクセを消すなら "W" 系、月の締めに合わせるなら "MS"(月初)や "ME"(月末)を使い、この例のように 14 日周期が邪魔なら "2W-MON" のように周期の整数倍を選ぶと波ごと消えます。mean() は箱の中に欠損があっても残りの日で平均を取るため、欠損が多い週の点は見かけ上ふらつきます。箱の中の件数を .count() で確認し、7 日そろっていない週を除くか注記する習慣をつけてください。売上のような量を扱うときに mean() と sum() を取り違えると、週次の線が日次の 1/7 の高さになって読み違えの原因になります。