ふくふくHukuhuku Inc.
EP.16Prep 18分公開: 2026-05-10

時系列の周期性を発見する:「うちは波があるよね」を仕入計画に変える分析手法

売上・トラフィック・在庫の時系列に「気づいていない周期性」が隠れていることは多い。曜日効果・月次サイクル・四半期波動・景気循環を、自己相関・STL分解・fft で炙り出し、機会損失の解消につなげる実装ガイド。

#時系列#周期性#STL分解#fft#自己相関#機会損失
CO📔 Google Colab で開く(上から順にセルを実行)
シェア

うちの売上、なんとなく波があるよね」── 経営者やマーケ担当の口からよく出るセリフ。ところが多くの場合、その波の正体は誰も解明していないまま、感覚で仕入や人員配置が決められています。結果として、ピークの月だけ仕入が足りずに機会損失閑散期に過剰在庫で資金繰り悪化、という古典的な失敗が繰り返される。

この記事では、時系列データに隠れた周期性を発見する手法を、コード付きで体系的に整理します。自己相関 / STL分解 / という3つの代表的な道具を「業務に効かせるための使い分け」目線で紹介。最後に、発見した周期性をどう経営判断に翻訳するかまで踏み込みます。

📔 ノートブック付き

本記事のコードを全部つなげた実行可能ノートブックを `/notebooks/prep-16-cyclicality.ipynb` で配布。仮想 EC 売上データに「曜日周期 + 月次周期 + 長期トレンド + 」を仕込み、3 手法それぞれが何をどこまで検出できるかを、自分の目で確かめられます。

1. 経営に効かせる「周期性発見」という仕事

本題に入る前に、なぜこの分析がエンジニア仕事として重要かを整理します。

気づいていない周期 = 機会損失

気づかないと起きること気づくと出来ること
毎月25日に売上ピーク → 在庫切れで失注前週から在庫を 1.5 倍にしておく
金曜の問い合わせが平日の 2 倍 → カスタマー離脱金曜だけシフトを厚くする
夏のボーナス商戦が前年比で前倒し化 → キャンペーン乗り遅れシーズン開始日を週単位で再設計
四半期末に法人案件が集中 → 営業の取りこぼし四半期末2週間に営業リソースを集中配備
周期性 ≠ 季節性

厳密には 季節性 (seasonal) は「1 年・1 週・1 日のような 固定周期」、周期性 (cyclic) は「景気循環や製品ライフサイクルなど 可変周期で長期」を指します。本記事ではビジネス慣用に合わせて両者を「波がある = 周期性」とまとめて扱い、必要に応じて区別します。

2. 時系列の3要素分解:トレンド・季節性・残差

時系列は古典的に 「長期傾向(トレンド)+ 規則的な波(季節性)+ それ以外(残差)」 に分けて扱います。残差の中に 景気循環や製品ライフサイクル波動(cyclic)不規則変動(irregular) が混ざります。

STL 分解で 3 要素に切り分ける(最頻出パターン)
Python
import pandas as pdfrom statsmodels.tsa.seasonal import STL
# 日次売上データを読み込みsales = pd.read_csv("daily_sales.csv", parse_dates=["date"], index_col="date")["amount"]
# STL: period=7 で「曜日周期」を抽出 (月次なら 30, 年次なら 365)stl = STL(sales, period=7, robust=True).fit()
# trend / seasonal / resid の 3 系列に分解されたprint("Trend:    ", stl.trend.tail(3).values)print("Seasonal: ", stl.seasonal.tail(3).values)print("Residual: ", stl.resid.tail(3).values)
# 4 つのチャートで一望stl.plot()
STL の使いどころ

1 つの周期成分が分かっている時の正攻法。曜日効果なら period=7、月次なら 30。複数周期がある場合(曜日 + 月次)は MSTL (Multiple Seasonal-Trend Decomposition) を使う(statsmodels 0.14+)。

3. 自己相関 (ACF / PACF):「何日前の自分とそっくり?」

自己相関関数 (ACF) は「今日の値と n 日前の値がどれだけ相関しているか」を ラグごとに計算します。ピークが立っているラグ = 周期。曜日周期があれば 7, 14, 21... のラグでピークが立つ。

自己相関プロットで周期を目視発見
Python
import matplotlib.pyplot as pltfrom statsmodels.graphics.tsaplots import plot_acf, plot_pacf
fig, axes = plt.subplots(2, 1, figsize=(10, 6))plot_acf(sales, lags=60, ax=axes[0])plot_pacf(sales, lags=60, ax=axes[1])axes[0].set_title("ACF: 周期がある場合、ラグ N でピークが立つ")axes[1].set_title("PACF: 単独ラグの寄与だけを抜き出した版")plt.tight_layout()plt.show()
# プログラム的にピークを抽出(青の信頼区間外のラグ)from statsmodels.tsa.stattools import acfimport numpy as np
acf_vals, confint = acf(sales, nlags=60, alpha=0.05, fft=True)significant_lags = [    lag for lag in range(1, len(acf_vals))    if abs(acf_vals[lag]) > (confint[lag, 1] - acf_vals[lag])]print("有意な周期ラグ候補:", significant_lags[:10])# 例) [7, 14, 21, 28, 30, 60]  → 曜日周期 7 と月次周期 30 の両方を検出
ACFのピットフォール

長いトレンドがあると ACF が常に高く出て、周期のピークが見えなくなる。前処理として 差分 (`sales.diff()`) を取るか、STL の残差 に対して ACF をかけると、純粋な周期だけが浮かび上がります。

4. フーリエ変換 (FFT):「隠れた周波数」を炙り出す

FFT (高速フーリエ変換) は時系列を 周波数の集合 に分解します。スペクトルのピーク = 強い周期成分。ACF が「ラグごとの相関」を見るのに対し、FFT は「どの長さの周期がどれだけ強く効いているか」を一望できます。未知の周期を発見するのに強い。

FFT で隠れた周期を周波数領域で見つける
Python
import numpy as npimport matplotlib.pyplot as plt
# 平均を引いてから FFT (DC 成分を除去)y = sales.values - sales.mean()N = len(y)freqs = np.fft.rfftfreq(N, d=1.0)        # サンプリング間隔=1日spectrum = np.abs(np.fft.rfft(y))
# 周期 = 1 / 周波数periods = np.where(freqs > 0, 1 / freqs, np.inf)
# パワーが強い順に上位5周期top = np.argsort(spectrum)[::-1][:5]for i in top:    if 2 <= periods[i] <= N / 2:        print(f"周期 {periods[i]:.1f} 日 (強さ {spectrum[i]:.0f})")
# 可視化(横軸を周期に取ると経営層に説明しやすい)plt.figure(figsize=(10, 4))plt.plot(periods[1:N//2], spectrum[1:N//2])plt.xscale("log"); plt.xlabel("周期 (日)"); plt.ylabel("強さ")plt.title("周期スペクトル: 山の位置が「効いている周期」")plt.show()
FFT が刺さる場面

「事業上気づいていない周期」を発見するのに最強。例: 「30 日周期があると思ってたら、実は 28 日周期 = 給料日サイクル」「45 日付近にも山があった = 配送頻度に紐付く」など、仮説外の周期まで見える。

5. 統計検定で「周期性の有無」をクロにする

目視や ACF で「あるっぽい」を判断するだけだと、業務報告に弱い。Friedman 検定 / Kruskal-Wallis / Seasonal Mann-Kendall などで、統計的に周期性が有意かどうか を p 値で示すと、経営層への説得力が増します。

曜日周期の有無を統計検定で判定
Python
from scipy.stats import kruskal
df = sales.to_frame("amount")df["weekday"] = df.index.dayofweek  # 0=月 ... 6=日
# 曜日ごとの売上分布が等しい (= 曜日周期なし) という帰無仮説を検定groups = [df[df["weekday"] == d]["amount"].values for d in range(7)]stat, p = kruskal(*groups)print(f"Kruskal-Wallis statistic={stat:.2f}, p={p:.4g}")if p < 0.05:    print("→ 曜日によって売上分布が有意に異なる = 曜日周期あり")else:    print("→ 曜日周期は統計的には有意でない")

6. ケーススタディ:気づかれていない 28 日周期を発見する

実際の現場では、複数の周期が重なって目視では見抜けないケースが多い。仮想 EC 店舗データで体験してみます。

意図的に多重周期を仕込んだ売上データを生成
Python
import numpy as npimport pandas as pd
np.random.seed(42)days = 365 * 2t = np.arange(days)trend = 1000 + 0.5 * t                                     # 長期成長weekly = 200 * np.sin(2 * np.pi * t / 7)                   # 曜日効果monthly = 150 * np.sin(2 * np.pi * t / 28)                 # 給料日 28 日周期noise = np.random.normal(0, 80, days)
sales = pd.Series(    trend + weekly + monthly + noise,    index=pd.date_range("2025-01-01", periods=days),    name="amount",)sales.head()

このデータを目視するだけでは、ノイズに紛れて 7 日周期と 28 日周期の両方を見抜くのは困難。実際にやってみましょう。

FFT + ACF で多重周期を発見し可視化
Python
# FFT スペクトルの上位 3 周期y = sales.values - sales.mean()freqs = np.fft.rfftfreq(len(y), d=1.0)spectrum = np.abs(np.fft.rfft(y))periods = np.where(freqs > 0, 1 / freqs, np.inf)mask = (periods >= 2) & (periods <= len(y) / 2)top = sorted(np.argsort(spectrum * mask)[::-1][:3], key=lambda i: -spectrum[i])for i in top:    print(f"発見: 周期 {periods[i]:.1f} 日, 強さ {spectrum[i]:.0f}")# 出力例:#   発見: 周期 7.0 日, 強さ 36500#   発見: 周期 28.0 日, 強さ 27300#   発見: 周期 365.0 日, 強さ 18000  ← 長期トレンドが残ったもの
# MSTL で複数周期を同時分解from statsmodels.tsa.seasonal import MSTLmstl = MSTL(sales, periods=[7, 28]).fit()mstl.plot()
経営層に出すグラフ

「FFT スペクトルで山が立っている所が、実は気づいていなかった周期です」 という1枚を見せると、納得感が一気に増します。MSTL の 4 段(元データ・トレンド・周期1・周期2・残差)も同等に強い。残差が小さければ「説明できた割合が高い」と直感的に伝わる。

7. 売上以外にも周期性は眠っている:Web アクセス・在庫・問い合わせ

本記事は売上を例に書いていますが、周期性発見の対象は売上に限りません。むしろ「気づかれていない周期」が転がりやすいのは、普段ダッシュボードで眺めるだけで深掘りしていないログです。

対象想定される周期と業務インパクト
Web サイト全体の PV曜日 / 時間帯ピーク → サーバ autoscale、メンテ枠の選定、広告出稿タイミング
特定ページの訪問数料金ページが月初に跳ねる / 採用ページが日曜夜に跳ねる → CTA文言の出し分け、コンバージョン施策
ブログ記事への流入公開後N日でロングテール周期 / 検索アルゴリズム更新タイミング → 改版時期、続編出し時期
問い合わせフォーム送信曜日 / 時間ピーク → カスタマーサポートのシフト最適化
カート投入から購入までの遅延週末/月末を越えると放棄率上昇 → リマインドメールの自動送信タイミング
のレイテンシ業務時間帯に劣化 / バッチ時間帯にピーク → 設計とスケール戦略
在庫消費スピード曜日/月次パターン → 発注タイミングの自動最適化
でのメンション数イベント連動の日次パターン → エンゲージメントの旬の見極め
Webアクセスへの応用は最も着手しやすい

GA4 / Search Console / Cloudflare Analytics などから日次・時間別のCSVが簡単に引けます。特定ページ単位 の周期分析は意外な発見が多い。例えば「料金ページは火曜10時にピーク(B2B営業前の比較検討タイム)」「採用ページは日曜21時に跳ねる(翌週応募の準備)」など、ターゲット行動の生データが浮かび上がります。

GA4 から日次PVを取得し、ページ別に周期性スコアを出す
Python
import pandas as pdfrom statsmodels.tsa.stattools import acf
# GA4 BigQuery export を SQL で日次 PV にしておく想定# columns: date, page_path, pageviewsdf = pd.read_csv("ga4_daily_pageviews.csv", parse_dates=["date"])
def cyclicality_score(series, period=7):    """指定周期での自己相関の強さをスコア化(-1〜1)"""    if len(series) < period * 2:        return None    s = series - series.mean()    a = acf(s, nlags=period, fft=True)    return float(a[period])
# ページごとに 7日周期のスコアresult = (    df.groupby("page_path")      .apply(lambda g: cyclicality_score(g.set_index("date")["pageviews"], period=7))      .dropna()      .sort_values(ascending=False)      .head(10))print("曜日周期が強いページ TOP10:")print(result)
「気づいていなかった周期」を組織で共有する

Web アクセスの周期性は マーケ・カスタマー・営業・インフラ の各部署にとって全部使えるネタ。発見ごとに Slack/Notion に「今月見つけた周期 TOP3」 のような共有を回すと、組織の感度が継続的に上がります。

8. 周期性を業務に翻訳する

発見した周期をそのまま放置するのは無価値。具体的なオペレーション変更に落とすのが本番です。

発見した周期業務への翻訳例
曜日周期 (7 日) のピークが金曜金曜の在庫を木曜夜に増配 / シフトを厚く
月次周期 (28-30 日) のピークが月末前週からプロモーションを開始、Webサーバの autoscale 倍増
四半期周期 (約90日) のピークが期末営業リソースを期末2週に集中、カスタマーへの請求を1週前倒し
年次周期 (365日) のピークがクリスマス前10月から仕入を強化、12月初旬から物流増便
未知の周期 (例: 41日) を発見まず仮説立て (給与・物流・取引先支払サイト・キャンペーン履歴) → 説明変数として回帰モデルに組込み

9. 落とし穴

短期データで判定しない

最低 2 周期分のデータがないと信頼できる検出はできない。月次周期を見たいなら最低 3-4 ヶ月、年次周期なら最低 2 年。短いデータで「周期がない」と結論するのは早計。

祝日・特売・キャンペーンの効果

カレンダー要因で激しく動くデータは、まず祝日・キャンペーン日を ダミー変数として除去してから周期分析を。Prophet の `holidays` 引数や、自前のフラグ列を使う。

ノイズ過多のデータ

SNR (signal-to-noise ratio) が低いケースでは FFT も ACF も役に立たない。移動平均で平滑化 してから分析するか、集計粒度を上げる(時間単位 → 日単位 → 週単位)ことで効果が見えることがある。

10. 実装の道具箱

手法 / ツール強み向くケース
statsmodels.STL / MSTL古典的 + 高速、結果が解釈しやすい周期長が決まっている時の主役
numpy.fft未知周期の発見に強い、依存ゼロ「何の周期があるか分からない」探索
Prophet (Meta)祝日・季節性を簡単に組込み予測も同時にやりたい時
pmdarima.auto_arima周期項を含む ARIMA の自動探索予測モデル建てる本格段階
tsfeatures (R/Python)周期性スコアを特徴量として抽出複数系列を一気に比較したい
sktime時系列 の統合フレームワーク・分類も同時に

11. ふくふくの提案:「周期発見プロジェクト」の進め方

  • Phase 1(1〜2 週間): 主要 (売上・問い合わせ数・在庫消費)に対して FFT + STL を一括実行、気づかれていない周期を3〜5 個リストアップ
  • Phase 2(2〜3 週間): 各周期の業務的な意味づけを関係部門と議論。仮説(給料日 / 配送 / 取引先支払 / キャンペーン由来)を立てて、説明変数で裏付け
  • Phase 3(1〜2 ヶ月): 周期に応じた オペレーション変更案 を作成(仕入・人員・キャンペーン)、A/B 的にロールアウト
  • Phase 4(継続運用): ダッシュボードに周期スペクトル監視を組み込み、新しい周期が立ち上がったら自動アラート

12. まとめ

  • 売上・問い合わせ・在庫の波には 気づかれていない周期 が眠っていることが多い
  • STL/MSTL は周期長が決まってる時の主役、FFT は未知の周期発見に強い、ACF は併用
  • 発見だけで終わらせず、仕入・人員・キャンペーン など具体的なオペ変更に落とすことで初めて機会損失が減る
  • 短期データ・祝日効果・ノイズの 3 つの落とし穴に注意
  • 経営層への報告は「FFT スペクトルの山」「MSTL の 4 段グラフ」「周期別売上分布のボックスプロット」が刺さる

関連記事 / 次の話

周期性発見の隣接トピック(変化点検出 / 異常イベント検出 / 状態空間モデル / 自己回帰)は読者リアクション次第で順次追加していきます。

関連記事

時系列の補間・差分・ADF検定前処理 EP.13「時系列前処理:補間・差分・ADF検定」異常検知 (STL残差で外れ値)異常検知ハンドブック EP.04KPI 設計 → 指標設計の教科書を参照。

シェア

この記事の感想を教えてください

あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。

シリーズの外も探す:

まずは、現状を聞かせてください。

要件が固まっていなくて大丈夫です。現状診断と方針提案までを無料でお手伝いします。

無料相談フォームへ hello [at] hukuhuku [dot] co [dot] jp