階層的時系列

上級

5.15.1

階層的時系列

最終更新 2020-01-29 読了時間 2 分
まとめ
  • 複数の時系列が階層構造(全社→事業部→製品など)を持つケースを理解する。
  • 階層間で整合性のある予測を行うためのボトムアップ・トップダウン・最適調整の3つのアプローチを学ぶ。
  • 商業動態統計の実データを例に、階層構造の具体的なイメージをつかむ。

階層的時系列とは #

時系列データが複数あるとき、そのデータが階層的な構造を持つことがあります。

例:地域・カテゴリごとの販売データ #

経済産業省のサイトでは『商業動態統計』が定期的に公開されています。 これは、日本の商業を営む企業の販売活動の動向をチェックすることを目的として集められているデータです。たとえば、2022年1月の付表3 百貨店・スーパー販売 都道府県別、商品別販売額等には以下のようなデータが含まれています。

百貨店・スーパー販売 都道府県別、商品別販売額等のデータ例

このデータには以下のような階層構造が含まれています。

階層構造の図


階層構造の具体例 #

企業の売上データを例にすると、次のような階層が考えられます。

全社売上
├── 事業部A
│   ├── 製品A-1
│   └── 製品A-2
└── 事業部B
    ├── 製品B-1
    └── 製品B-2

このとき、各レベルの時系列には次の整合性制約があります。

  • 全社売上 = 事業部Aの売上 + 事業部Bの売上
  • 事業部Aの売上 = 製品A-1の売上 + 製品A-2の売上

各レベルで個別に予測すると、この合計の整合性が崩れることがあります。


予測の調整手法 #

階層的時系列の予測には、整合性を保つために3つの代表的なアプローチがあります。

ボトムアップ(Bottom-Up) #

もっとも細かいレベル(製品単位など)で個別に予測し、上位レベルは合算して求めます。

  • 利点: 各製品の特性を反映しやすい
  • 欠点: 末端データにノイズが多いと、集約しても精度が出にくい

トップダウン(Top-Down) #

全社レベルで予測し、過去の構成比率で下位レベルに配分します。

  • 利点: 上位レベルの系列は安定していることが多く、予測精度が出やすい
  • 欠点: 構成比率が変動する場合(新製品の投入など)に対応しにくい

最適調整(Optimal Reconciliation) #

全レベルで独立に予測し、最小分散基準などで整合性を満たすように事後調整します。

  • 利点: 各レベルの情報を最大限に活用できる
  • 欠点: 共分散行列の推定が必要で、実装がやや複雑

Pythonでの実装例 #

scikit-htshierarchicalforecastなどのライブラリで階層的予測を実装できます。

1
2
3
4
5
6
# 階層構造の定義例
hierarchy = {
    "全社": ["事業部A", "事業部B"],
    "事業部A": ["製品A1", "製品A2"],
    "事業部B": ["製品B1", "製品B2"],
}
 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

# 人工データの生成
np.random.seed(42)
dates = pd.date_range("2020-01-01", periods=24, freq="MS")

# 末端レベルの系列を生成
a1 = 100 + np.cumsum(np.random.randn(24) * 5)
a2 = 80 + np.cumsum(np.random.randn(24) * 4)
b1 = 120 + np.cumsum(np.random.randn(24) * 6)
b2 = 60 + np.cumsum(np.random.randn(24) * 3)

df = pd.DataFrame({
    "製品A1": a1, "製品A2": a2,
    "製品B1": b1, "製品B2": b2,
}, index=dates)

# 上位レベルを集約で計算
df["事業部A"] = df["製品A1"] + df["製品A2"]
df["事業部B"] = df["製品B1"] + df["製品B2"]
df["全社"] = df["事業部A"] + df["事業部B"]

print(df[["全社", "事業部A", "事業部B"]].head())

ボトムアップ予測の簡易例 #

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from statsmodels.tsa.holtwinters import ExponentialSmoothing

# 末端レベルで個別に予測
forecasts = {}
for col in ["製品A1", "製品A2", "製品B1", "製品B2"]:
    model = ExponentialSmoothing(
        df[col], trend="add", seasonal=None,
    ).fit()
    forecasts[col] = model.forecast(6)

# 上位レベルは合算
forecasts["事業部A"] = forecasts["製品A1"] + forecasts["製品A2"]
forecasts["事業部B"] = forecasts["製品B1"] + forecasts["製品B2"]
forecasts["全社"] = forecasts["事業部A"] + forecasts["事業部B"]

# 整合性の確認
print("全社 == 事業部A + 事業部B:")
print(np.allclose(
    forecasts["全社"],
    forecasts["事業部A"] + forecasts["事業部B"],
))

ボトムアップでは定義上、整合性が常に保たれます。トップダウンや独立予測では事後調整が必要です。