: De middernachtparadox
Stel je dit eens voor. Je bouwt een model om de elektriciteitsvraag of het aantal taxi’s te voorspellen. Je geeft het dus een tijd (bijvoorbeeld minuten) vanaf middernacht. Schoon en eenvoudig. Juist?
Nu ziet uw model het 23:59 (minuut 1439 van de dag) En 00:01 (1e minuut van de dag). Voor jou is het twee minuten. Voor uw model is de afstand erg groot. Dat is de middernachtparadox. En ja, jouw model is misschien tijdblind.
Waarom gebeurt dit?
Omdat de meeste machine learning-modellen getallen behandelen als rechte lijnen en niet als cirkels.
Lineaire regressie, KNN, SVM en zelfs neurale netwerken zullen getallen logisch behandelen, ervan uitgaande dat hogere getallen ‘meer’ betekenen dan lagere getallen. Ze weten niet dat de tijd blijft draaien. Middernacht was het laatste geval dat ze nooit vergeven.
Als u ooit zonder succes informatie per uur aan uw model hebt toegevoegd en u zich vervolgens hebt afgevraagd waarom uw model onder de dagelijkse beperkingen worstelde, is dit misschien de reden.
Standaard coderingsfout
Laten we het hebben over de gebruikelijke aanpak. Je hebt er waarschijnlijk minstens één gebruikt.
Je codeert uren als getallen van 0 tot en met 23. Nu is er een kunstmatige kloof tussen 23 uur en 0 uur. Daarom beschouwt het model middernacht als de grootste sprong op een dag. Is middernacht echter meer verschillend van 23.00 uur dan 22.00 uur van 21.00 uur?
Natuurlijk niet. Maar jouw model weet dat niet.
Hier is een weergave van de klok wanneer deze zich in de “lineaire” modus bevindt.
# 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 klok één-hot coderen? Vierentwintig binaire kolommen. Probleem opgelost, toch? Ja… sommige. Je herstelt de kunstmatige kloof, maar je verliest de nabijheid. 02.00 uur ligt niet dichter bij 3.00 uur dan 22.00 uur.
Je blaast ook afmetingen op. Voor bomen is het vervelend. Voor lineaire modellen kan dit inefficiënt zijn.
Laten we dus verder gaan met een haalbaar alternatief.
- De oplossing: trigonometrische mapping
Hier is een mentaliteitsverandering:
Denk niet langer aan tijd als een lijn. Zie het als een cirkel.
Een dag van 24 uur keert zichzelf terug. Uw codering moet dus ook iteratief zijn en in cirkels denken. Elk uur is een punt op gelijke afstand van een cirkel. Om een punt op een cirkel weer te geven, gebruik je dus niet slechts één getal, maar twee coördinaten: X En Jij.
Dat is waar de sinus- en cosinusfuncties 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')

Het volgende is een wiskundige formule voor het berekenen van de uurcyclus in een dag:

- Eerst,
2 * π * hour / 24elk uur omzetten in een hoek. Middernacht en 23.00 uur eindigen op bijna dezelfde positie op de cirkel. - Dan sinus En cosinus projecteer die hoek in twee coördinaten.
- Deze twee waarden bepalen samen op unieke wijze het uur. Het is nu 23:00 uur en 00:00 uur nadert in de speelkamer. Precies wat je al die tijd wilde.
Hetzelfde idee is van toepassing op minuten, dagen van de week of maanden van het jaar.
Code
Laten we experimenteren met deze dataset Apparatuurenergievoorspelling (4). We zullen proberen de voorspellingen te verbeteren met behulp van Random Forest Regressor-modellen (boomgebaseerde modellen).
Candanedo, L. (2017). Apparatuurenergievoorspelling (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
Verkrijg de gegevens.
# 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 lineair eerste keer, als basis voor onze 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 coderen we de cyclustijdcomponenten (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 zagen een stijging van 1% in score en 1 punt in RMSE.
Score: 0.9416365489096074
RMSE: 62.87008070927842
Ik weet zeker dat dit er niet veel uitziet, maar houd er rekening mee dat dit speelgoedvoorbeeld een eenvoudig, kant-en-klaar model gebruikt zonder enige gegevensverwerking of opschoning. We zien vooral de effecten van sinus- en cosinustransformaties.
Wat hier feitelijk gebeurt, is dat de vraag naar elektriciteit zich in het echte leven niet tegen middernacht herstelt. En nu ziet uw model eindelijk die continuïteit.
Waarom je sinus en cosinus nodig hebt
Laat je niet verleiden om het te gebruiken gewoon sinusomdat het genoeg voelt. Eén kolom in plaats van twee. Schoner, toch?
Helaas doorbreekt dat de symmetrie. Op een 24-uursklok kunnen 6.00 uur en 18.00 uur dezelfde sinuswaarde produceren. Verschillende tijden met dezelfde codering kunnen schadelijk zijn omdat het huidige model de ochtendspits verwart met de avondspits. Niet ideaal dus, tenzij je van verwarrende voorspellingen houdt.
Het gebruik van sinus en cosinus lost dit op. Samen geven ze elk horloge zijn 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 echt? Ja. Vooral bepaalde.
Op afstand gebaseerd model
KNN en SVM zijn sterk afhankelijk van afstandsberekeningen. Cyclische codering voorkomt valse “lange afstanden” aan de grens. Je buren worden eigenlijk weer buren.
Neurale netwerken
Neurale netwerken leren sneller met een vloeiende functieruimte. Cyclische codering elimineert scherpe discontinuïteiten midden in de nacht. Dit betekent doorgaans snellere convergentie en betere stabiliteit.
Op bomen gebaseerd model
Gradient Boosted Trees zoals XGBoost of LightGBM kunnen deze patronen uiteindelijk leren. Cyclische codering geeft hen een voorsprong. Als je om uitvoering en interpretatie geeft, is het de moeite waard.
7. Wanneer moet je het gebruiken?
Stel jezelf altijd de vragen: Herhaalt dit kenmerk zich in een cyclus? Zo ja, overweeg dan cyclisch coderen.
Veel voorkomende voorbeelden zijn:
- Uren per dag
- Dag van de week
- Maanden van het jaar
- Windrichting (graden)
- Als het zich herhaalt, kun je proberen het als een lus te coderen.
Voordat je gaat
Tijd is niet slechts een getal. Dat zijn coördinaten op een cirkel.
Als je het als een rechte lijn behandelt, kan je model op beperkingen stuiten en moeite hebben om de variabele te begrijpen als een cyclus, iets dat zich herhaalt en een patroon heeft.
Cyclische codering met sinussen en cosinussen verbetert dit op elegante wijze, waardoor de nabijheid behouden blijft, artefacten worden verminderd en het model sneller leert.
Dus, de volgende keer dat uw voorspellingen er vreemd uitzien als de dag verandert, probeer dan dit nieuwe hulpmiddel dat u heeft geleerd, en laat het uw model laten schitteren zoals het hoort.
Als je deze inhoud leuk vindt, vind dan meer van mijn werk en neem contact met mij op via mijn website.
https://gustavorsantos.me
GitHub-opslagplaats
Hier is de volledige code voor deze oefening.
https://github.com/gurezende/Time-Series/tree/main/Sine%20Cosine%20Time%20Encode
Referenties en verder lezen
(1. Codeertijden voor 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 bespreking van de kenmerken van cycli):
https://www.kaggle.com/code/avanwyk/encoding-cyclical-features-for-deep-learning
(4. Dataset voor energievoorspelling van apparatuur) https://archive.ics.uci.edu/dataset/374/appliances+energy+prediction



