Décomposition : saisonnalité#

Objectif

  • modéliser la saisonnalité (pour repérer les mouvements saisonniers de notre série)

  • soustraire la saisonnalité

  • déterminer les ordres de différenciation, d’autorégressif, de moyenne mobile

Outils pour la saisonnalité

  • seasonal plot (sans et avec superposition de séries)

  • périodogramme

1import matplotlib.pyplot as plt
2import pandas as pd
3import statsmodels.api as sm
4from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
5
6from src.functions.seasonality import SeasonalPlotter, check_stationarity_and_difference
7from src.utils import init_notebook
1init_notebook()

Import des données traitées de la tendance#

1data_folder = "data/processed_data/detrend_data/LinearMADetrend/window-100"
2stock_name = "AAPL"
1df_detrend = pd.read_csv(
2    f"{data_folder}/{stock_name}.csv", parse_dates=["Date"], index_col="Date"
3)
4print(f"{df_detrend.shape = }")
df_detrend.shape = (756, 6)
1df_detrend.head()
Open High Low Close Adj Close Volume
Date
2019-01-02 -2.636750 -1.646748 -2.801751 -1.879250 -3.465916 148158800
2019-01-03 -5.465638 -5.030637 -5.960637 -5.913137 -7.341764 365248800
2019-01-04 -5.434760 -4.429759 -5.617259 -4.502261 -5.991877 234428400
2019-01-07 -4.487831 -4.455330 -5.187832 -4.680332 -6.166622 219111200
2019-01-08 -4.372408 -3.807405 -4.632406 -4.074907 -5.589533 164101200
1# Take close price only
2close_detrend = df_detrend["Close"]

Graphe de saisonnalité en utilisant l’API statsmodels#

Saisonnalité mensuelle#

Pour chaque mois, on fait la moyenne des prix de clôture.
Puis on affiche le graphe de saisonnalité.
La barre rouge représente, pour le mois de janvier par exemple, la moyenne sur tous les mois de janvier (01-19, 01-20, 01-21 etc.)

1close_monthly_resample = close_detrend.resample("M").mean()
2close_monthly_resample.head()
Date
2019-01-31   -3.792133
2019-02-28   -1.279019
2019-03-31    0.514090
2019-04-30    3.201218
2019-05-31   -0.825486
Freq: M, Name: Close, dtype: float64
1fig = sm.graphics.tsa.month_plot(close_monthly_resample, ylabel="Close price")
../_images/5d92707e347dd17e0f0c507e21cabfe8c0f52debecc500a7be740b41c3864982.png

Saisonnalité hebdomadaire#

1# Resample on week frequency
2close_weekly_resample = close_detrend.resample("W").mean()
3close_weekly_resample
Date
2019-01-06    -4.098216
2019-01-13    -3.975125
2019-01-20    -3.798467
2019-01-27    -4.138694
2019-02-03    -2.734122
                ...    
2021-12-05     6.961971
2021-12-12    16.062935
2021-12-19    16.276648
2021-12-26    14.106925
2022-01-02    18.462308
Freq: W-SUN, Name: Close, Length: 157, dtype: float64
1# Create groupby object needed by seasonal_plot function of statsmodels
2groupby_week_object = close_weekly_resample.groupby(lambda resample: resample.week)
1# Define xticklabels, index of week
2xticklabels = [f"{i}" for i in range(1, 54)]
 1# Seasonal plot
 2
 3_, ax = plt.subplots(figsize=(10, 6))
 4fig = sm.graphics.tsa.seasonal_plot(
 5    groupby_week_object, xticklabels=xticklabels, ylabel="Close price", ax=ax
 6)
 7
 8# Prevent some xticks from plotting
 9every_nth_tick = 2
10for n, label in enumerate(ax.xaxis.get_ticklabels()):
11    if n % every_nth_tick != 0:
12        label.set_visible(False)
../_images/ee7ea015eec763b976adcabaf1f096501b135c383f3a9ab49ae791a5b5b7d6a3.png

Graphe de saisonnalité par superposition de séries#

 1# Copy dataframe for seasonal plot
 2df_detrend_seasonality = df_detrend.copy()
 3
 4# Adapt index for function
 5df_detrend_seasonality.index = df_detrend_seasonality.index.to_period("D")
 6
 7# Define dummies
 8df_detrend_seasonality["weekofyear"] = df_detrend_seasonality.index.week
 9df_detrend_seasonality["dayofyear"] = df_detrend_seasonality.index.dayofyear
10df_detrend_seasonality["year"] = df_detrend_seasonality.index.year
1# Seasonal plot
2_, ax = plt.subplots(figsize=(12, 6))
3SeasonalPlotter.seasonal_plot(
4    X=df_detrend_seasonality, y="Close", period="year", freq="weekofyear", ax=ax
5)
/home/runner/work/stock-analysis/stock-analysis/src/functions/seasonality.py:61: FutureWarning: 

The `ci` parameter is deprecated. Use `errorbar=('ci', False)` for the same effect.

  ax = sns.lineplot(
<Axes: title={'center': 'Seasonal Plot (year/weekofyear)'}, xlabel='weekofyear', ylabel='Close'>
../_images/238038060d57fa35040035c48af37783e6300ea1273149d5f2b14dbf19ec9164.png
1# Seasonal plot
2_, ax = plt.subplots(figsize=(12, 6))
3SeasonalPlotter.seasonal_plot(
4    X=df_detrend_seasonality, y="Close", period="year", freq="dayofyear", ax=ax
5)
/home/runner/work/stock-analysis/stock-analysis/src/functions/seasonality.py:61: FutureWarning: 

The `ci` parameter is deprecated. Use `errorbar=('ci', False)` for the same effect.

  ax = sns.lineplot(
<Axes: title={'center': 'Seasonal Plot (year/dayofyear)'}, xlabel='dayofyear', ylabel='Close'>
../_images/623518a6693bf13c4dac8de99bb4b1d941dfc2454927a7cf187ec833ef618787.png

Périodogramme#

1SeasonalPlotter.plot_periodogram(time_series=df_detrend["Close"], detrend=False)
<Axes: title={'center': 'Periodogram'}, ylabel='Variance'>
../_images/1c195292569fd2a31a8094ba90ed26db6abf491e0ee07ef0028f387164ffe61c.png

Le périodogramme met en évidence des fréquences importantes semi-annuelle et trimestrielle.
Ces fréquences se constatent sur les graphes de saisonnalité, avec des cycles trimestrielles.

Différenciation saisonnière#

1# Define the seasonal frequencies
2quarterly_freq = 90
3semiannual_freq = 180
1# Differencing on quaterly frequency
2df_detrend["close_without_season"] = df_detrend["Close"] - df_detrend["Close"].shift(
3    quarterly_freq
4)
5
6# Differencing on semiannual frequency
7df_detrend["close_without_season"] = df_detrend["close_without_season"] - df_detrend[
8    "Close"
9].shift(semiannual_freq)
1df_detrend["close_without_season"].isna().sum()
180

Nous choisissons donc le paramètre \(D\) valant 90 pour la saisonnalité dans le modèle SARIMA.

Nous avons désormais un jeu de données sans saisonnalité avec 180 jours de données manquantes.
Cet effet est dû à la différenciation semi annuelle.

1df_detrend["Close"].plot(title="Close price detrended")
<Axes: title={'center': 'Close price detrended'}, xlabel='Date'>
../_images/e2fe18c7ec436a079b4c5292ab448f1522bb77e387c62b3c5b18109436cecac8.png
1df_detrend["close_without_season"].plot(
2    title="Close price detrended and differenced for seasonality"
3)
<Axes: title={'center': 'Close price detrended and differenced for seasonality'}, xlabel='Date'>
../_images/a15d4e91d47542661de4a7c7b1fed88722ea30dce5505730c6f0a965ab773021.png
1df_detrend["close_without_season"].to_csv(
2    "data/processed_data/season_treated_data/quaterly_semiannual_differencing/AAPL.csv"
3)

Détermination de l’ordre autorégressif#

1lag = (
2    len(df_detrend["Close"]) // 10
3)  # Display lags from 0 to 1/10 of time series length
4
5_ = plot_pacf(df_detrend["Close"], lags=lag, title="PACF plot - Close price detrended")
../_images/ed9195396513fd87b5203ca01662cf182645b59aa608588e693d5051b67cdc26.png

Nous choisissons \(P\) valant 2 pour la partie saisonnière du modèle SARIMA.

Détermination de l’ordre de moyenne mobile#

1lag = (
2    len(df_detrend["Close"]) // 10
3)  # Display lags from 0 to 1/10 of time series length
4
5_ = plot_acf(df_detrend["Close"], lags=lag, title="ACF plot - Close price detrended")
../_images/41da9301751150242956d0e84189d6234d2007276e7432ee1868592947079405.png

Le graphique des autocorrélations passe à 0, au seuil de confiance de 95%, au retard 14.
Nous choisissons donc le paramètre \(Q\) valant 14 pour la partie saisonnière du modèle SARIMA.

Modèle ARIMA#

Cette partie est consacrée à la détermination des paramètres du modèle prédictif ARIMA de paramètres \(p, d, q\) qui désignent respectivement :

  • L’ordre autorégressif

  • L’ordre de différenciation pour atteindre la stationnarité

  • L’ordre de moyenne mobile

1time_series = df_detrend["close_without_season"]
2time_series = time_series[~time_series.isna()]
3time_series
Date
2019-09-19     2.746602
2019-09-20     5.162185
2019-09-23     3.355712
2019-09-24     3.423930
2019-09-25     3.872582
                ...    
2021-12-23     4.962022
2021-12-27    14.379935
2021-12-28     9.723572
2021-12-29    10.662810
2021-12-30     5.430709
Name: close_without_season, Length: 576, dtype: float64

Détermination de l’ordre de différenciation#

1stationary_time_series, num_diffs, adfuller_pvalue = check_stationarity_and_difference(
2    time_series
3)
pvalue for adfuller test on differencing order 1 is 0.0

Une différenciation suffit à obtenir une série stationnaire.
Nous choisissons donc le paramètres \(d\) valant 1 pour le modèle ARIMA.

Détermination de l’ordre autorégressif#

1lag = (
2    len(stationary_time_series) // 10
3)  # Display lags from 0 to 1/10 of time series length
4
5_ = plot_pacf(
6    stationary_time_series, lags=lag, title="PACF plot - Close price detrended"
7)
../_images/ad538d24dc1fad24cdb9e315ec73d8995e0c8daf575c02b60db72d953ace0d83.png

Le graphique des autocorrélations partielles passe à 0, au seuil de confiance de 95%, au deuxième retard.
Nous choisissons donc le paramètre \(p\) valant 2 pour le modèle ARIMA.

Détermination de l’ordre de moyenne mobile#

1lag = (
2    len(stationary_time_series) // 10
3)  # Display lags from 0 to 1/10 of time series length
4
5_ = plot_acf(stationary_time_series, lags=lag, title="ACF plot - Close price detrended")
../_images/d04abc836dc5666bec93a69ed5f2a895e32fda934f9f8f2ef4aa6068d2bb0fff.png

Le graphique des autocorrélations passe à 0, au seuil de confiance de 95%, au deuxième retard.
Nous choisissons donc le paramètre \(q\) valant 2 pour le modèle ARIMA.