フーリエ変換を用いた季節変動のモデリング

フーリエ変換を使った季節変動のモデリングによる美容とファッションの解析

シグナル処理からのテクニックを使用して時系列予測のパフォーマンスを向上させる

時系列データのモデリングとその予測は複雑なトピックです。予測タスクのモデルのパフォーマンスを向上させるために使用できる多くのテクニックがあります。ここでは、予測モデルが時間特性を吸収し利用する方法を改善することができるテクニックについて説明します。主な焦点は、トレーニング時系列予測モデルにフィードする季節特性の作成です。フーリエ変換を特徴作成プロセスに組み込むことで、ここで簡単にゲインを得ることができます。

この記事では、時系列予測の基本的な側面に精通していることを前提としています。一般的なトピックについては議論しませんが、その一部を洗練させることに焦点を当てます。時系列予測の導入については、このトピックのKaggleコースの紹介記事を参照してください。ここで議論するテクニックは、季節性のレッスンに基づいています。

データセット

Quarterly Australian Portland Cement production datasetを考慮してください。オーストラリアのポートランドセメントの四半期総生産量(百万トン)を1956:Q1から2014:Q1まで示しています。

df = pd.read_csv('Quarterly_Australian_Portland_Cement_production_776_10.csv', usecols=['time', 'value'])# convert time from year float to a proper datetime formatdf['time'] = df['time'].apply(lambda x: str(int(x)) + '-' + str(int(1 + 12 * (x % 1))).rjust(2, '0'))df['time'] = pd.to_datetime(df['time'])df = df.set_index('time').to_period()df.rename(columns={'value': 'production'}, inplace=True)

        productiontime              1956Q1       0.4651956Q2       0.5321956Q3       0.5611956Q4       0.5701957Q1       0.529...            ...2013Q1       2.0492013Q2       2.5282013Q3       2.6372013Q4       2.5652014Q1       2.229[233 rows x 1 columns]

これはトレンド、季節成分、その他の属性を持つ時系列データです。観測値は四半期ごとに行われ、数十年にわたります。最初に季節成分を確認するために、パリオドグラムプロットを使用してみましょう(すべてのコードは最後のリンク先のノートブックに含まれています)。

periodogram

パリオドグラムはスペクトル成分(季節成分)のパワー密度を示しています。最も強力な成分は4四半期または1年の周期を持つものです。これはグラフ上で最も強力な上下変動が10年ごとに約10回起こるという視覚的な印象を確認します。また、2四半期の周期性を持つ成分もあります。これは同じ季節現象であり、4四半期の周期性が単純な正弦波ではなくより複雑な形状を持つことを意味しています。単純化のために、4よりも長い期間の成分は無視します。

このデータにモデルを適合させようとする場合、おそらくデータセットの終了後の時間にセメントの生産を予測するために、年間の季節性を特徴または特徴のセットでキャプチャするのは良いアイデアです。例を見てみましょう。

季節特性

季節成分はさまざまな方法でモデル化することができます。しばしば、時間のダミーまたはサイン-コサインのペアとして表されます。これらは、モデル化しようとしている季節性と周期が等しいか、その倍数の合成特徴です。

年間のダミー変数:

seasonal_year = DeterministicProcess(index=df.index, constant=False, seasonal=True).in_sample()print(seasonal_year)

        s(1,4)  s(2,4)  s(3,4)  s(4,4)time                                  1956Q1     1.0     0.0     0.0     0.01956Q2     0.0     1.0     0.0     0.01956Q3     0.0     0.0     1.0     0.01956Q4     0.0     0.0     0.0     1.01957Q1     1.0     0.0     0.0     0.0...        ...     ...     ...     ...2013Q1     1.0     0.0     0.0     0.02013Q2     0.0     1.0     0.0     0.02013Q3     0.0     0.0     1.0     0.02013Q4     0.0     0.0     0.0     1.02014Q1     1.0     0.0     0.0     0.0[233 rows x 4 columns]

年間サイン・コサインのペア:

cfr = CalendarFourier(freq='Y', order=2)seasonal_year_trig = DeterministicProcess(index=df.index, seasonal=False, additional_terms=[cfr]).in_sample()with pd.option_context('display.max_columns', None, 'display.expand_frame_repr', False):    print(seasonal_year_trig)

        sin(1,freq=A-DEC)  cos(1,freq=A-DEC)  sin(2,freq=A-DEC)  cos(2,freq=A-DEC)time                                                                              1956Q1           0.000000           1.000000           0.000000           1.0000001956Q2           0.999963           0.008583           0.017166          -0.9998531956Q3           0.017166          -0.999853          -0.034328           0.9994111956Q4          -0.999963          -0.008583           0.017166          -0.9998531957Q1           0.000000           1.000000           0.000000           1.000000...                   ...                ...                ...                ...2013Q1           0.000000           1.000000           0.000000           1.0000002013Q2           0.999769           0.021516           0.043022          -0.9990742013Q3           0.025818          -0.999667          -0.051620           0.9986672013Q4          -0.999917          -0.012910           0.025818          -0.9996672014Q1           0.000000           1.000000           0.000000           1.000000[233 rows x 4 columns]

時間ダミーは季節現象の非常に複雑な波形を捉えることができます。一方で、周期がNである場合、N個の時間ダミーが必要になります。したがって、周期が非常に長い場合は、多くのダミー列が必要になるため、望ましくない場合があります。非罰則モデルでは、一般的にはN-1個のダミーが使用されますが、多重共線性の問題を回避するために1つは削除されます(ここでは無視します)。

サイン/コサインのペアは任意の長い時間周期をモデル化できます。各ペアは純粋な正弦波形をモデル化し、初期位相を持ちます。季節性特性の波形がより複雑になるほど、より多くのペアを追加する必要があります(CalendarFourierの出力のオーダーを増やします)。

(余談:モデルはモデルがデータに適合する間は初期位相phiを調整することができません。サインの値は事前に計算されているため、求めたいのは実際には単一の列のA*sin(2*pi*t/T + phi)です。ここでAはモデルが列に割り当てる重み、tは系列の時間インデックス、およびTは成分の周期です。ただし、モデルは初期位相phiを調整できず、求められた式はA1*sin(2*pi*t/T) + A2*cos(2*pi*t/T)と同等です。モデルは単に重みA1およびA2を見つける必要があります。)

TLDR:これらの2つのテクニックに共通するのは、季節性を繰り返しの特徴量セットで表現し、モデル化される時間周期(時間ダミーおよび基本的な正弦/余弦のペア)ごとに一度、または複数回循環する値(高次の正弦/余弦)を持つことです。また、これらの特徴量のそれぞれは、0から1の固定の範囲または-1から1の範囲で変動する値を持っています。これらは、ここで季節性をモデル化するために必要なすべての特徴です。

ここで季節性の特徴を使って線形モデルを適合させた場合に何が起こるかを見てみましょう。ただし、まず、モデルのトレーニングに使用される特徴のコレクションにトレンド特徴を追加する必要があります。

線形モデルの適合

トレンド特徴を作成し、それらを上記で生成された季節性の時間ダミーと結合しましょう:

trend_order = 2
trend_year = DeterministicProcess(index=df.index, constant=True, order=trend_order).in_sample()
X = trend_year.copy()
X = X.join(seasonal_year)

        const  trend  trend_squared  s(1,4)  s(2,4)  s(3,4)  s(4,4)time                                                               1956Q1    1.0    1.0            1.0     1.0     0.0     0.0     0.01956Q2    1.0    2.0            4.0     0.0     1.0     0.0     0.01956Q3    1.0    3.0            9.0     0.0     0.0     1.0     0.01956Q4    1.0    4.0           16.0     0.0     0.0     0.0     1.01957Q1    1.0    5.0           25.0     1.0     0.0     0.0     0.0...       ...    ...            ...     ...     ...     ...     ...2013Q1    1.0  229.0        52441.0     1.0     0.0     0.0     0.02013Q2    1.0  230.0        52900.0     0.0     1.0     0.0     0.02013Q3    1.0  231.0        53361.0     0.0     0.0     1.0     0.02013Q4    1.0  232.0        53824.0     0.0     0.0     0.0     1.02014Q1    1.0  233.0        54289.0     1.0     0.0     0.0     0.0[233 rows x 7 columns]

これはモデルのトレーニング/検証に使用されるXデータフレーム(特徴)です。私たちは、2次のトレンド特徴と、年次の季節性に必要な4つの時間ダミーを使ってデータをモデル化しています。yデータフレーム(ターゲット)はセメント生産数だけです。

データから2010年の観測値を含む検証セットを切り出しましょう。上記のX特徴とセメント生産(テスト部分)によって表されるyターゲットに対して線形モデルを適合させ、その後、検証でモデルのパフォーマンスを評価します。トレンドのみのモデルでも同様のことを行いますが、季節性は無視されます。

def do_forecast(X, index_train, index_test, trend_order):
    X_train = X.loc[index_train]
    X_test = X.loc[index_test]
    y_train = df['production'].loc[index_train]
    y_test = df['production'].loc[index_test]
    model = LinearRegression(fit_intercept=False)
    _ = model.fit(X_train, y_train)
    y_fore = pd.Series(model.predict(X_test), index=index_test)
    y_past = pd.Series(model.predict(X_train), index=index_train)
    trend_columns = X_train.columns.to_list()[0 : trend_order + 1]
    model_trend = LinearRegression(fit_intercept=False)
    _ = model_trend.fit(X_train[trend_columns], y_train)
    y_trend_fore = pd.Series(model_trend.predict(X_test[trend_columns]), index=index_test)
    y_trend_past = pd.Series(model_trend.predict(X_train[trend_columns]), index=index_train)
    RMSLE = mean_squared_log_error(y_test, y_fore, squared=False)
    print(f'RMSLE: {RMSLE}')
    ax = df.plot(**plot_params, title='AUS Cement Production - Forecast')
    ax = y_past.plot(color='C0', label='Backcast')
    ax = y_fore.plot(color='C3', label='Forecast')
    ax = y_trend_past.plot(ax=ax, color='C0', linewidth=3, alpha=0.333, label='Trend Past')
    ax = y_trend_fore.plot(ax=ax, color='C3', linewidth=3, alpha=0.333, label='Trend Future')
    _ = ax.legend()

do_forecast(X, index_train, index_test, trend_order)

RMSLE: 0.03846449744356434
モデルの検証

青は訓練、赤は検証です。フルモデルは鋭くて細い線です。傾向のみのモデルは幅広くぼやけた線です。

これは悪くはありませんが、顕著な問題が1つあります。モデルは、一定振幅の年次季節性を学習しています。実際には年次変動は時間とともに増加していますが、モデルは固定振幅の変動にしか固執できません。明らかに、私たちはモデルに固定振幅の季節的な特徴しか与えておらず、その他の特徴(ターゲットのラグなど)を使ってこの問題を克服するのに役立つものはありません。

固定振幅の問題を解決するために、シグナル(データ)をより詳しく調べてみましょう。

フーリエスペクトログラム

パワースペクトル密度は、信号中のすべてのスペクトル成分(データ中のすべての季節成分)を強調表示し、それらの全体的な強度の概要を提供しますが、これは全時間区間での総和です。各季節成分の振幅がデータセット全体で時間とともに変化する方法については何も示しません。

その変動を捉えるには、代わりにフーリエスペクトログラムを使用する必要があります。これはパワースペクトル密度と同様ですが、データセット全体にわたって多数の時間窓で繰り返し実行されます。パワースペクトル密度とスペクトログラムの両方は、scipyライブラリのメソッドとして利用できます。

上記で言及した2四半期と4四半期の季節成分のスペクトログラムをプロットしましょう。いつものように、コンパニオンノートブックのリンク先にフルコードがあります。

spectrum = compute_spectrum(df['production'], 4, 0.1)plot_spectrogram(spectrum, figsize_x=10)
スペクトログラム

この図は、2四半期と4四半期の成分の強度、つまり時間の経過における「強さ」を示しています。最初はかなり弱いですが、2010年頃に非常に強くなります。これは、この記事の冒頭でデータセットプロットで見ることができる変動と一致します。

もし、モデルに固定振幅の季節的特徴を与える代わりに、スペクトログラムが示すようにこれらの特徴の振幅を時間に応じて調整したらどうなるでしょうか?最終的なモデルは検証でより良いパフォーマンスを発揮するでしょうか?

季節成分の調整

4四半期の季節成分を選びましょう。この成分のトレンド(order=2)に対して線形モデル(エンベロープモデルと呼ばれます)を学習させ、トレーニングデータに対してそれを適用(model.fit())、そのトレンドを検証/テストデータまで延長します(model.predict()):

envelope_features = DeterministicProcess(index=X.index, constant=True, order=2).in_sample()spec4_train = compute_spectrum(df['production'].loc[index_train], max_period=4)spec4_model = LinearRegression()spec4_model.fit(envelope_features.loc[spec4_train.index], spec4_train['4.0'])spec4_regress = pd.Series(spec4_model.predict(envelope_features), index=X.index)ax = spec4_train['4.0'].plot(label='component envelope', color='gray')spec4_regress.loc[spec4_train.index].plot(ax=ax, color='C0', label='envelope regression: past')spec4_regress.loc[index_test].plot(ax=ax, color='C3', label='envelope regression: future')_ = ax.legend()
エンベロープの適合

青い線は、4四半期の成分の強度の変化をトレーニングデータで二次的なトレンド(order=2)として適合させたものです。赤い線は、同じものを検証データまで延長(予測)したものです。

時間による4四半期の季節成分の変動をモデル化しました。エンベロープモデルの出力を取り、4四半期の季節成分に対応する時系列に乗じることができます:

spec4_regress = spec4_regress / spec4_regress.mean()season_columns = ['s(1,4)', 's(2,4)', 's(3,4)', 's(4,4)']for c in season_columns:    X[c] = X[c] * spec4_regressprint(X)

        const  trend  trend_squared    s(1,4)    s(2,4)    s(3,4)    s(4,4)time                                                                       1956Q1    1.0    1.0            1.0  0.179989  0.000000  0.000000  0.0000001956Q2    1.0    2.0            4.0  0.000000  0.181109  0.000000  0.0000001956Q3    1.0    3.0            9.0  0.000000  0.000000  0.182306  0.0000001956Q4    1.0    4.0           16.0  0.000000  0.000000  0.000000  0.1835811957Q1    1.0    5.0           25.0  0.184932  0.000000  0.000000  0.000000...       ...    ...            ...       ...       ...       ...       ...2013Q1    1.0  229.0        52441.0  2.434701  0.000000  0.000000  0.0000002013Q2    1.0  230.0        52900.0  0.000000  2.453436  0.000000  0.0000002013Q3    1.0  231.0        53361.0  0.000000  0.000000  2.472249  0.0000002013Q4    1.0  232.0        53824.0  0.000000  0.000000  0.000000  2.4911392014Q1    1.0  233.0        54289.0  2.510106  0.000000  0.000000  0.000000[233 rows x 7 columns]

4つの時間ダミーはもはや0または1ではありません。それらは成分のエンベロープによって乗算され、今ではその振幅は時間とともに変動します。

モデルを再学習する

修正された時間ダミーを使用して、メインモデルを再び学習しましょう。

do_forecast(X, index_train, index_test, trend_order)

RMSLE: 0.02546321729737165
model validation, adjusted time dummies

完璧なフィットを目指しているわけではありませんが、このトイモデルは明らかにパフォーマンスが向上しており、ターゲットの4四半期の変動により密接に追従しています。パフォーマンスメトリックも51%改善されており、非常に良い結果です。

最終コメント

モデルのパフォーマンスを向上させるのは広範なトピックです。モデルの振る舞いは、単一の特徴や単一の技術に依存しません。ただし、与えられたモデルのすべてを抽出しようとする場合、意味のある特徴を与えることが重要です。時間ダミーやサイン/コサインフーリエ対は、それらがモデリングしている季節変動の時間的な変動を反映している場合、より意味があります。

フーリエ変換による季節性成分のエンベロープの調整は、特に線形モデルに対して効果的です。ブーステッドツリーにはあまり利益はないですが、ほとんどのモデルで改善が見られます。カレンダーなどの決定論的な特徴を線形モデルが処理し、より複雑な特徴をブーステッドツリーモデルが処理するハイブリッドモデルを使用している場合、線形モデルが「正しく判断する」ことが重要です。その結果、他のモデルに行うべき作業が少なくなります。

季節の特徴を調整するために使用するエンベロープモデルが、他のモデルと同様にトレーニングデータだけを使用して調整され、トレーニング中にテストデータを見ることはありません。エンベロープを調整した後、時間ダミーにはターゲットからの情報が含まれています。これらは純粋に決定論的な特徴ではなく、任意の予測期間にわたって事前に計算可能なものではありません。したがって、この時点で、注意を払わないと検証/テストデータからトレーニングデータに情報が漏れる可能性があります。

この記事で使用されているデータセットは、パブリックドメイン(CC0)ライセンスの下でここで入手できます:

KEY2STATS

© 2023 KEY2STATS – RStudio®はRStudio, Inc.の登録商標です。AP®はThe Collegeの登録商標です…

www.key2stats.com

この記事で使用されているコードはこちらで見つけることができます:

misc/seasonal_features_fourier_article/cement.ipynb at master · FlorinAndrei/misc

random stuff. GitHubでアカウントを作成してFlorinAndrei/miscの開発に貢献してください。

github.com

この記事で説明されているアイデアを使用して、KaggleのStore Sales — Time Series Forecastingコンペティションに提出されたノートブック:

フーリエスペクトログラム、アンサンブルモデル

Kaggleノートブックを使用して、ストアセールスの時系列予測のデータで機械学習コードを探索と実行

www.kaggle.com

Kaggleに提出されたノートブックの開発バージョンが含まれるGitHubリポジトリはこちら:

GitHub – FlorinAndrei/timeseries-forecasting-fourier-time-dummies: フーリエ調整された時間ダミーを使用した時系列予測…

フーリエ調整された時間ダミーを使用した時系列予測 – GitHub…

github.com

この記事で使用されているすべての画像とコードは著者によって作成されました。

We will continue to update VoAGI; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more