
Votre modèle est-il aveugle au temps ? Les arguments en faveur du codage de fonctionnalités cyclique
: Le paradoxe de minuit
Imaginez ça. Vous construisez un modèle pour prédire la demande d’électricité ou les prises en charge des taxis. Ainsi, vous lui donnez du temps (par exemple des minutes) à partir de minuit. Propre et simple. Droite?
Maintenant, votre modèle voit 23h59 (minute 1439 du jour) et 00:01 (minute 1 de la journée). Pour vous, ils sont espacés de deux minutes. Pour votre modèle, ils sont très éloignés. C’est le paradoxe de minuit. Et oui, votre modèle est probablement aveugle au temps.
Pourquoi cela arrive-t-il ?
Parce que la plupart des modèles d’apprentissage automatique traitent les nombres comme des lignes droites et non comme des cercles.
La régression linéaire, les KNN, les SVM et même les réseaux de neurones traiteront les nombres de manière logique, en supposant que les nombres plus élevés sont « plus » que les nombres inférieurs. Ils ne savent pas que le temps passe. Minuit est le cas extrême qu’ils ne pardonnent jamais.
Si vous avez déjà ajouté des informations horaires à votre modèle sans succès et que vous vous demandez plus tard pourquoi votre modèle a du mal à respecter les limites quotidiennes, voici probablement pourquoi.
L’échec du codage standard
Parlons des approches habituelles. Vous en avez probablement utilisé au moins un.
Vous codez les heures sous forme de nombres de 0 à 23. Il existe désormais une falaise artificielle entre l’heure 23 et l’heure 0. Ainsi, ce modèle pense que minuit est le plus grand saut de la journée. Cependant, minuit est-il vraiment plus différent de 23 heures que 22 heures ne l’est de 21 heures ?
Bien sûr que non. Mais votre modèle ne le sait pas.
Voici la représentation des heures lorsqu’elles sont en mode « linéaire ».
# Generate data
date_today = pd.to_datetime('today').normalize()
datetime_24_hours = pd.date_range(start=date_today, periods=24, freq='h')
df = pd.DataFrame({'dt': datetime_24_hours})
df['hour'] = df['dt'].dt.hour
# Calculate Sin and Cosine
df["hour_sin"] = np.sin(2 * np.pi * df["hour"] / 24)
df["hour_cos"] = np.cos(2 * np.pi * df["hour"] / 24)
# Plot the Hours in Linear mode
plt.figure(figsize=(15, 5))
plt.plot(df['hour'], [1]*24, linewidth=3)
plt.title('Hours in Linear Mode')
plt.xlabel('Hour')
plt.xticks(np.arange(0, 24, 1))
plt.ylabel('Value')
plt.show()

Et si on codait les heures à chaud ? Vingt-quatre colonnes binaires. Problème résolu, non ? Eh bien… partiellement. Vous avez réparé l’écart artificiel, mais vous avez perdu la proximité. 2 heures du matin n’est plus plus proche de 3 heures du matin que de 22 heures.
Vous avez également fait exploser la dimensionnalité. Pour les arbres, c’est ennuyeux. Pour les modèles linéaires, c’est probablement inefficace.
Passons donc à une alternative réalisable.
- La solution : la cartographie trigonométrique
Voici le changement de mentalité :
Arrêtez de considérer le temps comme une ligne. Pensez-y comme à un cercle.
Une journée de 24 heures revient sur elle-même. Votre encodage doit donc également boucler, en pensant en rond. Chaque heure est un point régulièrement espacé sur un cercle. Maintenant, pour représenter un point sur un cercle, vous n’utilisez pas un seul nombre, mais plutôt deux coordonnées: x et oui.
C’est là qu’interviennent le sinus et le cosinus.
La géométrie derrière
Chaque angle d’un cercle peut être mappé à un point unique en utilisant le sinus et le cosinus. Cela donne à votre modèle une représentation fluide et continue du temps.
plt.figure(figsize=(5, 5))
plt.scatter(df['hour_sin'], df['hour_cos'], linewidth=3)
plt.title('Hours in Cyclical Mode')
plt.xlabel('Hour')

Voici la formule mathématique pour calculer les cycles pour les heures de la journée :

- D’abord,
2 * π * hour / 24convertit chaque heure en angle. Minuit et 23 heures se retrouvent presque à la même position sur le cercle. - Alors sinus et cosinus projetez cet angle en deux coordonnées.
- Ces deux valeurs ensemble définissent de manière unique l’heure. Désormais, 23h00 et minuit sont proches dans l’espace des fonctionnalités. Exactement ce que vous vouliez depuis le début.
La même idée fonctionne pour les minutes, les jours de la semaine ou les mois de l’année.
Code
Expérimentons avec cet ensemble de données Prédiction énergétique des appareils électroménagers [4]. Nous essaierons d’améliorer la prédiction en utilisant un modèle Random Forest Regressor (un modèle arborescent).
Candanedo, L. (2017). Prédiction énergétique des appareils électroménagers [Dataset]. Référentiel d’apprentissage automatique UCI. https://doi.org/10.24432/C5VC8G. Licence Creative Commons 4.0.
# Imports
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split
from sklearn.metrics import root_mean_squared_error
from ucimlrepo import fetch_ucirepo
Obtenez des données.
# fetch dataset
appliances_energy_prediction = fetch_ucirepo(id=374)
# data (as pandas dataframes)
X = appliances_energy_prediction.data.features
y = appliances_energy_prediction.data.targets
# To Pandas
df = pd.concat([X, y], axis=1)
df['date'] = df['date'].apply(lambda x: x[:10] + ' ' + x[11:])
df['date'] = pd.to_datetime(df['date'])
df['month'] = df['date'].dt.month
df['day'] = df['date'].dt.day
df['hour'] = df['date'].dt.hour
df.head(3)
Créons un modèle rapide avec le linéaire temps d’abord, comme base de comparaison.
# X and y
# X = df.drop(['Appliances', 'rv1', 'rv2', 'date'], axis=1)
X = df[['hour', 'day', 'T1', 'RH_1', 'T_out', 'Press_mm_hg', 'RH_out', 'Windspeed', 'Visibility', 'Tdewpoint']]
y = df['Appliances']
# Train Test Split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# Fit the model
lr = RandomForestRegressor().fit(X_train, y_train)
# Score
print(f'Score: {lr.score(X_train, y_train)}')
# Test RMSE
y_pred = lr.predict(X_test)
rmse = root_mean_squared_error(y_test, y_pred)
print(f'RMSE: {rmse}')
Les résultats sont ici.
Score: 0.9395797670166536
RMSE: 63.60964667197874
Ensuite, nous coderons les composantes temporelles cycliques (day et hour) et recyclez le modèle.
# Add cyclical hours sin and cosine
df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24)
df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24)
df['day_sin'] = np.sin(2 * np.pi * df['day'] / 31)
df['day_cos'] = np.cos(2 * np.pi * df['day'] / 31)
# X and y
X = df[['hour_sin', 'hour_cos', 'day_sin', 'day_cos','T1', 'RH_1', 'T_out', 'Press_mm_hg', 'RH_out', 'Windspeed', 'Visibility', 'Tdewpoint']]
y = df['Appliances']
# Train Test Split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# Fit the model
lr_cycle = RandomForestRegressor().fit(X_train, y_train)
# Score
print(f'Score: {lr_cycle.score(X_train, y_train)}')
# Test RMSE
y_pred = lr_cycle.predict(X_test)
rmse = root_mean_squared_error(y_test, y_pred)
print(f'RMSE: {rmse}')
Et les résultats. Nous constatons une amélioration de 1% du score et de 1 point du RMSE.
Score: 0.9416365489096074
RMSE: 62.87008070927842
Je suis sûr que cela ne ressemble pas à grand-chose, mais rappelons-nous que cet exemple de jouet utilise un modèle simple prêt à l’emploi sans aucun traitement ni nettoyage des données. Nous voyons principalement l’effet de la transformation sinus et cosinus.
Ce qui se passe réellement ici, c’est que, dans la vraie vie, la demande d’électricité ne se réinitialise pas à minuit. Et maintenant, votre modèle voit enfin cette continuité.
Pourquoi vous avez besoin à la fois du sinus et du cosinus
Ne tombez pas dans la tentation d’utiliser seulement sinuscomme cela semble suffisant. Une colonne au lieu de deux. Plus propre, non ?
Malheureusement, cela brise la symétrie. Sur une horloge de 24 heures, 6 heures du matin et 18 heures peuvent produire la même valeur sinusoïdale. Des heures différentes avec un encodage identique peuvent être mauvaises car le modèle confond désormais l’heure de pointe du matin avec l’heure de pointe du soir. Ce n’est donc pas idéal, sauf si vous aimez les prédictions confuses.
L’utilisation à la fois du sinus et du cosinus résout ce problème. Ensemble, ils donnent à chaque heure une empreinte unique sur le cercle. Pensez-y comme à la latitude et à la longitude. Vous avez besoin des deux pour savoir où vous êtes.
Impact et résultats dans le monde réel
Alors, est-ce que cela aide réellement les modèles ? Oui. Surtout certains.
Modèles basés sur la distance
KNN et SVM s’appuient fortement sur les calculs de distance. Le codage cyclique empêche les fausses « longues distances » aux frontières. Vos voisins redeviennent effectivement voisins.
Réseaux de neurones
Les réseaux de neurones apprennent plus rapidement grâce à des espaces de fonctionnalités fluides. Le codage cyclique supprime les discontinuités marquées à minuit. Cela signifie généralement une convergence plus rapide et une meilleure stabilité.
Modèles basés sur des arbres
Les arbres à gradient boosté comme XGBoost ou LightGBM peuvent éventuellement apprendre ces modèles. L’encodage cyclique leur donne une longueur d’avance. Si vous vous souciez des performances et de l’interprétabilité, cela en vaut la peine.
7. Quand devriez-vous l’utiliser ?
Posez-vous toujours la question : Cette fonctionnalité se répète-t-elle dans un cycle ? Si oui, envisagez le codage cyclique.
Les exemples courants sont :
- Heure de la journée
- Jour de la semaine
- Mois de l’année
- Direction du vent (degrés)
- S’il boucle, vous pouvez essayer de l’encoder comme une boucle.
Avant de partir
Le temps n’est pas qu’un chiffre. C’est une coordonnée sur un cercle.
Si vous la traitez comme une ligne droite, votre modèle peut trébucher sur les limites et avoir du mal à comprendre cette variable comme un cycle, quelque chose qui se répète et qui a un modèle.
L’encodage cyclique avec sinus et cosinus corrige ce problème avec élégance, préservant la proximité, réduisant les artefacts et aidant les modèles à apprendre plus rapidement.
Alors la prochaine fois que vos prédictions semblent bizarres en fonction des changements de jour, essayez ce nouvel outil que vous avez appris et laissez-le faire briller votre modèle comme il se doit.
Si vous avez aimé ce contenu, retrouvez plus de mon travail et mes contacts sur mon site Web.
Dépôt GitHub
Voici tout le code de cet exercice.
https://github.com/gurezende/Time-Series/tree/main/Sine%20Cosine%20Time%20Encode
Références et lectures complémentaires
[1. Encoding hours Stack Exchange]: https://stats.stackexchange.com/questions/451295/encoding-cyclical-feature-minutes-and-hours
[2. NumPy trigonometric functions]: https://numpy.org/doc/stable/reference/routines.math.html
[3. Practical discussion on cyclical features]:
https://www.kaggle.com/code/avanwyk/encoding-cyclical-features-for-deep-learning
[4. Appliances Energy Prediction Dataset] https://archive.ics.uci.edu/dataset/374/appliances+energy+prediction



