: De middernachtparadox
Stel je dit eens voor. U bouwt een model om de vraag naar elektriciteit of het aantal taxi’s te voorspellen. Je geeft hem dus tijd (zoals minuten) vanaf middernacht. Schoon en eenvoudig. Rechts?
Nu ziet uw model het 23:59 (minuut 1439 op de dag) En 00:01 (minuut 1 van de dag). Voor jou liggen ze twee minuten uit elkaar. Voor jouw model liggen ze erg ver uit elkaar. Dat is de middernachtparadox. En ja, jouw model is waarschijnlijk tijdblind.
Waarom gebeurt dit?
Omdat de meeste machine learning-modellen getallen behandelen als rechte lijnen en niet als cirkels.
Lineaire regressie, KNN, SVM’s en zelfs neurale netwerken zullen getallen logisch behandelen, ervan uitgaande dat hogere getallen ‘meer’ zijn dan lagere. Ze weten niet dat de tijd om zich heen grijpt. Middernacht is het randgeval dat ze nooit vergeven.
Als u ooit zonder succes informatie per uur aan uw model hebt toegevoegd en u zich later afvroeg waarom uw model rond de daggrenzen worstelt, is dit waarschijnlijk de reden.
Het falen van standaardcodering
Laten we het hebben over de gebruikelijke benaderingen. Je hebt er waarschijnlijk minstens één gebruikt.
Je codeert uren als getallen van 0 tot en met 23. Nu is er een kunstmatige klif tussen uur 23 en uur 0. Dit model denkt dus dat middernacht de grootste sprong van de dag is. Is middernacht echter echt meer verschillend van 23.00 uur dan 22.00 uur van 21.00 uur?
Natuurlijk niet. Maar jouw model weet dat niet.
Hier ziet u de weergave van de uren in de “lineaire” modus.
# 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()
Wat als we de uren in één keer coderen? Vierentwintig binaire kolommen. Probleem opgelost, toch? Nou ja… gedeeltelijk. Je hebt de kunstmatige kloof gedicht, maar je bent de nabijheid kwijtgeraakt. 02.00 uur ligt niet dichter bij 03.00 uur dan bij 22.00 uur.
Je hebt ook de dimensionaliteit geëxplodeerd. Voor bomen is dat vervelend. Voor lineaire modellen is het waarschijnlijk inefficiënt.
Laten we dus verder gaan met een haalbaar alternatief.
- De oplossing: trigonometrische mapping
Hier is de mentaliteitsverandering:
Denk niet langer aan de tijd als een lijn. Zie het als een cirkel.
Een dag van 24 uur keert terug naar zichzelf. Je codering moet dus ook in een lus plaatsvinden, waarbij je in cirkels moet denken. Elk uur is een gelijkmatig verdeeld punt op een cirkel. Om een punt op een cirkel weer te geven, gebruik je niet één getal, maar in plaats daarvan gebruik je twee coördinaten: X En j.
Dat is waar sinus en cosinus binnenkomen.
De geometrie erachter
Elke hoek op een cirkel kan met behulp van sinus en cosinus worden toegewezen aan een uniek punt. Hierdoor krijgt uw model een vloeiende, continue weergave van de tijd.
plt.figure(figsize=(5, 5))
plt.scatter(df('hour_sin'), df('hour_cos'), linewidth=3)
plt.title('Hours in Cyclical Mode')
plt.xlabel('Hour')

Hier is de wiskundige formule om cycli voor uren van de dag te berekenen:

- Eerst,
2 * π * hour / 24zet elk uur om in een hoek. Middernacht en 23.00 uur eindigen bijna op dezelfde positie op de cirkel. - Dan sinus En cosinus projecteer die hoek in twee coördinaten.
- Deze twee waarden definiëren samen op unieke wijze het uur. Nu liggen 23:00 en 00:00 dichtbij in de speelruimte. Precies wat je al die tijd wilde.
Hetzelfde idee werkt voor minuten, dagen van de week of maanden van het jaar.
Code
Laten we experimenteren met deze dataset Apparaten Energievoorspelling (4). We zullen proberen de voorspelling te verbeteren met behulp van een Random Forest Regressor-model (een op bomen gebaseerd model).
Candanedo, L. (2017). Apparaten Energievoorspelling (dataset). UCI Machine Learning-opslagplaats. https://doi.org/10.24432/C5VC8G. Creative Commons 4.0-licentie.
# 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
Gegevens ophalen.
# 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)
Laten we een snel model maken met de lineair tijd eerst, als onze basis voor vergelijking.
# 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}')
De resultaten zijn hier.
Score: 0.9395797670166536
RMSE: 63.60964667197874
Vervolgens zullen we de cyclische tijdcomponenten coderen (day En hour) en train het model opnieuw.
# 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}')
En de resultaten. We zien een verbetering van 1% in de score en 1 punt in de RMSE.
Score: 0.9416365489096074
RMSE: 62.87008070927842
Ik weet zeker dat dit er niet veel uitziet, maar laten we niet vergeten dat dit speelgoedvoorbeeld een eenvoudig, kant-en-klaar model gebruikt zonder enige gegevensverwerking of opschoning. We zien vooral het effect van de sinus- en cosinustransformatie.
Wat hier werkelijk gebeurt, is dat in het echte leven de vraag naar elektriciteit niet om middernacht wordt gereset. En nu ziet uw model eindelijk die continuïteit.
Waarom je zowel sinus als cosinus nodig hebt
Val niet in de verleiding om te gebruiken alleen sinusomdat het genoeg voelt. Eén kolom in plaats van twee. Schoner, toch?
Helaas breekt het de symmetrie. Op een 24-uursklok kunnen 06.00 uur en 18.00 uur dezelfde sinuswaarde produceren. Verschillende tijden met identieke codering kunnen slecht zijn omdat het model nu de ochtendspits met de avondspits verwart. Niet ideaal dus, tenzij je van verwarde voorspellingen houdt.
Het gebruik van zowel sinus als cosinus lost dit op. Samen geven ze elk uur een unieke vingerafdruk op de cirkel. Zie het als breedte- en lengtegraad. Je hebt beide nodig om te weten waar je bent.
Impact en resultaten in de echte wereld
Helpt dit modellen eigenlijk? Ja. Vooral bepaalde.
Op afstand gebaseerde modellen
KNN en SVM’s zijn sterk afhankelijk van afstandsberekeningen. Cyclische codering voorkomt valse ‘lange afstanden’ bij grenzen. Je buren worden eigenlijk weer buren.
Neurale netwerken
Neurale netwerken leren sneller dankzij soepele featureruimtes. Cyclische codering verwijdert scherpe discontinuïteiten om middernacht. Dat betekent meestal snellere convergentie en betere stabiliteit.
Op bomen gebaseerde modellen
Gradient Boosted Trees zoals XGBoost of LightGBM kunnen deze patronen uiteindelijk leren. Cyclische codering geeft hen een voorsprong. Als je om prestaties en interpreteerbaarheid geeft, is het de moeite waard.
7. Wanneer moet u dit gebruiken?
Stel jezelf altijd de vraag: Herhaalt deze functie zich in een cyclus? Zo ja, overweeg dan cyclische codering.
Veel voorkomende voorbeelden zijn:
- Uur van de dag
- Dag van de week
- Maand van het jaar
- Windrichting (graden)
- Als het in een lus zit, kun je proberen het als een lus te coderen.
Voordat je gaat
Tijd is niet slechts een getal. Het is een coördinaat op een cirkel.
Als je het als een rechte lijn behandelt, kan je model struikelen over grenzen en moeite hebben om die variabele te begrijpen als een cyclus, iets dat zich herhaalt en een patroon heeft.
Cyclische codering met sinus en cosinus lost dit op elegante wijze op, waardoor de nabijheid behouden blijft, artefacten worden verminderd en modellen sneller leren.
Dus de volgende keer dat uw voorspellingen er bij dagveranderingen raar uitzien, probeer dan dit nieuwe hulpmiddel dat u heeft geleerd, en laat het uw model laten schitteren zoals het hoort.
Als je deze inhoud leuk vond, vind dan meer van mijn werk en mijn contacten op mijn website.
https://gustavorsantos.me
GitHub-opslagplaats
Hier is de hele code van deze oefening.
https://github.com/gurezende/Time-Series/tree/main/Sine%20Cosine%20Time%20Encode
Referenties en verder lezen
(1. Coderingsuren Stack Exchange): https://stats.stackexchange.com/questions/451295/encoding-cyclical-feature-minutes-and-hours
(2. NumPy trigonometrische functies): https://numpy.org/doc/stable/reference/routines.math.html
(3. Praktische discussie over conjuncturele kenmerken):
https://www.kaggle.com/code/avanwyk/encoding-cyclical-features-for-deep-learning
(4. Dataset energievoorspelling apparaten) https://archive.ics.uci.edu/dataset/374/appliances+energy+prediction


