Comment rechercher sur une grille les hyperparamètres SARIMA pour la prévision de séries chronologiques
Le modèle de moyenne mobile intégrée autorégressive saisonnière, ou SARIMA, est une approche permettant de modéliser des données de séries chronologiques univariées pouvant contenir des composantes de tendance et saisonnières.
Il s'agit d'une approche efficace pour la prévision de séries chronologiques, même si elle nécessite une analyse minutieuse et une expertise du domaine afin de configurer les sept hyperparamètres du modèle ou plus.
Une approche alternative pour configurer le modèle qui utilise du matériel moderne rapide et parallèle consiste à rechercher sur une grille une suite de configurations d'hyperparamètres afin de découvrir ce qui fonctionne le mieux. Souvent, ce processus peut révéler des configurations de modèle non intuitives qui entraînent des erreurs de prévision inférieures à celles spécifiées par une analyse minutieuse.
Dans ce didacticiel, vous découvrirez comment développer un cadre de recherche sur grille de tous les hyperparamètres du modèle SARIMA pour la prévision de séries chronologiques univariées.
Après avoir terminé ce tutoriel, vous saurez :
- Comment développer un cadre pour la recherche de grille de modèles SARIMA à partir de zéro en utilisant la validation progressive.
- Comment rechercher sur une grille les hyperparamètres du modèle SARIMA pour les données de séries chronologiques quotidiennes pour les naissances.
- Comment rechercher dans une grille les hyperparamètres du modèle SARIMA pour obtenir des données de séries chronologiques mensuelles sur les ventes de shampoings, les ventes de voitures et la température.
Démarrez votre projet avec mon nouveau livre Deep Learning for Time Series Forecasting, comprenant des tutoriels pas à pas et les fichiers code source Python pour tous les exemples.
Commençons.
- Mise à jour en avril 2019 : mise à jour du lien vers l'ensemble de données.
Présentation du didacticiel
Ce didacticiel est divisé en six parties ; ils sont:
- SARIMA pour la prévision de séries chronologiques
- Développer un cadre de recherche de grille
- Étude de cas 1 : Aucune tendance ni saisonnalité
- Étude de cas 2 : Tendance
- Étude de cas 3 : Saisonnalité
- Étude de cas 4 : Tendance et saisonnalité
SARIMA pour la prévision de séries chronologiques
La moyenne mobile intégrée autorégressive saisonnière, SARIMA ou Seasonal ARIMA, est une extension d'ARIMA qui prend explicitement en charge les données de séries chronologiques univariées avec une composante saisonnière.
Il ajoute trois nouveaux hyperparamètres pour spécifier l'autorégression (AR), la différenciation (I) et la moyenne mobile (MA) pour la composante saisonnière de la série, ainsi qu'un paramètre supplémentaire pour la période de saisonnalité.
Un modèle ARIMA saisonnier est formé en incluant des termes saisonniers supplémentaires dans l'ARIMA […] La partie saisonnière du modèle est constituée de termes très similaires aux composantes non saisonnières du modèle, mais ils impliquent des rétrogradations de la période saisonnière.
— Page 242, Prévisions : principes et pratiques, 2013.
La configuration d'un SARIMA nécessite la sélection d'hyperparamètres pour les éléments de tendance et saisonniers de la série.
Trois éléments de tendance nécessitent une configuration.
Ce sont les mêmes que le modèle ARIMA ; spécifiquement:
- p : ordre d'autorégression des tendances.
- d : ordre de différence de tendance.
- q : ordre de moyenne mobile de tendance.
Il y a quatre éléments saisonniers qui ne font pas partie d'ARIMA et qui doivent être configurés ; ils sont:
- P : Ordre autorégressif saisonnier.
- D : Ordre des différences saisonnières.
- Q : Ordre moyen mobile saisonnier.
- m : nombre de pas de temps pour une seule période saisonnière.
Ensemble, la notation d'un modèle SARIMA est spécifiée comme suit :
SARIMA(p,d,q)(P,D,Q)m
Le modèle SARIMA peut englober les modèles ARIMA, ARMA, AR et MA via les paramètres de configuration du modèle.
Les hyperparamètres de tendance et saisonniers du modèle peuvent être configurés en analysant les tracés d'autocorrélation et d'autocorrélation partielle, ce qui peut nécessiter une certaine expertise.
Une approche alternative consiste à rechercher sur une grille une suite de configurations de modèle et à découvrir quelles configurations fonctionnent le mieux pour une série temporelle univariée spécifique.
Les modèles ARIMA saisonniers peuvent potentiellement avoir un grand nombre de paramètres et de combinaisons de termes. Par conséquent, il convient d’essayer une large gamme de modèles lors de l’ajustement aux données et de choisir le modèle le mieux adapté en utilisant un critère approprié…
— Pages 143-144, Série chronologique d'introduction avec R, 2009.
Cette approche peut être plus rapide sur les ordinateurs modernes qu'un processus d'analyse et peut révéler des résultats surprenants qui pourraient ne pas être évidents et entraîner une erreur de prévision plus faible.
Développer un cadre de recherche de grille
Dans cette section, nous développerons un cadre pour la recherche sur grille des hyperparamètres du modèle SARIMA pour un problème de prévision de séries chronologiques univariées donné.
Nous utiliserons l'implémentation de SARIMA fournie par la bibliothèque statsmodels.
Ce modèle possède des hyperparamètres qui contrôlent la nature du modèle réalisé pour la série, la tendance et la saisonnalité, notamment :
- ordre : un tuple de paramètres p, d et q pour la modélisation de la tendance.
- sesonal_order : un tuple de paramètres P, D, Q et m pour la modélisation de la saisonnalité
- tendance : paramètre permettant de contrôler un modèle de tendance déterministe comme l'un des éléments suivants : "n", "c", "t", "ct" pour aucune tendance, constante, linéaire et constante avec tendance linéaire. , respectivement.
Si vous en savez suffisamment sur votre problème pour spécifier un ou plusieurs de ces paramètres, vous devez les spécifier. Sinon, vous pouvez essayer de rechercher ces paramètres dans une grille.
Nous pouvons commencer par définir une fonction qui adaptera un modèle avec une configuration donnée et effectuer une prévision en une étape.
Le sarima_forecast() ci-dessous implémente ce comportement.
La fonction prend un tableau ou une liste d'observations antérieures contiguës et une liste de paramètres de configuration utilisés pour configurer le modèle, en particulier deux tuples et une chaîne pour l'ordre des tendances, la tendance de l'ordre saisonnier et le paramètre.
Nous essayons également de rendre le modèle robuste en assouplissant les contraintes, telles que que les données doivent être stationnaires et que la transformée MA soit inversible.
# one-step sarima forecast
def sarima_forecast(history, config):
order, sorder, trend = config
# define model
model = SARIMAX(history, order=order, seasonal_order=sorder, trend=trend, enforce_stationarity=False, enforce_invertibility=False)
# fit model
model_fit = model.fit(disp=False)
# make one step forecast
yhat = model_fit.predict(len(history), len(history))
return yhat[0]
Ensuite, nous devons créer certaines fonctions pour ajuster et évaluer un modèle à plusieurs reprises via une validation progressive, notamment en divisant un ensemble de données en ensembles d'entraînement et de test et en évaluant les prévisions en une étape.
Nous pouvons diviser une liste ou un tableau NumPy de données en utilisant une tranche étant donné une taille spécifiée de division, par ex. le nombre de pas de temps à utiliser à partir des données de l'ensemble de test.
La fonction train_test_split() ci-dessous implémente cela pour un ensemble de données fourni et un nombre spécifié de pas de temps à utiliser dans l'ensemble de test.
# split a univariate dataset into train/test sets
def train_test_split(data, n_test):
return data[:-n_test], data[-n_test:]
Une fois les prévisions établies pour chaque étape de l'ensemble de données de test, elles doivent être comparées à l'ensemble de test afin de calculer un score d'erreur.
Il existe de nombreux scores d’erreur populaires pour la prévision de séries chronologiques. Dans ce cas, nous utiliserons l'erreur quadratique moyenne (RMSE), mais vous pouvez la modifier selon votre mesure préférée, par ex. MAPE, MAE, etc.
La fonction measure_rmse() ci-dessous calculera le RMSE à partir d'une liste de valeurs réelles (l'ensemble de test) et prédites.
# root mean squared error or rmse
def measure_rmse(actual, predicted):
return sqrt(mean_squared_error(actual, predicted))
Nous pouvons maintenant implémenter le schéma de validation walk-forward. Il s'agit d'une approche standard pour évaluer un modèle de prévision de séries chronologiques qui respecte l'ordre temporel des observations.
Tout d'abord, un ensemble de données de série chronologique univariée fourni est divisé en ensembles d'entraînement et de test à l'aide de la fonction train_test_split(). Ensuite, le nombre d'observations dans l'ensemble de test est énuméré. Pour chacun, nous adaptons un modèle à l’ensemble de l’historique et effectuons une prévision en une étape. La véritable observation pour le pas de temps est ensuite ajoutée à l'historique et le processus est répété. La fonction sarima_forecast() est appelée afin d'ajuster un modèle et de faire une prédiction. Enfin, un score d'erreur est calculé en comparant toutes les prévisions en une étape à l'ensemble de tests réel en appelant la fonction measure_rmse().
La fonction walk_forward_validation() ci-dessous implémente cela, en prenant une série temporelle univariée, un certain nombre de pas de temps à utiliser dans l'ensemble de test et un tableau de configuration de modèle.
# walk-forward validation for univariate data
def walk_forward_validation(data, n_test, cfg):
predictions = list()
# split dataset
train, test = train_test_split(data, n_test)
# seed history with training dataset
history = [x for x in train]
# step over each time-step in the test set
for i in range(len(test)):
# fit model and make forecast for history
yhat = sarima_forecast(history, cfg)
# store forecast in list of predictions
predictions.append(yhat)
# add actual observation to history for the next loop
history.append(test[i])
# estimate prediction error
error = measure_rmse(test, predictions)
return error
Si vous souhaitez faire des prédictions en plusieurs étapes, vous pouvez modifier l'appel à predict() dans la fonction sarima_forecast() et également modifier le calcul de l'erreur dans la predict(). Fonctionmeasure_rmse().
Nous pouvons appeler walk_forward_validation() à plusieurs reprises avec différentes listes de configurations de modèle.
Un problème possible est que certaines combinaisons de configurations de modèle peuvent ne pas être appelées pour le modèle et lèveront une exception, par ex. spécifiant certains aspects de la structure saisonnière dans les données, mais pas tous.
De plus, certains modèles peuvent également émettre des avertissements sur certaines données, par ex. à partir des bibliothèques d'algèbre linéaire appelées par la bibliothèque statsmodels.
Nous pouvons intercepter les exceptions et ignorer les avertissements pendant la recherche dans la grille en encapsulant tous les appels à walk_forward_validation() avec un try-sauf et un bloc pour ignorer les avertissements. Nous pouvons également ajouter un support de débogage pour désactiver ces protections dans le cas où nous voulons voir ce qui se passe réellement. Enfin, si une erreur se produit, nous pouvons renvoyer un résultat Aucun, sinon nous pouvons imprimer des informations sur les compétences de chaque modèle évalué. Ceci est utile lorsqu’un grand nombre de modèles sont évalués.
La fonction score_model() ci-dessous implémente cela et renvoie un tuple de (clé et résultat), où la clé est une version chaîne de la configuration du modèle testé.
# score a model, return None on failure
def score_model(data, n_test, cfg, debug=False):
result = None
# convert config to a key
key = str(cfg)
# show all warnings and fail on exception if debugging
if debug:
result = walk_forward_validation(data, n_test, cfg)
else:
# one failure during model validation suggests an unstable config
try:
# never show warnings when grid searching, too noisy
with catch_warnings():
filterwarnings("ignore")
result = walk_forward_validation(data, n_test, cfg)
except:
error = None
# check for an interesting result
if result is not None:
print(' > Model[%s] %.3f' % (key, result))
return (key, result)
Ensuite, nous avons besoin d'une boucle pour tester une liste de différentes configurations de modèles.
Il s'agit de la fonction principale qui pilote le processus de recherche de grille et appellera la fonction score_model() pour chaque configuration de modèle.
Nous pouvons considérablement accélérer le processus de recherche de grille en évaluant les configurations des modèles en parallèle. Une façon de procéder consiste à utiliser la bibliothèque Joblib.
Nous pouvons définir un objet Parallèle avec le nombre de cœurs à utiliser et le régler sur le nombre de scores détectés dans votre matériel.
executor = Parallel(n_jobs=cpu_count(), backend='multiprocessing')
Nous pouvons alors créer une liste de tâches à exécuter en parallèle, qui sera un appel à la fonction score_model() pour chaque configuration de modèle dont nous disposons.
tasks = (delayed(score_model)(data, n_test, cfg) for cfg in cfg_list)
Enfin, nous pouvons utiliser l'objet Parallel pour exécuter la liste des tâches en parallèle.
scores = executor(tasks)
C'est ça.
Nous pouvons également fournir une version non parallèle d'évaluation de toutes les configurations de modèle au cas où nous souhaiterions déboguer quelque chose.
scores = [score_model(data, n_test, cfg) for cfg in cfg_list]
Le résultat de l'évaluation d'une liste de configurations sera une liste de tuples, chacun avec un nom qui résume une configuration de modèle spécifique et l'erreur du modèle évalué avec cette configuration comme RMSE ou Aucun s'il y a eu une erreur.
Nous pouvons filtrer tous les scores avec Aucun.
scores = [r for r in scores if r[1] != None]
Nous pouvons ensuite trier tous les tuples de la liste par score dans l'ordre croissant (les meilleurs sont en premier), puis renvoyer cette liste de scores pour examen.
La fonction grid_search() ci-dessous implémente ce comportement étant donné un ensemble de données de séries chronologiques univariées, une liste de configurations de modèle (liste de listes) et le nombre de pas de temps à utiliser dans l'ensemble de test. Un argument parallèle facultatif permet d'activer ou de désactiver l'évaluation des modèles sur tous les cœurs, et est activé par défaut.
# grid search configs
def grid_search(data, cfg_list, n_test, parallel=True):
scores = None
if parallel:
# execute configs in parallel
executor = Parallel(n_jobs=cpu_count(), backend='multiprocessing')
tasks = (delayed(score_model)(data, n_test, cfg) for cfg in cfg_list)
scores = executor(tasks)
else:
scores = [score_model(data, n_test, cfg) for cfg in cfg_list]
# remove empty results
scores = [r for r in scores if r[1] != None]
# sort configs by error, asc
scores.sort(key=lambda tup: tup[1])
return scores
Nous avons presque terminé.
Il ne reste plus qu'à définir une liste de configurations de modèles à essayer pour un ensemble de données.
Nous pouvons définir cela de manière générique. Le seul paramètre que nous pouvons vouloir spécifier est la périodicité de la composante saisonnière de la série, si elle existe. Par défaut, nous ne supposerons aucune composante saisonnière.
La fonction sarima_configs() ci-dessous créera une liste de configurations de modèle à évaluer.
Les configurations supposent que chacune des composantes AR, MA et I pour la tendance et la saisonnalité sont d'ordre inférieur, par ex. désactivé (0) ou en [1,2]. Vous souhaiterez peut-être étendre ces fourchettes si vous pensez que l’ordre pourrait être plus élevé. Une liste facultative de périodes saisonnières peut être spécifiée, et vous pouvez même modifier la fonction pour spécifier d'autres éléments que vous connaissez peut-être sur votre série chronologique.
En théorie, il existe 1 296 configurations de modèle possibles à évaluer, mais en pratique, beaucoup ne seront pas valides et entraîneront une erreur que nous capterons et ignorerons.
# create a set of sarima configs to try
def sarima_configs(seasonal=[0]):
models = list()
# define config lists
p_params = [0, 1, 2]
d_params = [0, 1]
q_params = [0, 1, 2]
t_params = ['n','c','t','ct']
P_params = [0, 1, 2]
D_params = [0, 1]
Q_params = [0, 1, 2]
m_params = seasonal
# create config instances
for p in p_params:
for d in d_params:
for q in q_params:
for t in t_params:
for P in P_params:
for D in D_params:
for Q in Q_params:
for m in m_params:
cfg = [(p,d,q), (P,D,Q,m), t]
models.append(cfg)
return models
Nous disposons désormais d'un cadre pour la recherche sur grille des hyperparamètres du modèle SARIMA via une validation progressive en une étape.
Il est générique et fonctionnera pour toute série temporelle univariée en mémoire fournie sous forme de liste ou de tableau NumPy.
Nous pouvons nous assurer que tous les éléments fonctionnent ensemble en le testant sur un ensemble de données artificiel en 10 étapes.
L’exemple complet est répertorié ci-dessous.
# grid search sarima hyperparameters
from math import sqrt
from multiprocessing import cpu_count
from joblib import Parallel
from joblib import delayed
from warnings import catch_warnings
from warnings import filterwarnings
from statsmodels.tsa.statespace.sarimax import SARIMAX
from sklearn.metrics import mean_squared_error
# one-step sarima forecast
def sarima_forecast(history, config):
order, sorder, trend = config
# define model
model = SARIMAX(history, order=order, seasonal_order=sorder, trend=trend, enforce_stationarity=False, enforce_invertibility=False)
# fit model
model_fit = model.fit(disp=False)
# make one step forecast
yhat = model_fit.predict(len(history), len(history))
return yhat[0]
# root mean squared error or rmse
def measure_rmse(actual, predicted):
return sqrt(mean_squared_error(actual, predicted))
# split a univariate dataset into train/test sets
def train_test_split(data, n_test):
return data[:-n_test], data[-n_test:]
# walk-forward validation for univariate data
def walk_forward_validation(data, n_test, cfg):
predictions = list()
# split dataset
train, test = train_test_split(data, n_test)
# seed history with training dataset
history = [x for x in train]
# step over each time-step in the test set
for i in range(len(test)):
# fit model and make forecast for history
yhat = sarima_forecast(history, cfg)
# store forecast in list of predictions
predictions.append(yhat)
# add actual observation to history for the next loop
history.append(test[i])
# estimate prediction error
error = measure_rmse(test, predictions)
return error
# score a model, return None on failure
def score_model(data, n_test, cfg, debug=False):
result = None
# convert config to a key
key = str(cfg)
# show all warnings and fail on exception if debugging
if debug:
result = walk_forward_validation(data, n_test, cfg)
else:
# one failure during model validation suggests an unstable config
try:
# never show warnings when grid searching, too noisy
with catch_warnings():
filterwarnings("ignore")
result = walk_forward_validation(data, n_test, cfg)
except:
error = None
# check for an interesting result
if result is not None:
print(' > Model[%s] %.3f' % (key, result))
return (key, result)
# grid search configs
def grid_search(data, cfg_list, n_test, parallel=True):
scores = None
if parallel:
# execute configs in parallel
executor = Parallel(n_jobs=cpu_count(), backend='multiprocessing')
tasks = (delayed(score_model)(data, n_test, cfg) for cfg in cfg_list)
scores = executor(tasks)
else:
scores = [score_model(data, n_test, cfg) for cfg in cfg_list]
# remove empty results
scores = [r for r in scores if r[1] != None]
# sort configs by error, asc
scores.sort(key=lambda tup: tup[1])
return scores
# create a set of sarima configs to try
def sarima_configs(seasonal=[0]):
models = list()
# define config lists
p_params = [0, 1, 2]
d_params = [0, 1]
q_params = [0, 1, 2]
t_params = ['n','c','t','ct']
P_params = [0, 1, 2]
D_params = [0, 1]
Q_params = [0, 1, 2]
m_params = seasonal
# create config instances
for p in p_params:
for d in d_params:
for q in q_params:
for t in t_params:
for P in P_params:
for D in D_params:
for Q in Q_params:
for m in m_params:
cfg = [(p,d,q), (P,D,Q,m), t]
models.append(cfg)
return models
if __name__ == '__main__':
# define dataset
data = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0]
print(data)
# data split
n_test = 4
# model configs
cfg_list = sarima_configs()
# grid search
scores = grid_search(data, cfg_list, n_test)
print('done')
# list top 3 configs
for cfg, error in scores[:3]:
print(cfg, error)
L’exécution de l’exemple imprime d’abord l’ensemble de données de série chronologique artificiel.
Ensuite, les configurations du modèle et leurs erreurs sont signalées au fur et à mesure de leur évaluation, tronquées ci-dessous par souci de concision.
Enfin, les configurations et les erreurs des trois principales configurations sont signalées. Nous pouvons voir que de nombreux modèles atteignent des performances parfaites sur ce problème simple de séries chronologiques artificielles à croissance linéaire.
[10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0]
...
> Model[[(2, 0, 0), (2, 0, 0, 0), 'ct']] 0.001
> Model[[(2, 0, 0), (2, 0, 1, 0), 'ct']] 0.000
> Model[[(2, 0, 1), (0, 0, 0, 0), 'n']] 0.000
> Model[[(2, 0, 1), (0, 0, 1, 0), 'n']] 0.000
done
[(2, 1, 0), (1, 0, 0, 0), 'n'] 0.0
[(2, 1, 0), (2, 0, 0, 0), 'n'] 0.0
[(2, 1, 1), (1, 0, 1, 0), 'n'] 0.0
Maintenant que nous disposons d'un cadre robuste pour la recherche de grilles d'hyperparamètres du modèle SARIMA, testons-le sur une suite d'ensembles de données de séries chronologiques univariées standard.
Les ensembles de données ont été choisis à des fins de démonstration ; Je ne suggère pas qu'un modèle SARIMA soit la meilleure approche pour chaque ensemble de données ; peut-être qu'un ETS ou autre chose serait plus approprié dans certains cas.
Étude de cas 1 : Aucune tendance ni saisonnalité
L’ensemble de données « naissances quotidiennes des femmes » résume le total quotidien des naissances féminines en Californie, aux États-Unis, en 1959.
L'ensemble de données n'a pas de tendance évidente ni de composante saisonnière.
Téléchargez l'ensemble de données directement à partir d'ici :
- naissances-totales-quotidiennes-de-femmes.csv
Enregistrez le fichier sous le nom de fichier « daily-total-female-births.csv » dans votre répertoire de travail actuel.
Nous pouvons charger cet ensemble de données sous forme de série Pandas en utilisant la fonction read_csv().
series = read_csv('daily-total-female-births.csv', header=0, index_col=0)
L'ensemble de données comporte un an, soit 365 observations. Nous utiliserons les 200 premiers pour la formation et les 165 restants comme ensemble de test.
L’exemple complet de grille recherchant le problème de prévision de séries chronologiques univariées féminines quotidiennes est répertorié ci-dessous.
# grid search sarima hyperparameters for daily female dataset
from math import sqrt
from multiprocessing import cpu_count
from joblib import Parallel
from joblib import delayed
from warnings import catch_warnings
from warnings import filterwarnings
from statsmodels.tsa.statespace.sarimax import SARIMAX
from sklearn.metrics import mean_squared_error
from pandas import read_csv
# one-step sarima forecast
def sarima_forecast(history, config):
order, sorder, trend = config
# define model
model = SARIMAX(history, order=order, seasonal_order=sorder, trend=trend, enforce_stationarity=False, enforce_invertibility=False)
# fit model
model_fit = model.fit(disp=False)
# make one step forecast
yhat = model_fit.predict(len(history), len(history))
return yhat[0]
# root mean squared error or rmse
def measure_rmse(actual, predicted):
return sqrt(mean_squared_error(actual, predicted))
# split a univariate dataset into train/test sets
def train_test_split(data, n_test):
return data[:-n_test], data[-n_test:]
# walk-forward validation for univariate data
def walk_forward_validation(data, n_test, cfg):
predictions = list()
# split dataset
train, test = train_test_split(data, n_test)
# seed history with training dataset
history = [x for x in train]
# step over each time-step in the test set
for i in range(len(test)):
# fit model and make forecast for history
yhat = sarima_forecast(history, cfg)
# store forecast in list of predictions
predictions.append(yhat)
# add actual observation to history for the next loop
history.append(test[i])
# estimate prediction error
error = measure_rmse(test, predictions)
return error
# score a model, return None on failure
def score_model(data, n_test, cfg, debug=False):
result = None
# convert config to a key
key = str(cfg)
# show all warnings and fail on exception if debugging
if debug:
result = walk_forward_validation(data, n_test, cfg)
else:
# one failure during model validation suggests an unstable config
try:
# never show warnings when grid searching, too noisy
with catch_warnings():
filterwarnings("ignore")
result = walk_forward_validation(data, n_test, cfg)
except:
error = None
# check for an interesting result
if result is not None:
print(' > Model[%s] %.3f' % (key, result))
return (key, result)
# grid search configs
def grid_search(data, cfg_list, n_test, parallel=True):
scores = None
if parallel:
# execute configs in parallel
executor = Parallel(n_jobs=cpu_count(), backend='multiprocessing')
tasks = (delayed(score_model)(data, n_test, cfg) for cfg in cfg_list)
scores = executor(tasks)
else:
scores = [score_model(data, n_test, cfg) for cfg in cfg_list]
# remove empty results
scores = [r for r in scores if r[1] != None]
# sort configs by error, asc
scores.sort(key=lambda tup: tup[1])
return scores
# create a set of sarima configs to try
def sarima_configs(seasonal=[0]):
models = list()
# define config lists
p_params = [0, 1, 2]
d_params = [0, 1]
q_params = [0, 1, 2]
t_params = ['n','c','t','ct']
P_params = [0, 1, 2]
D_params = [0, 1]
Q_params = [0, 1, 2]
m_params = seasonal
# create config instances
for p in p_params:
for d in d_params:
for q in q_params:
for t in t_params:
for P in P_params:
for D in D_params:
for Q in Q_params:
for m in m_params:
cfg = [(p,d,q), (P,D,Q,m), t]
models.append(cfg)
return models
if __name__ == '__main__':
# load dataset
series = read_csv('daily-total-female-births.csv', header=0, index_col=0)
data = series.values
print(data.shape)
# data split
n_test = 165
# model configs
cfg_list = sarima_configs()
# grid search
scores = grid_search(data, cfg_list, n_test)
print('done')
# list top 3 configs
for cfg, error in scores[:3]:
print(cfg, error)
L'exécution de l'exemple peut prendre quelques minutes sur du matériel moderne.
Les configurations de modèle et le RMSE sont imprimés au fur et à mesure que les modèles sont évalués. Les trois principales configurations de modèle et leurs erreurs sont signalées à la fin de l'analyse.
Remarque : Vos résultats peuvent varier en raison de la nature stochastique de l'algorithme ou de la procédure d'évaluation, ou des différences de précision numérique. Pensez à exécuter l’exemple plusieurs fois et comparez le résultat moyen.
On voit que le meilleur résultat était un RMSE d’environ 6,77 naissances avec la configuration suivante :
- Ordre : (1, 0, 2)
- Ordre saisonnier : (1, 0, 1, 0)
- Paramètre de tendance : « t » pour une tendance linéaire
Il est surprenant qu’une configuration comportant certains éléments saisonniers ait donné lieu à l’erreur la plus faible. Je n'aurais pas deviné cette configuration et je serais probablement resté fidèle à un modèle ARIMA.
...
> Model[[(2, 1, 2), (1, 0, 1, 0), 'ct']] 6.905
> Model[[(2, 1, 2), (2, 0, 0, 0), 'ct']] 7.031
> Model[[(2, 1, 2), (2, 0, 1, 0), 'ct']] 6.985
> Model[[(2, 1, 2), (1, 0, 2, 0), 'ct']] 6.941
> Model[[(2, 1, 2), (2, 0, 2, 0), 'ct']] 7.056
done
[(1, 0, 2), (1, 0, 1, 0), 't'] 6.770349800255089
[(0, 1, 2), (1, 0, 2, 0), 'ct'] 6.773217122759515
[(2, 1, 1), (2, 0, 2, 0), 'ct'] 6.886633191752254
Étude de cas 2 : Tendance
L’ensemble de données « shampoing » résume les ventes mensuelles de shampoing sur une période de trois ans.
L'ensemble de données contient une tendance évidente mais aucune composante saisonnière évidente.
Téléchargez l'ensemble de données directement à partir d'ici :
- shampooing.csv
Enregistrez le fichier sous le nom de fichier « shampooing.csv » dans votre répertoire de travail actuel.
Nous pouvons charger cet ensemble de données sous forme de série Pandas en utilisant la fonction read_csv().
# parse dates
def custom_parser(x):
return datetime.strptime('195'+x, '%Y-%m')
# load dataset
series = read_csv('shampoo.csv', header=0, index_col=0, date_parser=custom_parser)
L'ensemble de données compte trois années, soit 36 observations. Nous utiliserons les 24 premiers pour la formation et les 12 restants comme ensemble de test.
L’exemple complet de grille recherchant le problème de prévision de séries chronologiques univariées des ventes de shampooing est répertorié ci-dessous.
# grid search sarima hyperparameters for monthly shampoo sales dataset
from math import sqrt
from multiprocessing import cpu_count
from joblib import Parallel
from joblib import delayed
from warnings import catch_warnings
from warnings import filterwarnings
from statsmodels.tsa.statespace.sarimax import SARIMAX
from sklearn.metrics import mean_squared_error
from pandas import read_csv
from pandas import datetime
# one-step sarima forecast
def sarima_forecast(history, config):
order, sorder, trend = config
# define model
model = SARIMAX(history, order=order, seasonal_order=sorder, trend=trend, enforce_stationarity=False, enforce_invertibility=False)
# fit model
model_fit = model.fit(disp=False)
# make one step forecast
yhat = model_fit.predict(len(history), len(history))
return yhat[0]
# root mean squared error or rmse
def measure_rmse(actual, predicted):
return sqrt(mean_squared_error(actual, predicted))
# split a univariate dataset into train/test sets
def train_test_split(data, n_test):
return data[:-n_test], data[-n_test:]
# walk-forward validation for univariate data
def walk_forward_validation(data, n_test, cfg):
predictions = list()
# split dataset
train, test = train_test_split(data, n_test)
# seed history with training dataset
history = [x for x in train]
# step over each time-step in the test set
for i in range(len(test)):
# fit model and make forecast for history
yhat = sarima_forecast(history, cfg)
# store forecast in list of predictions
predictions.append(yhat)
# add actual observation to history for the next loop
history.append(test[i])
# estimate prediction error
error = measure_rmse(test, predictions)
return error
# score a model, return None on failure
def score_model(data, n_test, cfg, debug=False):
result = None
# convert config to a key
key = str(cfg)
# show all warnings and fail on exception if debugging
if debug:
result = walk_forward_validation(data, n_test, cfg)
else:
# one failure during model validation suggests an unstable config
try:
# never show warnings when grid searching, too noisy
with catch_warnings():
filterwarnings("ignore")
result = walk_forward_validation(data, n_test, cfg)
except:
error = None
# check for an interesting result
if result is not None:
print(' > Model[%s] %.3f' % (key, result))
return (key, result)
# grid search configs
def grid_search(data, cfg_list, n_test, parallel=True):
scores = None
if parallel:
# execute configs in parallel
executor = Parallel(n_jobs=cpu_count(), backend='multiprocessing')
tasks = (delayed(score_model)(data, n_test, cfg) for cfg in cfg_list)
scores = executor(tasks)
else:
scores = [score_model(data, n_test, cfg) for cfg in cfg_list]
# remove empty results
scores = [r for r in scores if r[1] != None]
# sort configs by error, asc
scores.sort(key=lambda tup: tup[1])
return scores
# create a set of sarima configs to try
def sarima_configs(seasonal=[0]):
models = list()
# define config lists
p_params = [0, 1, 2]
d_params = [0, 1]
q_params = [0, 1, 2]
t_params = ['n','c','t','ct']
P_params = [0, 1, 2]
D_params = [0, 1]
Q_params = [0, 1, 2]
m_params = seasonal
# create config instances
for p in p_params:
for d in d_params:
for q in q_params:
for t in t_params:
for P in P_params:
for D in D_params:
for Q in Q_params:
for m in m_params:
cfg = [(p,d,q), (P,D,Q,m), t]
models.append(cfg)
return models
# parse dates
def custom_parser(x):
return datetime.strptime('195'+x, '%Y-%m')
if __name__ == '__main__':
# load dataset
series = read_csv('shampoo.csv', header=0, index_col=0, date_parser=custom_parser)
data = series.values
print(data.shape)
# data split
n_test = 12
# model configs
cfg_list = sarima_configs()
# grid search
scores = grid_search(data, cfg_list, n_test)
print('done')
# list top 3 configs
for cfg, error in scores[:3]:
print(cfg, error)
L'exécution de l'exemple peut prendre quelques minutes sur du matériel moderne.
Les configurations de modèle et le RMSE sont imprimés au fur et à mesure que les modèles sont évalués. Les trois principales configurations de modèle et leurs erreurs sont signalées à la fin de l'analyse.
Remarque : Vos résultats peuvent varier en raison de la nature stochastique de l'algorithme ou de la procédure d'évaluation, ou des différences de précision numérique. Pensez à exécuter l’exemple plusieurs fois et comparez le résultat moyen.
On constate que le meilleur résultat était un RMSE d'environ 54,76 ventes avec la configuration suivante :
- Ordre de tendance : (0, 1, 2)
- Ordre saisonnier : (2, 0, 2, 0)
- Paramètre de tendance : « t » (tendance linéaire)
...
> Model[[(2, 1, 2), (1, 0, 1, 0), 'ct']] 68.891
> Model[[(2, 1, 2), (2, 0, 0, 0), 'ct']] 75.406
> Model[[(2, 1, 2), (1, 0, 2, 0), 'ct']] 80.908
> Model[[(2, 1, 2), (2, 0, 1, 0), 'ct']] 78.734
> Model[[(2, 1, 2), (2, 0, 2, 0), 'ct']] 82.958
done
[(0, 1, 2), (2, 0, 2, 0), 't'] 54.767582003072874
[(0, 1, 1), (2, 0, 2, 0), 'ct'] 58.69987083057107
[(1, 1, 2), (0, 0, 1, 0), 't'] 58.709089340600094
Étude de cas 3 : Saisonnalité
L'ensemble de données « températures moyennes mensuelles » résume les températures mensuelles moyennes de l'air au château de Nottingham, en Angleterre, de 1920 à 1939, en degrés Fahrenheit.
L'ensemble de données comporte une composante saisonnière évidente et aucune tendance évidente.
Téléchargez l'ensemble de données directement à partir d'ici :
- température-moyenne-mensuelle.csv
Enregistrez le fichier sous le nom de fichier « monthly-mean-temp.csv » dans votre répertoire de travail actuel.
Nous pouvons charger cet ensemble de données sous forme de série Pandas en utilisant la fonction read_csv().
series = read_csv('monthly-mean-temp.csv', header=0, index_col=0)
L'ensemble de données compte 20 ans, soit 240 observations. Nous réduirons l'ensemble de données aux cinq dernières années de données (60 observations) afin d'accélérer le processus d'évaluation du modèle et utiliserons la dernière année, soit 12 observations, pour l'ensemble de test.
# trim dataset to 5 years
data = data[-(5*12):]
La période de la composante saisonnière est d'environ un an, soit 12 observations. Nous l'utiliserons comme période saisonnière dans l'appel à la fonction sarima_configs() lors de la préparation des configurations du modèle.
# model configs
cfg_list = sarima_configs(seasonal=[0, 12])
L’exemple complet de grille recherchant le problème de prévision des séries chronologiques de températures moyennes mensuelles est répertorié ci-dessous.
# grid search sarima hyperparameters for monthly mean temp dataset
from math import sqrt
from multiprocessing import cpu_count
from joblib import Parallel
from joblib import delayed
from warnings import catch_warnings
from warnings import filterwarnings
from statsmodels.tsa.statespace.sarimax import SARIMAX
from sklearn.metrics import mean_squared_error
from pandas import read_csv
# one-step sarima forecast
def sarima_forecast(history, config):
order, sorder, trend = config
# define model
model = SARIMAX(history, order=order, seasonal_order=sorder, trend=trend, enforce_stationarity=False, enforce_invertibility=False)
# fit model
model_fit = model.fit(disp=False)
# make one step forecast
yhat = model_fit.predict(len(history), len(history))
return yhat[0]
# root mean squared error or rmse
def measure_rmse(actual, predicted):
return sqrt(mean_squared_error(actual, predicted))
# split a univariate dataset into train/test sets
def train_test_split(data, n_test):
return data[:-n_test], data[-n_test:]
# walk-forward validation for univariate data
def walk_forward_validation(data, n_test, cfg):
predictions = list()
# split dataset
train, test = train_test_split(data, n_test)
# seed history with training dataset
history = [x for x in train]
# step over each time-step in the test set
for i in range(len(test)):
# fit model and make forecast for history
yhat = sarima_forecast(history, cfg)
# store forecast in list of predictions
predictions.append(yhat)
# add actual observation to history for the next loop
history.append(test[i])
# estimate prediction error
error = measure_rmse(test, predictions)
return error
# score a model, return None on failure
def score_model(data, n_test, cfg, debug=False):
result = None
# convert config to a key
key = str(cfg)
# show all warnings and fail on exception if debugging
if debug:
result = walk_forward_validation(data, n_test, cfg)
else:
# one failure during model validation suggests an unstable config
try:
# never show warnings when grid searching, too noisy
with catch_warnings():
filterwarnings("ignore")
result = walk_forward_validation(data, n_test, cfg)
except:
error = None
# check for an interesting result
if result is not None:
print(' > Model[%s] %.3f' % (key, result))
return (key, result)
# grid search configs
def grid_search(data, cfg_list, n_test, parallel=True):
scores = None
if parallel:
# execute configs in parallel
executor = Parallel(n_jobs=cpu_count(), backend='multiprocessing')
tasks = (delayed(score_model)(data, n_test, cfg) for cfg in cfg_list)
scores = executor(tasks)
else:
scores = [score_model(data, n_test, cfg) for cfg in cfg_list]
# remove empty results
scores = [r for r in scores if r[1] != None]
# sort configs by error, asc
scores.sort(key=lambda tup: tup[1])
return scores
# create a set of sarima configs to try
def sarima_configs(seasonal=[0]):
models = list()
# define config lists
p_params = [0, 1, 2]
d_params = [0, 1]
q_params = [0, 1, 2]
t_params = ['n','c','t','ct']
P_params = [0, 1, 2]
D_params = [0, 1]
Q_params = [0, 1, 2]
m_params = seasonal
# create config instances
for p in p_params:
for d in d_params:
for q in q_params:
for t in t_params:
for P in P_params:
for D in D_params:
for Q in Q_params:
for m in m_params:
cfg = [(p,d,q), (P,D,Q,m), t]
models.append(cfg)
return models
if __name__ == '__main__':
# load dataset
series = read_csv('monthly-mean-temp.csv', header=0, index_col=0)
data = series.values
# trim dataset to 5 years
data = data[-(5*12):]
# data split
n_test = 12
# model configs
cfg_list = sarima_configs(seasonal=[0, 12])
# grid search
scores = grid_search(data, cfg_list, n_test)
print('done')
# list top 3 configs
for cfg, error in scores[:3]:
print(cfg, error)
L'exécution de l'exemple peut prendre quelques minutes sur du matériel moderne.
Les configurations de modèle et le RMSE sont imprimés au fur et à mesure que les modèles sont évalués. Les trois principales configurations de modèle et leurs erreurs sont signalées à la fin de l'analyse.
Remarque : Vos résultats peuvent varier en raison de la nature stochastique de l'algorithme ou de la procédure d'évaluation, ou des différences de précision numérique. Pensez à exécuter l’exemple plusieurs fois et comparez le résultat moyen.
On voit que le meilleur résultat était un RMSE d’environ 1,5 degrés avec la configuration suivante :
- Ordre de tendance : (0, 0, 0)
- Ordre saisonnier : (1, 0, 1, 12)
- Paramètre de tendance : 'n' (pas de tendance)
Comme on pouvait s'y attendre, le modèle n'a pas de composante de tendance et une composante ARMA saisonnière sur 12 mois.
...
> Model[[(2, 1, 2), (2, 1, 0, 12), 't']] 4.599
> Model[[(2, 1, 2), (1, 1, 0, 12), 'ct']] 2.477
> Model[[(2, 1, 2), (2, 0, 0, 12), 'ct']] 2.548
> Model[[(2, 1, 2), (2, 0, 1, 12), 'ct']] 2.893
> Model[[(2, 1, 2), (2, 1, 0, 12), 'ct']] 5.404
done
[(0, 0, 0), (1, 0, 1, 12), 'n'] 1.5577613610905712
[(0, 0, 0), (1, 1, 0, 12), 'n'] 1.6469530713847962
[(0, 0, 0), (2, 0, 0, 12), 'n'] 1.7314448163607488
Étude de cas 4 : Tendance et saisonnalité
L'ensemble de données « Ventes mensuelles de voitures » résume les ventes mensuelles de voitures au Québec, Canada entre 1960 et 1968.
L'ensemble de données a une tendance évidente et une composante saisonnière.
Téléchargez l'ensemble de données directement à partir d'ici :
- ventes-de-voitures-mensuelles.csv
Enregistrez le fichier sous le nom de fichier « monthly-car-sales.csv » dans votre répertoire de travail actuel.
Nous pouvons charger cet ensemble de données sous forme de série Pandas en utilisant la fonction read_csv().
series = read_csv('monthly-car-sales.csv', header=0, index_col=0)
L'ensemble de données compte 9 années, soit 108 observations. Nous utiliserons la dernière année, soit 12 observations, comme ensemble de tests.
La durée de la composante saisonnière pourrait être de six mois ou de 12 mois. Nous essaierons les deux comme période saisonnière dans l'appel à la fonction sarima_configs() lors de la préparation des configurations du modèle.
# model configs
cfg_list = sarima_configs(seasonal=[0,6,12])
L'exemple de grille complet recherchant le problème de prévision des séries chronologiques de ventes mensuelles de voitures est répertorié ci-dessous.
# grid search sarima hyperparameters for monthly car sales dataset
from math import sqrt
from multiprocessing import cpu_count
from joblib import Parallel
from joblib import delayed
from warnings import catch_warnings
from warnings import filterwarnings
from statsmodels.tsa.statespace.sarimax import SARIMAX
from sklearn.metrics import mean_squared_error
from pandas import read_csv
# one-step sarima forecast
def sarima_forecast(history, config):
order, sorder, trend = config
# define model
model = SARIMAX(history, order=order, seasonal_order=sorder, trend=trend, enforce_stationarity=False, enforce_invertibility=False)
# fit model
model_fit = model.fit(disp=False)
# make one step forecast
yhat = model_fit.predict(len(history), len(history))
return yhat[0]
# root mean squared error or rmse
def measure_rmse(actual, predicted):
return sqrt(mean_squared_error(actual, predicted))
# split a univariate dataset into train/test sets
def train_test_split(data, n_test):
return data[:-n_test], data[-n_test:]
# walk-forward validation for univariate data
def walk_forward_validation(data, n_test, cfg):
predictions = list()
# split dataset
train, test = train_test_split(data, n_test)
# seed history with training dataset
history = [x for x in train]
# step over each time-step in the test set
for i in range(len(test)):
# fit model and make forecast for history
yhat = sarima_forecast(history, cfg)
# store forecast in list of predictions
predictions.append(yhat)
# add actual observation to history for the next loop
history.append(test[i])
# estimate prediction error
error = measure_rmse(test, predictions)
return error
# score a model, return None on failure
def score_model(data, n_test, cfg, debug=False):
result = None
# convert config to a key
key = str(cfg)
# show all warnings and fail on exception if debugging
if debug:
result = walk_forward_validation(data, n_test, cfg)
else:
# one failure during model validation suggests an unstable config
try:
# never show warnings when grid searching, too noisy
with catch_warnings():
filterwarnings("ignore")
result = walk_forward_validation(data, n_test, cfg)
except:
error = None
# check for an interesting result
if result is not None:
print(' > Model[%s] %.3f' % (key, result))
return (key, result)
# grid search configs
def grid_search(data, cfg_list, n_test, parallel=True):
scores = None
if parallel:
# execute configs in parallel
executor = Parallel(n_jobs=cpu_count(), backend='multiprocessing')
tasks = (delayed(score_model)(data, n_test, cfg) for cfg in cfg_list)
scores = executor(tasks)
else:
scores = [score_model(data, n_test, cfg) for cfg in cfg_list]
# remove empty results
scores = [r for r in scores if r[1] != None]
# sort configs by error, asc
scores.sort(key=lambda tup: tup[1])
return scores
# create a set of sarima configs to try
def sarima_configs(seasonal=[0]):
models = list()
# define config lists
p_params = [0, 1, 2]
d_params = [0, 1]
q_params = [0, 1, 2]
t_params = ['n','c','t','ct']
P_params = [0, 1, 2]
D_params = [0, 1]
Q_params = [0, 1, 2]
m_params = seasonal
# create config instances
for p in p_params:
for d in d_params:
for q in q_params:
for t in t_params:
for P in P_params:
for D in D_params:
for Q in Q_params:
for m in m_params:
cfg = [(p,d,q), (P,D,Q,m), t]
models.append(cfg)
return models
if __name__ == '__main__':
# load dataset
series = read_csv('monthly-car-sales.csv', header=0, index_col=0)
data = series.values
print(data.shape)
# data split
n_test = 12
# model configs
cfg_list = sarima_configs(seasonal=[0,6,12])
# grid search
scores = grid_search(data, cfg_list, n_test)
print('done')
# list top 3 configs
for cfg, error in scores[:3]:
print(cfg, error)
L'exécution de l'exemple peut prendre quelques minutes sur du matériel moderne.
Les configurations de modèle et le RMSE sont imprimés au fur et à mesure que les modèles sont évalués. Les trois principales configurations de modèle et leurs erreurs sont signalées à la fin de l'analyse.
Remarque : Vos résultats peuvent varier en raison de la nature stochastique de l'algorithme ou de la procédure d'évaluation, ou des différences de précision numérique. Pensez à exécuter l’exemple plusieurs fois et comparez le résultat moyen.
On constate que le meilleur résultat était un RMSE d'environ 1 551 ventes avec la configuration suivante :
- Ordre de tendance : (0, 0, 0)
- Ordre saisonnier : (1, 1, 0, 12)
- Paramètre de tendance : « t » (tendance linéaire)
> Model[[(2, 1, 2), (2, 1, 1, 6), 'ct']] 2246.248
> Model[[(2, 1, 2), (2, 0, 2, 12), 'ct']] 10710.462
> Model[[(2, 1, 2), (2, 1, 2, 6), 'ct']] 2183.568
> Model[[(2, 1, 2), (2, 1, 0, 12), 'ct']] 2105.800
> Model[[(2, 1, 2), (2, 1, 1, 12), 'ct']] 2330.361
> Model[[(2, 1, 2), (2, 1, 2, 12), 'ct']] 31580326686.803
done
[(0, 0, 0), (1, 1, 0, 12), 't'] 1551.8423920342414
[(0, 0, 0), (2, 1, 1, 12), 'c'] 1557.334614575545
[(0, 0, 0), (1, 1, 0, 12), 'c'] 1559.3276311282675
Rallonges
Cette section répertorie quelques idées pour étendre le didacticiel que vous souhaiterez peut-être explorer.
- Transformations de données. Mettez à jour le framework pour prendre en charge les transformations de données configurables telles que la normalisation et la standardisation.
- Prévisions de tracé. Mettez à jour le cadre pour réajuster un modèle avec la meilleure configuration et prévoir l'intégralité de l'ensemble de données de test, puis tracez la prévision par rapport aux observations réelles de l'ensemble de test.
- Régler la quantité d'historique. Mettez à jour le cadre pour ajuster la quantité de données historiques utilisées pour ajuster le modèle (par exemple dans le cas des 10 années de données de température maximale).
Si vous explorez l’une de ces extensions, j’aimerais le savoir.
Lectures complémentaires
Cette section fournit plus de ressources sur le sujet si vous souhaitez approfondir.
Messages
- Comment créer un modèle ARIMA pour la prévision de séries chronologiques avec Python
- Comment rechercher sur une grille les hyperparamètres du modèle ARIMA avec Python
- Une introduction douce à l'autocorrélation et à l'autocorrélation partielle
Livres
- Chapitre 8 Modèles ARIMA, Prévisions : principes et pratiques, 2013.
- Chapitre 7, Modèles non stationnaires, Série chronologique d'introduction avec R, 2009.
API
- Une introduction douce à SARIMA pour la prévision de séries chronologiques en Python
- Analyse des séries chronologiques de Statsmodels par méthodes d'espace d'état
- API statsmodels.tsa.statespace.sarimax.SARIMAX
- API statsmodels.tsa.statespace.sarimax.SARIMAXResults
- Statsmodels Carnet SARIMAX
- Joblib : exécuter des fonctions Python en tant que tâches de pipeline
Articles
- Moyenne mobile intégrée autorégressive sur Wikipédia
Résumé
Dans ce didacticiel, vous avez découvert comment développer un cadre de recherche sur grille de tous les hyperparamètres du modèle SARIMA pour la prévision de séries temporelles univariées.
Concrètement, vous avez appris :
- Comment développer un cadre pour la recherche de grille de modèles SARIMA à partir de zéro en utilisant la validation progressive.
- Comment rechercher sur une grille les hyperparamètres du modèle SARIMA pour les données de séries chronologiques quotidiennes pour les naissances.
- Comment rechercher dans une grille les hyperparamètres du modèle SARIMA pour obtenir des données de séries chronologiques mensuelles sur les ventes de shampoings, les ventes de voitures et la température.
Avez-vous des questions ?
Posez vos questions dans les commentaires ci-dessous et je ferai de mon mieux pour y répondre.