STEP 35:移動平均と季節調整

📉 STEP 35: 移動平均と季節調整

データから季節性を除去し、真のトレンドを見極めよう

📋 このステップで学ぶこと

  • 移動平均を使う目的と効果
  • 単純移動平均と加重移動平均の違い
  • 中心化移動平均の考え方
  • 季節分解(加法モデル・乗法モデル)
  • 季節指数の計算と季節調整
  • ExcelとPythonでの実装方法

学習時間の目安: 3.5時間

🔍 1. 移動平均とは何か

なぜ移動平均を使うのか

📌 データの「ノイズ」を除去し、本質的なパターンを見つける

生データには様々な変動が混在しています

生データの問題点:
・日々の売上は大きく変動する
・一時的なイベント(セール、天候)の影響
・曜日による違い
・測定誤差やデータ入力ミス
「木を見て森を見ず」になりがち

移動平均の効果:
・短期的な変動(ノイズ)を平滑化
・長期的なトレンドが明確に
・異常値の影響を軽減
・意思決定がしやすくなる

ビジネスでの活用シーン:

1. 経営報告
「先月の売上は前月比-5%でした」
→ 一時的な変動?本当の減少?
→ 移動平均で判断

2. 在庫管理
日々の出荷数は変動が大きい
→ 移動平均で安定した需要予測

3. KPIモニタリング
・コンバージョン率の推移
・顧客満足度スコア
→ 移動平均でトレンドを把握

移動平均のイメージ

💡 「窓」をスライドさせながら平均を計算
3期間移動平均の動き:

データ: [100, 110, 105, 115, 120, 125]

ステップ1: 窓を1〜3期間に設定
[100, 110, 105, 115, 120, 125]
→ 平均 = (100+110+105)/3 = 105

ステップ2: 窓を1つ右にスライド
[100, 110, 105, 115, 120, 125]
→ 平均 = (110+105+115)/3 = 110

ステップ3: さらに右にスライド
[100, 110, 105, 115, 120, 125]
→ 平均 = (105+115+120)/3 = 113.3

ステップ4: 最後まで
[100, 110, 105, 115, 120, 125]
→ 平均 = (115+120+125)/3 = 120

結果:
元データ: 100, 110, 105, 115, 120, 125
移動平均: -, -, 105, 110, 113, 120

→ 3月の落ち込み(105)が目立たなくなり、
  上昇トレンドが明確に!

📊 2. 移動平均の種類

単純移動平均(SMA: Simple Moving Average)

📌 すべての期間に等しい重みをつける
計算式:
SMA = (x₁ + x₂ + … + xₙ) / n

具体例(3期間移動平均):
売上: 100, 110, 120万円

SMA = (100 + 110 + 120) / 3
= 330 / 3
= 110万円

特徴:
✅ シンプルで計算が容易
✅ Excelで簡単に実装
✅ 説明しやすい(経営層向け)
✅ ノイズを効果的に除去

欠点:
❌ すべての期間を同じ重みで扱う
❌ 最近の変化に鈍感
❌ 急激な変化への反応が遅い
❌ トレンド転換の検出が遅れる

加重移動平均(WMA: Weighted Moving Average)

💡 最近のデータに大きな重みをつける
計算式:
WMA = Σ(wᵢ × xᵢ) / Σwᵢ

具体例(3期間、重み: 1, 2, 3):
売上: 100, 110, 120万円
重み: 1(古い), 2(中間), 3(最新)

WMA = (1×100 + 2×110 + 3×120) / (1+2+3)
= (100 + 220 + 360) / 6
= 680 / 6
= 113.3万円

SMAとの比較:
SMA = 110.0万円
WMA = 113.3万円
差 = +3.3万円

WMAは上昇トレンドをより反映!

なぜ差が出るか:
・SMA: 100, 110, 120を等しく扱う
・WMA: 120を最も重視(重み3)
→ 最新の上昇傾向を強く反映

用途:
・トレンド転換を早く検出したい
・株価のテクニカル分析
・短期予測

中心化移動平均

📌 偶数期間の移動平均を正確に計算する方法
問題: 12ヶ月移動平均はどこに配置する?

例: 1月〜12月のデータで12ヶ月MA計算
→ この平均値は「何月」の値?
→ 6月と7月の間(6.5月)に相当
→ グラフ上でずれてしまう!

解決策: 2×12移動平均

ステップ1: 12ヶ月移動平均を計算
MA₁ = (1月〜12月の平均) → 6.5月の位置
MA₂ = (2月〜翌1月の平均) → 7.5月の位置

ステップ2: 隣り合う2つの平均を再平均
中心化MA = (MA₁ + MA₂) / 2
→ 7月の位置に配置!

Pythonでの実装:
df['MA_12'] = df['sales'].rolling(12, center=True).mean()

なぜ重要か:
・季節分解で正確な結果を得るため
・トレンドと元データの比較を正確に
・実務では自動計算されることが多い

期間の選び方

💡 目的に応じて最適な期間を選択
期間 特徴 用途 注意点
3ヶ月 変化に敏感
ノイズ残る
短期トレンド
月次レポート
季節性は除去できない
6ヶ月 バランス良い
実務で人気
中期トレンド
四半期レポート
半年周期は除去可能
12ヶ月 年次季節性除去
滑らか
長期トレンド
年次計画
変化への反応遅い

実務のコツ:
・迷ったら3ヶ月と12ヶ月を併記
・短期と長期の両方の視点を提供
・経営層は「両方見たい」ことが多い

💻 3. Pythonでの移動平均実装

基本的な実装

# ============================================ # 移動平均の実装 # ============================================ # このセクションで学ぶこと: # 1. pandas の rolling() 関数の使い方 # 2. 単純移動平均(SMA)の計算 # 3. 加重移動平均(WMA)の計算 # 4. 期間による平滑化効果の違い import pandas as pd import numpy as np import matplotlib.pyplot as plt # ============================================ # サンプルデータ作成 # ============================================ # 実際のビジネスデータに近い形で作成 # – 上昇トレンド(成長企業を想定) # – 年次季節性(夏に売上増) # – ランダムなノイズ(日々の変動) np.random.seed(42) # 再現性のため固定 # 36ヶ月分の日付を生成 # freq=’MS’: Month Start(月初) months = pd.date_range(‘2022-01-01’, ‘2024-12-01′, freq=’MS’) # 時系列の3要素を作成 # 1. トレンド: 100万円 → 150万円へ線形に成長 trend = np.linspace(100, 150, len(months)) # 2. 季節性: 振幅±20万円の年次サイクル # 2*pi/12 = 12ヶ月で1周期 seasonality = 20 * np.sin(np.arange(len(months)) * 2 * np.pi / 12) # 3. ノイズ: 標準偏差5万円のランダム変動 noise = np.random.normal(0, 5, len(months)) # 3要素を合成 sales = trend + seasonality + noise # データフレーム作成 df = pd.DataFrame({ ‘date’: months, ‘sales’: sales }) print(“【サンプルデータ確認】”) print(f”期間: {df[‘date’].min().strftime(‘%Y年%m月’)} 〜 {df[‘date’].max().strftime(‘%Y年%m月’)}”) print(f”データ数: {len(df)}ヶ月”) print(f”売上範囲: {df[‘sales’].min():.1f} 〜 {df[‘sales’].max():.1f}万円”) print()

rolling()関数の詳細

# ============================================ # pandas の rolling() 関数 # ============================================ # rolling() は「窓関数」を適用するための関数 # # 主なパラメータ: # window: 窓のサイズ(何期間分を使うか) # min_periods: 計算に必要な最小データ数(デフォルト=window) # center: 窓の中心を現在位置にするか(デフォルト=False) # ============================================ # 1. 基本的な使い方 # ============================================ # 3ヶ月移動平均 df[‘SMA_3’] = df[‘sales’].rolling(window=3).mean() # 上記は以下と同じ意味: # df[‘SMA_3’] = df[‘sales’].rolling( # window=3, # 3期間の窓 # min_periods=3, # 最低3期間必要(最初の2行はNaN) # center=False # 窓の右端が現在位置 # ).mean() # ============================================ # 2. 複数期間の移動平均 # ============================================ df[‘SMA_6’] = df[‘sales’].rolling(window=6).mean() # 6ヶ月 df[‘SMA_12’] = df[‘sales’].rolling(window=12).mean() # 12ヶ月 # ============================================ # 3. 中心化移動平均(center=True) # ============================================ # center=True: 窓の中心が現在位置になる # → 季節分解でよく使用 df[‘SMA_12_center’] = df[‘sales’].rolling(window=12, center=True).mean() # 違いの確認 print(“【center=True と center=False の違い】”) print() print(“通常の12ヶ月MA(center=False):”) print(” → 1月〜12月のデータで12月に値が入る”) print(” → 「過去12ヶ月の平均」を表す”) print() print(“中心化12ヶ月MA(center=True):”) print(” → 1月〜12月のデータで6月付近に値が入る”) print(” → 「その時点のトレンド」を表す”) print() # ============================================ # 4. min_periods の使い方 # ============================================ # min_periods: 計算に必要な最小データ数 # → NaNを減らしたい時に使用 df[‘SMA_3_min1’] = df[‘sales’].rolling(window=3, min_periods=1).mean() print(“【min_periods の効果】”) print() print(“min_periods=3(デフォルト):”) print(df[[‘date’, ‘sales’, ‘SMA_3’]].head(5).to_string(index=False)) print() print(“min_periods=1:”) print(df[[‘date’, ‘sales’, ‘SMA_3_min1’]].head(5).to_string(index=False)) print() print(“→ min_periods=1 だと1期目から計算される(データ不足でも)”)

加重移動平均の実装

# ============================================ # 加重移動平均(WMA)の実装 # ============================================ # pandasには標準でWMA関数がないため、自作する def weighted_moving_average(series, weights): “”” 加重移動平均を計算する関数 Parameters: ———– series : pd.Series or np.array 時系列データ weights : list or np.array 重み(古いデータから順に指定) 例: [1, 2, 3] → 最も古いデータに1、最新に3 Returns: ——– list : 加重移動平均値のリスト 計算式: WMA = Σ(weight_i × value_i) / Σ(weight_i) “”” weights = np.array(weights) result = [] for i in range(len(series)): # 窓サイズ分のデータがない場合はNaN if i < len(weights) - 1: result.append(np.nan) else: # 直近n期間のデータを取得 window = series[i - len(weights) + 1 : i + 1] # 加重平均を計算 wma = np.sum(window * weights) / np.sum(weights) result.append(wma) return result # ============================================ # 線形重みでWMA計算 # ============================================ # 重み: [1, 2, 3] → 最新データを最も重視 weights_linear = np.array([1, 2, 3]) df['WMA_3'] = weighted_moving_average(df['sales'].values, weights_linear) # ============================================ # SMAとWMAの比較 # ============================================ print("【SMA vs WMA の比較】") print() print("直近6ヶ月のデータ:") print(df[['date', 'sales', 'SMA_3', 'WMA_3']].tail(6).to_string(index=False)) print() # 差を計算 df['SMA_WMA_diff'] = df['WMA_3'] - df['SMA_3'] recent_diff = df['SMA_WMA_diff'].tail(6).mean() print(f"WMA - SMA の平均差: {recent_diff:+.2f}万円") print() if recent_diff > 0: print(“→ WMA > SMA: 上昇トレンドをWMAがより反映”) else: print(“→ WMA < SMA: 下降トレンドをWMAがより反映")

可視化

# ============================================ # 可視化 # ============================================ fig, axes = plt.subplots(2, 2, figsize=(16, 10)) # ============================================ # グラフ1: 期間別の移動平均比較 # ============================================ ax1 = axes[0, 0] ax1.plot(df[‘date’], df[‘sales’], label=’実績’, alpha=0.4, marker=’o’, markersize=3, color=’gray’) ax1.plot(df[‘date’], df[‘SMA_3′], label=’3ヶ月MA’, linewidth=2, color=’red’) ax1.plot(df[‘date’], df[‘SMA_6′], label=’6ヶ月MA’, linewidth=2, color=’blue’) ax1.plot(df[‘date’], df[‘SMA_12′], label=’12ヶ月MA’, linewidth=2, color=’green’) ax1.set_title(‘期間別 移動平均の比較’, fontsize=13, fontweight=’bold’) ax1.set_ylabel(‘売上(万円)’, fontsize=11) ax1.legend(loc=’upper left’) ax1.grid(alpha=0.3) # ============================================ # グラフ2: SMA vs WMA # ============================================ ax2 = axes[0, 1] ax2.plot(df[‘date’], df[‘sales’], label=’実績’, alpha=0.4, marker=’o’, markersize=3, color=’gray’) ax2.plot(df[‘date’], df[‘SMA_3′], label=’単純MA(3ヶ月)’, linewidth=2, color=’blue’) ax2.plot(df[‘date’], df[‘WMA_3′], label=’加重MA(3ヶ月)’, linewidth=2, linestyle=’–‘, color=’red’) ax2.set_title(‘単純移動平均 vs 加重移動平均’, fontsize=13, fontweight=’bold’) ax2.set_ylabel(‘売上(万円)’, fontsize=11) ax2.legend(loc=’upper left’) ax2.grid(alpha=0.3) # ============================================ # グラフ3: 平滑化効果の比較 # ============================================ ax3 = axes[1, 0] ax3.plot(df[‘date’], df[‘sales’], label=’元データ’, alpha=0.6, color=’gray’, linewidth=1) ax3.plot(df[‘date’], df[‘SMA_3′], label=’3ヶ月MA(弱い平滑化)’, linewidth=2, color=’orange’) ax3.plot(df[‘date’], df[‘SMA_12′], label=’12ヶ月MA(強い平滑化)’, linewidth=2, color=’purple’) ax3.set_title(‘期間による平滑化効果の違い’, fontsize=13, fontweight=’bold’) ax3.set_xlabel(‘日付’, fontsize=11) ax3.set_ylabel(‘売上(万円)’, fontsize=11) ax3.legend(loc=’upper left’) ax3.grid(alpha=0.3) # ============================================ # グラフ4: 変動の大きさ比較(標準偏差) # ============================================ ax4 = axes[1, 1] labels = [‘元データ’, ‘3ヶ月MA’, ‘6ヶ月MA’, ’12ヶ月MA’] stds = [ df[‘sales’].std(), df[‘SMA_3’].std(), df[‘SMA_6’].std(), df[‘SMA_12’].std() ] colors = [‘gray’, ‘red’, ‘blue’, ‘green’] bars = ax4.bar(labels, stds, color=colors, alpha=0.7, edgecolor=’black’) ax4.set_ylabel(‘標準偏差(変動の大きさ)’, fontsize=11) ax4.set_title(‘移動平均による平滑化効果’, fontsize=13, fontweight=’bold’) ax4.grid(axis=’y’, alpha=0.3) # 値を表示 for bar, std in zip(bars, stds): ax4.text(bar.get_x() + bar.get_width()/2, bar.get_height(), f'{std:.1f}’, ha=’center’, va=’bottom’, fontsize=11) plt.tight_layout() plt.show() print(“【平滑化効果の数値比較】”) print(f”元データの標準偏差: {df[‘sales’].std():.2f}”) print(f”3ヶ月MAの標準偏差: {df[‘SMA_3’].std():.2f} (元の{df[‘SMA_3’].std()/df[‘sales’].std()*100:.0f}%)”) print(f”6ヶ月MAの標準偏差: {df[‘SMA_6’].std():.2f} (元の{df[‘SMA_6’].std()/df[‘sales’].std()*100:.0f}%)”) print(f”12ヶ月MAの標準偏差: {df[‘SMA_12’].std():.2f} (元の{df[‘SMA_12’].std()/df[‘sales’].std()*100:.0f}%)”) print() print(“→ 期間が長いほど変動が小さくなる(平滑化効果が大きい)”)

移動平均のラグ(遅延)問題

⚠️ 移動平均は「過去を見ている」ため反応が遅れる
ラグとは:
・移動平均は過去n期間のデータを使う
・そのため、変化が起きてもすぐには反映されない
・期間が長いほどラグが大きい

実務での問題:
・トレンド転換の検出が遅れる
・急激な売上変化に気づかない
・意思決定のタイミングを逃す

対策:
・短期MAと長期MAを併用
・加重移動平均(WMA)を使う
・指数平滑法を検討(STEP 34参照)
# ============================================ # 移動平均のラグ(遅延)問題の可視化 # ============================================ # 問題: 急激な変化が起きた時、移動平均は # どれくらい遅れて反応するか? # 急激な変化を含むデータを作成 df_lag = df.copy() change_point = 20 # 20ヶ月目に急上昇 # 20ヶ月目以降、売上が30万円増加 df_lag.loc[change_point:, ‘sales’] = df_lag.loc[change_point:, ‘sales’] + 30 # 移動平均を再計算 df_lag[‘SMA_3’] = df_lag[‘sales’].rolling(window=3).mean() df_lag[‘SMA_6’] = df_lag[‘sales’].rolling(window=6).mean() df_lag[‘SMA_12’] = df_lag[‘sales’].rolling(window=12).mean() # 可視化 fig, axes = plt.subplots(1, 2, figsize=(16, 5)) # グラフ1: ラグの比較 ax1 = axes[0] ax1.plot(df_lag[‘date’], df_lag[‘sales’], label=’実績(急上昇あり)’, alpha=0.6, marker=’o’, markersize=3, color=’gray’) ax1.plot(df_lag[‘date’], df_lag[‘SMA_3′], label=’3ヶ月MA’, linewidth=2, color=’red’) ax1.plot(df_lag[‘date’], df_lag[‘SMA_12′], label=’12ヶ月MA’, linewidth=2, color=’blue’) ax1.axvline(x=df_lag[‘date’].iloc[change_point], color=’green’, linestyle=’–‘, linewidth=2, label=’変化点’) ax1.set_title(‘移動平均のラグ(遅延)’, fontsize=13, fontweight=’bold’) ax1.set_xlabel(‘日付’, fontsize=11) ax1.set_ylabel(‘売上(万円)’, fontsize=11) ax1.legend() ax1.grid(alpha=0.3) # グラフ2: ラグの大きさを数値で比較 ax2 = axes[1] # 変化点から何ヶ月後に追いつくか計算 target_level = df_lag[‘sales’].iloc[change_point:].mean() # 各MAが目標レベルの90%に達するまでの期間 def months_to_catch_up(ma_series, target, start_idx): threshold = target * 0.9 for i, val in enumerate(ma_series.iloc[start_idx:]): if not np.isnan(val) and val >= threshold: return i return len(ma_series) – start_idx lag_3 = months_to_catch_up(df_lag[‘SMA_3’], target_level, change_point) lag_6 = months_to_catch_up(df_lag[‘SMA_6’], target_level, change_point) lag_12 = months_to_catch_up(df_lag[‘SMA_12’], target_level, change_point) labels = [‘3ヶ月MA’, ‘6ヶ月MA’, ’12ヶ月MA’] lags = [lag_3, lag_6, lag_12] colors = [‘red’, ‘orange’, ‘blue’] bars = ax2.bar(labels, lags, color=colors, alpha=0.7, edgecolor=’black’) ax2.set_ylabel(‘追いつくまでの月数’, fontsize=11) ax2.set_title(‘ラグの大きさ比較’, fontsize=13, fontweight=’bold’) ax2.grid(axis=’y’, alpha=0.3) for bar, lag in zip(bars, lags): ax2.text(bar.get_x() + bar.get_width()/2, bar.get_height(), f'{lag}ヶ月’, ha=’center’, va=’bottom’, fontsize=12) plt.tight_layout() plt.show() print(“【ラグ問題のまとめ】”) print() print(f”変化点: {df_lag[‘date’].iloc[change_point].strftime(‘%Y年%m月’)}”) print(f”3ヶ月MA: 約{lag_3}ヶ月で追いつく(反応が速い)”) print(f”6ヶ月MA: 約{lag_6}ヶ月で追いつく”) print(f”12ヶ月MA: 約{lag_12}ヶ月で追いつく(反応が遅い)”) print() print(“→ トレンド転換を早く検出したい場合は短期MAを使う”) print(“→ ノイズを除去したい場合は長期MAを使う”) print(“→ 両方を併用するのがベストプラクティス”)

🌊 4. 季節性の理解と分解

季節性とは

📌 周期的に繰り返されるパターン
季節性の種類:

1. 年次季節性(12ヶ月周期)
・アイスクリーム: 夏に売上増
・暖房器具: 冬に売上増
・お歳暮: 12月に需要集中
・入学グッズ: 3〜4月に需要集中

2. 週次季節性(7日周期)
・外食: 金土日に客数増
・ECサイト: 週末にアクセス増
・法人向けサービス: 平日に需要集中

3. 日次季節性(24時間周期)
・カフェ: 朝と昼にピーク
・居酒屋: 夜にピーク
・電力使用量: 日中にピーク

なぜ季節性を分析するのか:
✅ 真のトレンドを見極める
✅ 正確な前年同月比を計算
✅ 需要予測の精度向上
✅ 在庫・人員計画の最適化
✅ 異常値の検出が容易に

加法モデルと乗法モデル

💡 どちらのモデルを選ぶか
加法モデル(Additive):
売上 = トレンド + 季節性 + 残差

特徴:
・季節変動が「金額」で一定
・例: 毎年冬に+20万円
・売上100万でも1000万でも+20万

向いているケース:
・売上規模が安定している
・季節変動が金額ベースで一定
・成熟した市場


乗法モデル(Multiplicative):
売上 = トレンド × 季節性 × 残差

特徴:
・季節変動が「割合」で一定
・例: 毎年冬は売上の×1.2倍
・売上100万なら+20万、1000万なら+200万

向いているケース:
・売上が成長/縮小している
・季節変動が比例的
・成長企業やスタートアップ


選び方のコツ:
1. グラフを見て季節変動の幅を確認
2. 変動幅が一定 → 加法モデル
3. 変動幅が拡大/縮小 → 乗法モデル
4. 迷ったら両方試して比較

季節分解の実装

# ============================================ # 季節分解の実装 # ============================================ # 季節分解とは: # 時系列データを3つの成分に分解する手法 # 1. トレンド: 長期的な傾向 # 2. 季節性: 周期的なパターン # 3. 残差: 説明できない変動(ノイズ) from statsmodels.tsa.seasonal import seasonal_decompose # ============================================ # 加法モデルによる季節分解 # ============================================ # seasonal_decompose() のパラメータ: # model: ‘additive’(加法)or ‘multiplicative’(乗法) # period: 季節周期(月次データで年次季節性なら12) # extrapolate_trend: トレンドの端の欠損を補完 result_add = seasonal_decompose( df[‘sales’], # 分解するデータ model=’additive’, # 加法モデル period=12 # 12ヶ月周期 ) # 分解結果の確認 print(“【加法モデルの季節分解結果】”) print() print(“元データ = トレンド + 季節性 + 残差”) print() # 各成分を確認(中央の月を例に) mid_idx = len(df) // 2 print(f”例: {df[‘date’].iloc[mid_idx].strftime(‘%Y年%m月’)}”) print(f” 元データ: {df[‘sales’].iloc[mid_idx]:.1f}万円”) print(f” トレンド: {result_add.trend.iloc[mid_idx]:.1f}万円”) print(f” 季節性: {result_add.seasonal.iloc[mid_idx]:+.1f}万円”) print(f” 残差: {result_add.resid.iloc[mid_idx]:+.1f}万円”) print(f” 合計: {result_add.trend.iloc[mid_idx] + result_add.seasonal.iloc[mid_idx] + result_add.resid.iloc[mid_idx]:.1f}万円”) print() # ============================================ # 乗法モデルによる季節分解 # ============================================ result_mult = seasonal_decompose( df[‘sales’], model=’multiplicative’, period=12 ) print(“【乗法モデルの季節分解結果】”) print() print(“元データ = トレンド × 季節性 × 残差”) print() print(f”例: {df[‘date’].iloc[mid_idx].strftime(‘%Y年%m月’)}”) print(f” 元データ: {df[‘sales’].iloc[mid_idx]:.1f}万円”) print(f” トレンド: {result_mult.trend.iloc[mid_idx]:.1f}万円”) print(f” 季節性: {result_mult.seasonal.iloc[mid_idx]:.3f}(×倍率)”) print(f” 残差: {result_mult.resid.iloc[mid_idx]:.3f}(×倍率)”)

季節分解の可視化

# ============================================ # 季節分解の可視化 # ============================================ fig, axes = plt.subplots(4, 1, figsize=(14, 12)) # 1. 元データ axes[0].plot(df[‘date’], df[‘sales’], color=’black’, linewidth=1) axes[0].set_title(‘① 元データ(売上)’, fontsize=12, fontweight=’bold’) axes[0].set_ylabel(‘売上(万円)’, fontsize=10) axes[0].grid(alpha=0.3) # 2. トレンド成分 axes[1].plot(df[‘date’], result_add.trend, color=’blue’, linewidth=2) axes[1].set_title(‘② トレンド成分(長期的な傾向)’, fontsize=12, fontweight=’bold’) axes[1].set_ylabel(‘売上(万円)’, fontsize=10) axes[1].grid(alpha=0.3) # 解説テキスト axes[1].text(0.02, 0.95, ‘→ 12ヶ月移動平均で算出\n→ 季節性を除いた本質的な成長’, transform=axes[1].transAxes, fontsize=10, verticalalignment=’top’, bbox=dict(boxstyle=’round’, facecolor=’wheat’, alpha=0.5)) # 3. 季節成分 axes[2].plot(df[‘date’], result_add.seasonal, color=’green’, linewidth=1) axes[2].axhline(y=0, color=’black’, linestyle=’–‘, alpha=0.5) axes[2].set_title(‘③ 季節成分(周期的なパターン)’, fontsize=12, fontweight=’bold’) axes[2].set_ylabel(‘季節効果(万円)’, fontsize=10) axes[2].grid(alpha=0.3) # 解説テキスト axes[2].text(0.02, 0.95, ‘→ 毎年同じパターンが繰り返される\n→ プラス=平均より高い月、マイナス=低い月’, transform=axes[2].transAxes, fontsize=10, verticalalignment=’top’, bbox=dict(boxstyle=’round’, facecolor=’lightgreen’, alpha=0.5)) # 4. 残差成分 axes[3].plot(df[‘date’], result_add.resid, color=’red’, linewidth=1, alpha=0.7) axes[3].axhline(y=0, color=’black’, linestyle=’–‘, alpha=0.5) axes[3].set_title(‘④ 残差成分(説明できない変動)’, fontsize=12, fontweight=’bold’) axes[3].set_xlabel(‘日付’, fontsize=10) axes[3].set_ylabel(‘残差(万円)’, fontsize=10) axes[3].grid(alpha=0.3) # 解説テキスト axes[3].text(0.02, 0.95, ‘→ トレンドと季節性で説明できない部分\n→ ランダムなノイズや一時的なイベント’, transform=axes[3].transAxes, fontsize=10, verticalalignment=’top’, bbox=dict(boxstyle=’round’, facecolor=’lightyellow’, alpha=0.5)) plt.tight_layout() plt.show() print(“【季節分解の解釈】”) print() print(“トレンド: 100万円 → 150万円へ成長(+50%)”) print(“季節性: 夏にプラス、冬にマイナスのパターン”) print(“残差: ±5万円程度のランダムな変動”)

📈 5. 季節指数と季節調整

季節指数とは

📌 各月の「平均からの乖離」を数値化
季節指数の意味:
・年間平均を100とした場合の相対値
・季節指数120 = 平均の1.2倍(20%高い)
・季節指数80 = 平均の0.8倍(20%低い)

計算式:
季節指数 = (その月の平均) / (全体の平均) × 100

例:
・年間平均売上: 100万円
・7月の平均売上: 130万円
・7月の季節指数 = 130 / 100 × 100 = 130
→ 「7月は平均より30%高い」

12ヶ月の季節指数の合計 = 1200
(12ヶ月 × 平均100 = 1200)

季節指数の計算

# ============================================ # 季節指数の計算 # ============================================ # ステップ: # 1. 月を抽出 # 2. 月別の平均売上を計算 # 3. 全体平均で割って指数化 # ステップ1: 月を抽出 df[‘month’] = df[‘date’].dt.month # ステップ2: 月別平均売上 monthly_avg = df.groupby(‘month’)[‘sales’].mean() # ステップ3: 全体平均 overall_avg = df[‘sales’].mean() # ステップ4: 季節指数(全体平均を100として) seasonal_index = (monthly_avg / overall_avg * 100).round(1) # 結果表示 print(“【季節指数の計算結果】”) print() print(f”全体平均売上: {overall_avg:.1f}万円”) print() month_names = [‘1月’, ‘2月’, ‘3月’, ‘4月’, ‘5月’, ‘6月’, ‘7月’, ‘8月’, ‘9月’, ’10月’, ’11月’, ’12月’] print(“月別売上と季節指数:”) print(“-” * 50) for month in range(1, 13): avg = monthly_avg[month] idx = seasonal_index[month] # 棒グラフ風の表示 bar_length = int((idx – 70) / 2) # 70〜130を0〜30にスケール bar = ‘█’ * max(0, bar_length) if idx > 100: comment = f”+{idx-100:.0f}%” color_hint = “↑高” elif idx < 100: comment = f"{idx-100:.0f}%" color_hint = "↓低" else: comment = "±0%" color_hint = "→平均" print(f"{month_names[month-1]}: 平均{avg:6.1f}万円 → 指数{idx:5.1f} ({comment:>5}) {bar}”) print(“-” * 50) print(f”季節指数の合計: {seasonal_index.sum():.0f}(理論値: 1200)”) print() # 季節性の強さを評価 max_idx = seasonal_index.max() min_idx = seasonal_index.min() range_idx = max_idx – min_idx print(“【季節性の強さ】”) print(f”最高: {month_names[seasonal_index.idxmax()-1]} = {max_idx:.1f}”) print(f”最低: {month_names[seasonal_index.idxmin()-1]} = {min_idx:.1f}”) print(f”変動幅: {range_idx:.1f}ポイント”) print() if range_idx > 30: print(“→ 季節性が強い(季節調整が必須)”) elif range_idx > 15: print(“→ 季節性がやや強い(季節調整を推奨)”) else: print(“→ 季節性は弱い(季節調整は任意)”)

季節調整の実施

💡 季節要因を除去して「真の実力」を見る
季節調整の計算式:
季節調整済み売上 = 実績 ÷ (季節指数 / 100)

具体例:
・12月の実績: 240万円
・12月の季節指数: 120

季節調整済み = 240 ÷ (120/100)
= 240 ÷ 1.2
= 200万円

解釈:
「12月は季節的に売れやすい月なので、
 季節要因を除くと実質200万円相当の実力」

なぜ割るのか:
季節指数120 = 平均の1.2倍売れやすい
→ 1.2で割ると「平均的な月だったら」の値に
# ============================================ # 季節調整の実施 # ============================================ # 季節指数をデータフレームにマッピング # .map(): 月(1〜12)を対応する季節指数に変換 df[‘seasonal_index’] = df[‘month’].map(seasonal_index / 100) # 季節調整済み売上 = 実績 / 季節指数 df[‘seasonally_adjusted’] = df[‘sales’] / df[‘seasonal_index’] print(“【季節調整の結果】”) print() print(df[[‘date’, ‘sales’, ‘seasonal_index’, ‘seasonally_adjusted’]].tail(12).to_string(index=False)) print() # ============================================ # 季節調整の効果を確認 # ============================================ print(“【季節調整の効果】”) print() print(f”元データの標準偏差: {df[‘sales’].std():.2f}万円”) print(f”季節調整後の標準偏差: {df[‘seasonally_adjusted’].std():.2f}万円”) print(f”変動の減少率: {(1 – df[‘seasonally_adjusted’].std()/df[‘sales’].std())*100:.1f}%”) print() print(“→ 季節変動が除去され、データが平滑化されました”)

🎯 6. 実務での活用

前年同月比の正しい計算

# ============================================ # 前年同月比の計算 # ============================================ # 問題: 単純な前年同月比は季節要因を含んでしまう # 解決: 季節調整後のデータで比較する # 1. 単純な前年同月比(季節調整なし) df[‘yoy_raw’] = df.groupby(‘month’)[‘sales’].pct_change(periods=12) * 100 # 2. 季節調整後の前月比 df[‘mom_adjusted’] = df[‘seasonally_adjusted’].pct_change(periods=1) * 100 print(“【前年同月比 vs 季節調整後前月比】”) print() print(“直近12ヶ月:”) print(df[[‘date’, ‘sales’, ‘yoy_raw’, ‘seasonally_adjusted’, ‘mom_adjusted’]].tail(12).to_string(index=False)) print() # 解釈の例 last_row = df.iloc[-1] print(“【直近月の解釈】”) print(f”実績: {last_row[‘sales’]:.1f}万円”) print(f”前年同月比: {last_row[‘yoy_raw’]:+.1f}%”) print(f”季節調整済み: {last_row[‘seasonally_adjusted’]:.1f}万円”) print(f”季節調整後前月比: {last_row[‘mom_adjusted’]:+.1f}%”) print() print(“→ 季節調整後の前月比で「真の成長」を判断”)

Excelでの実装手順

💡 Excelで季節調整を行う具体的手順
データ準備(A〜C列):
A列: 日付(2022/1/1, 2022/2/1, …)
B列: 月(=MONTH(A2))
C列: 売上(実績値)

ステップ1: 12ヶ月移動平均(D列)
D7に入力(7行目=7月から計算開始):
=AVERAGE(C2:C13)
→ D7以降にコピー

ステップ2: 季節比率(E列)
E7に入力:
=C7/D7
→ E列全体にコピー

ステップ3: 月別平均季節比率(別シート)
G2に入力(1月の平均):
=AVERAGEIF($B$2:$B$37, 1, $E$2:$E$37)
→ G2:G13にコピー(2〜12月)

ステップ4: 季節指数の正規化(H列)
H2に入力:
=G2/AVERAGE($G$2:$G$13)*100
→ H列全体にコピー

ステップ5: 季節調整済み(元シートF列)
F2に入力:
=C2/VLOOKUP(B2, 季節指数!$A$2:$B$13, 2, FALSE)*100
→ F列全体にコピー

⚠️ 7. 実務でよくある間違い

移動平均・季節調整の落とし穴

❌ 間違い1: 季節調整前後のデータを混同する
問題:
「12月の売上は240万円、11月は200万円。
 20%も成長した!」

→ でも12月は季節的に売れやすい月…

正しい分析:
・12月の実績: 240万円
・12月の季節指数: 120
・季節調整済み: 240÷1.2 = 200万円
・11月の季節調整済み: 190万円
・真の成長率: +5.3%

対策:
・常に「季節調整済み」を明記
・レポートには両方の数値を記載
・「季節要因を除くと…」と説明
❌ 間違い2: 期間選択の根拠がない
問題:
「なんとなく3ヶ月移動平均を使っている」
「前任者がそうしていたから」

正しいアプローチ:

目的から逆算:
・季節性を除去したい → 12ヶ月MA
・短期トレンドを見たい → 3ヶ月MA
・バランス重視 → 6ヶ月MA

データ特性を確認:
・月次データで年次季節性 → 12ヶ月
・週次データで週次季節性 → 7日
・日次データで曜日効果 → 7日

対策:
・期間選択の理由をドキュメント化
・複数期間を試して比較
・経営層に説明できるようにする
❌ 間違い3: 移動平均だけで予測する
問題:
「来月の売上は今月の3ヶ月MAと同じ」
→ これは予測ではなく、過去の平均

移動平均の限界:
・過去のデータしか見ていない
・トレンドを外挿できない
・季節性を考慮した予測ができない

正しい使い方:
・移動平均 = 平滑化・トレンド把握
・予測 = 指数平滑法、ARIMA、Prophet
 (STEP 34参照)

対策:
・目的を明確に(分析 or 予測)
・予測には適切な手法を選択
・移動平均は「現状把握」に使う
❌ 間違い4: 異常値を無視して移動平均を計算
問題:
「コロナで4月の売上が0円だった」
「その月を含めて移動平均を計算」
→ 3ヶ月後まで移動平均が歪む

影響:
・3ヶ月MA: 3ヶ月間影響
・12ヶ月MA: 12ヶ月間影響
→ 長期MAほど影響が長引く

対策:
・異常値を検出して除外or補正
・前年同月の値で代替
・線形補間で埋める
・異常値を含む期間を明記

チェックリスト

✅ 移動平均・季節調整を使う前の確認
データ確認:
□ 欠損値はないか?
□ 異常値はないか?
□ データは十分な期間があるか?
 (12ヶ月MA → 最低24ヶ月必要)

目的確認:
□ 平滑化?トレンド把握?予測?
□ 季節性を除去する必要があるか?
□ 短期と長期どちらを重視?

手法選択:
□ 期間の根拠は説明できるか?
□ 加法モデルか乗法モデルか?
□ 結果は常識的に正しいか?

報告準備:
□ 元データと加工後の両方を示せるか?
□ 手法の限界を説明できるか?
□ 経営層に理解してもらえるか?

📝 STEP 35 のまとめ

✅ このステップで学んだこと
  • 移動平均の目的: ノイズを除去しトレンドを把握
  • 単純移動平均(SMA): シンプルで説明しやすい
  • 加重移動平均(WMA): 最近のデータを重視
  • 中心化移動平均: 季節分解で正確な結果を得る
  • 季節分解: トレンド + 季節性 + 残差に分解
  • 加法/乗法モデル: データ特性で使い分け
  • 季節指数: 各月の季節効果を数値化
  • 季節調整: 季節要因を除去し真の実力を見る
💡 実務での判断フロー

1. データの季節性を確認
・グラフで周期的なパターンがあるか
・季節指数の変動幅が15以上か

2. 季節性がある場合
・12ヶ月移動平均でトレンド把握
・季節指数を計算
・季節調整済みデータで分析

3. 季節性がない場合
・3〜6ヶ月移動平均でノイズ除去
・そのまま分析可能

4. 報告時のポイント
・元データと季節調整済み両方を提示
・「季節要因を除くと…」と明示
・経営層は両方の視点を求める

📝 練習問題

問題 1 基礎

以下の売上データについて、3ヶ月単純移動平均を計算してください。

1月: 100万円
2月: 110万円
3月: 105万円
4月: 115万円
5月: 120万円
6月: 125万円

【解答】
3ヶ月移動平均:

1月: – (データ不足)
2月: – (データ不足)
3月: (100 + 110 + 105) / 3 = 105.0万円
4月: (110 + 105 + 115) / 3 = 110.0万円
5月: (105 + 115 + 120) / 3 = 113.3万円
6月: (115 + 120 + 125) / 3 = 120.0万円

解説:

計算のポイント:

1. 窓の動き
3月: [1月, 2月, 3月] の平均
4月: [2月, 3月, 4月] の平均
→ 窓が1ヶ月ずつ右にスライド

2. 平滑化効果
元データ: 100→110→105→115→120→125
     (3月に一時的な下落)
移動平均: 105→110→113→120
     (滑らかな上昇トレンド)

3. 欠損値
最初の2期間は計算不可(NaN)
→ 3期間分のデータが必要なため
問題 2 基礎

3ヶ月間のデータ(100, 110, 120万円)について、
単純移動平均(SMA)と加重移動平均(WMA、重み1:2:3)を
それぞれ計算してください。

どちらの値が大きくなりますか?その理由も説明してください。

【解答】WMA(113.3万円) > SMA(110.0万円)
SMAの計算:
= (100 + 110 + 120) / 3
= 330 / 3
= 110.0万円

WMAの計算(重み1:2:3):
= (1×100 + 2×110 + 3×120) / (1+2+3)
= (100 + 220 + 360) / 6
= 680 / 6
= 113.3万円

理由:

データは上昇トレンド(100→110→120)

SMA: すべて同じ重み
→ 古い100も最新の120も同じ影響力

WMA: 最新データを重視
→ 120に重み3(最大)
→ 100に重み1(最小)
→ 上昇トレンドをより反映

結論:
・上昇トレンド → WMA > SMA
・下降トレンド → WMA < SMA
・横ばい → WMA ≒ SMA
問題 3 応用

ある商品の月別売上と季節指数が以下の通りです。

2024年12月の実績: 240万円
12月の季節指数: 120
11月の季節調整済み売上: 190万円

この商品の12月の季節調整済み売上を計算し、
11月と比較した真の成長率を求めてください。

【解答】季節調整済み = 200万円、成長率 = +5.3%
1. 季節調整済み売上の計算

季節調整済み = 実績 ÷ (季節指数 / 100)
= 240 ÷ (120 / 100)
= 240 ÷ 1.2
= 200万円

2. 前月比成長率

成長率 = (今月 – 前月) / 前月 × 100%
= (200 – 190) / 190 × 100%
= 10 / 190 × 100%
= +5.3%

解説:

なぜ季節調整が重要か:

単純比較(誤り):
11月 → 12月: 売上が大幅増!
→ でも12月は季節的に売れやすい月
→ 季節要因を含んだ見かけの成長

季節調整後(正しい):
11月: 190万円
12月: 200万円
→ 真の成長は+5.3%のみ

実務での報告例:
「12月の売上は240万円でしたが、
季節要因を除くと実質200万円相当です。
11月比+5.3%の成長となります。」
問題 4 応用

あなたはアイスクリーム店の店長です。
以下の月別売上データがあります。

1月: 60万円、7月: 150万円、年間平均: 100万円

質問1: 7月の季節指数を計算してください。
質問2: 加法モデルと乗法モデル、どちらが適切ですか?
質問3: その理由を説明してください。

【解答】
質問1: 7月の季節指数

季節指数 = 月平均 / 年間平均 × 100
= 150 / 100 × 100
= 150

(1月の季節指数 = 60/100×100 = 60)
質問2: 乗法モデルが適切

質問3: 理由

アイスクリーム店の特徴:

1. 季節変動が非常に大きい
・1月: 60万円(最低)
・7月: 150万円(最高)
・変動幅: 90万円(150%の差)

2. 成長すると変動幅も拡大する
・現在: 1月60万、7月150万(差90万)
・売上2倍になると:
 1月120万、7月300万(差180万)
→ 変動幅も2倍に

3. 乗法モデルが適切な理由
・季節変動が「割合」で一定
・7月は常に「平均の1.5倍」
・1月は常に「平均の0.6倍」
・売上規模が変わっても比率は同じ

❓ よくある質問

Q1: 移動平均の期間はどう決めればいいですか?
目的とデータの特性で決定します。

基本的な考え方:
・短期トレンド把握 → 3ヶ月
・中期トレンド把握 → 6ヶ月
・季節性除去 → 12ヶ月

実務での使い分け:
・月次レポート: 3ヶ月MA
・四半期レポート: 6ヶ月MA
・年次計画: 12ヶ月MA

ベストプラクティス:
3ヶ月と12ヶ月の両方を併記して、
短期と長期の視点を提供する
Q2: 加法モデルと乗法モデル、どちらを使えばいいですか?
季節変動の大きさの変化で判断します。

加法モデルを使う場合:
・季節変動が金額ベースで一定
・例: 毎年冬に+20万円
・売上規模が安定している

乗法モデルを使う場合:
・季節変動が割合で一定
・例: 冬は売上の1.2倍
・売上が成長/縮小している

判断方法:
1. グラフで季節変動の幅を確認
2. 年々変動幅が拡大 → 乗法
3. 変動幅が一定 → 加法
4. 迷ったら両方試して比較
Q3: 季節調整は必ず必要ですか?
季節性が強い場合は必須です。

季節調整が必要な場合:
・季節指数の変動幅が15以上
・前年同月比を正確に計算したい
・月次の成長率を評価したい
・KPIモニタリング

季節調整が不要な場合:
・季節性がほとんどない
・年間を通じて安定
・B2Bビジネスの一部

実務のコツ:
元データと季節調整後の両方を見る。
経営層には両方を提示するのがベスト。
Q4: Excelでできる一番簡単な季節調整は?
AVERAGE関数で12ヶ月移動平均を計算するのが最も簡単です。

手順:
1. D7セルに =AVERAGE(C2:C13) と入力
2. 下にコピー
3. これで季節性を除いたトレンドが見える

より正確な季節調整:
1. 季節比率 = 実績 / 12ヶ月MA
2. 月別に平均を取る
3. 実績 / 季節比率 = 季節調整済み

ツールの活用:
・データ分析ツールパックの「移動平均」
・期間=12で年次季節性を除去
📝

学習メモ

ビジネスデータ分析・意思決定 - Step 35

📋 過去のメモ一覧
#artnasekai #学習メモ
LINE