¿Su modelo es ciego al tiempo? El caso de la codificación de características cíclicas

: La paradoja de la medianoche

Imagínese esto. Estás construyendo un modelo para predecir la demanda de electricidad o la recogida de taxis. Entonces, le das tiempo (como minutos) a partir de la medianoche. Limpio y sencillo. ¿Bien?

Ahora su modelo ve las 23:59 (minuto 1439 del día) y las 00:01 (minuto 1 del día). Para ti, están separados por dos minutos. Para tu modelo, están muy separados. Ésa es la paradoja de la medianoche. Y sí, su modelo probablemente sea ciego al tiempo.

¿Por qué sucede esto?

Porque la mayoría de los modelos de aprendizaje automático tratan los números como líneas rectas, no como círculos.

La regresión lineal, KNN, SVM e incluso las redes neuronales tratarán los números de manera lógica, asumiendo que los números más altos son “más” que los más bajos. No saben que el tiempo pasa. Midnight es el caso extremo que nunca perdonan.

Si alguna vez agregó información horaria a su modelo sin éxito y luego se pregunta por qué su modelo tiene problemas con los límites diarios, probablemente este sea el motivo.

El fracaso de la codificación estándar

Hablemos de los enfoques habituales. Probablemente hayas utilizado al menos uno de ellos.

Codificas las horas como números del 0 al 23. Ahora hay un acantilado artificial entre la hora 23 y la hora 0. Por lo tanto, este modelo cree que la medianoche es el salto más grande del día. Sin embargo, ¿la medianoche es realmente más diferente de las 11 p.m. que las 10 p.m. de las 9 p.m.?

Por supuesto que no. Pero tu modelo no lo sabe.

Aquí está la representación de las horas cuando están en el modo “lineal”.

# Generar datos date_today = pd.to_datetime(‘today’).normalize() datetime_24_hours = pd.date_range(start=date_today, period=24, freq=’h’) df = pd.DataFrame({‘dt’: datetime_24_hours}) df[‘hour’] = gl[‘dt’].dt.hour # Calcular el seno y el coseno df[“hour_sin”] = np.sin(2 * np.pi * df[“hour”] / 24) gl[“hour_cos”] = np.cos(2 * np.pi * df[“hour”] / 24) # Trazar las horas en modo lineal plt.figure(figsize=(15, 5)) plt.plot(df[‘hour’], [1]*24, ancho de línea=3) plt.title(‘Horas en modo lineal’) plt.xlabel(‘Hora’) plt.xticks(np.arange(0, 24, 1)) plt.ylabel(‘Valor’) plt.show()

Horas en el Modo Lineal. Imagen del autor.

¿Qué pasa si codificamos las horas en caliente? Veinticuatro columnas binarias. Problema resuelto, ¿verdad? Bueno… parcialmente. Arreglaste la brecha artificial, pero perdiste proximidad. Las 2 a. m. ya no están más cerca de las 3 a. m. que de las 10 p. m.
También explotaste la dimensionalidad. Para los árboles, eso es molesto. Para modelos lineales, probablemente sea ineficiente.

Entonces, pasemos a una alternativa factible.

La solución: mapeo trigonométrico

Aquí está el cambio de mentalidad:

Deja de pensar en el tiempo como una línea. Piénselo como un círculo.

Un día de 24 horas vuelve a sí mismo. Entonces tu codificación también debería repetirse, pensando en círculos. Cada hora es un punto espaciado uniformemente en un círculo. Ahora, para representar un punto en un círculo, no usas un número, sino dos coordenadas: x e y.

Ahí es donde entran el seno y el coseno.

La geometría detrás de esto

Cada ángulo de un círculo se puede asignar a un punto único usando seno y coseno. Esto le da a su modelo una representación fluida y continua del tiempo.

plt.figure(tamaño de figura=(5, 5)) plt.scatter(df[‘hour_sin’]df[‘hour_cos’]ancho de línea=3) plt.title(‘Horas en modo cíclico’) plt.xlabel(‘Hora’)

Horas en modo cíclico después del seno y el coseno. Imagen del autor.

Aquí está la fórmula matemática para calcular los ciclos de las horas del día:

Primero, 2 * π * hora / 24 convierte cada hora en un ángulo. La medianoche y las 11 de la noche terminan casi en la misma posición en el círculo. Luego el seno y el coseno proyectan ese ángulo en dos coordenadas. Esos dos valores juntos definen de forma única la hora. Ahora las 23:00 y las 00:00 están cerca en el espacio de funciones. Exactamente lo que quisiste desde el principio.

La misma idea funciona para minutos, días de la semana o meses del año.

Código

Experimentemos con este conjunto de datos Predicción de energía de electrodomésticos [4]. Intentaremos mejorar la predicción utilizando un modelo de regresor de bosque aleatorio (un modelo basado en árboles).

Candanedo, L. (2017). Predicción de energía de electrodomésticos [Dataset]. Repositorio de aprendizaje automático de la UCI. https://doi.org/10.24432/C5VC8G. Licencia Creative Commons 4.0.

# Importaciones desde sklearn.ensemble importar RandomForestRegressor desde sklearn.model_selection importar train_test_split desde sklearn.metrics importar root_mean_squared_error desde ucimlrepo importar fetch_ucirepo

Obtener datos.

# recuperar el conjunto de datos dispositivos_energy_prediction = fetch_ucirepo(id=374) # datos (como marcos de datos de pandas) X = dispositivos_energy_prediction.data.features y = dispositivos_energy_prediction.data.targets # A Pandas df = pd.concat([X, y]eje=1) gl[‘date’] = gl[‘date’].aplicar(lambda x:x[:10] + ‘ ‘ + x[11:]) df[‘date’] = pd.to_datetime(df[‘date’]) df[‘month’] = gl[‘date’].dt.mes df[‘day’] = gl[‘date’].dt.día df[‘hour’] = gl[‘date’].dt.hora df.cabeza(3)

Primero creemos un modelo rápido con el tiempo lineal, como nuestra línea de base para la comparación.

# X e y # X = df.drop([‘Appliances’, ‘rv1’, ‘rv2’, ‘date’]eje=1) X = gl[[‘hour’, ‘day’, ‘T1’, ‘RH_1’, ‘T_out’, ‘Press_mm_hg’, ‘RH_out’, ‘Windspeed’, ‘Visibility’, ‘Tdewpoint’]]y = gl[‘Appliances’]

# Entrenar prueba dividir X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) # Ajustar el modelo lr = RandomForestRegressor().fit(X_train, y_train) # Puntuación print(f’Score: {lr.score(X_train, y_train)}’) # Prueba RMSE y_pred = lr.predict(X_test) rmse = root_mean_squared_error(y_test, y_pred) print(f’RMSE: {rmse}’)

Los resultados están aquí.

Puntuación: 0,9395797670166536 RMSE: 63,60964667197874

A continuación, codificaremos los componentes de tiempo cíclicos (día y hora) y volveremos a entrenar el modelo.

# Sumar horas cíclicas sin y coseno df[‘hour_sin’] = np.sin(2 * np.pi * df[‘hour’] / 24) gl[‘hour_cos’] = np.cos(2 * np.pi * df[‘hour’] / 24) gl[‘day_sin’] = np.sin(2 * np.pi * df[‘day’] / 31) gl[‘day_cos’] = np.cos(2 * np.pi * df[‘day’] / 31) # X e y X = gl[[‘hour_sin’, ‘hour_cos’, ‘day_sin’, ‘day_cos’,’T1′, ‘RH_1’, ‘T_out’, ‘Press_mm_hg’, ‘RH_out’, ‘Windspeed’, ‘Visibility’, ‘Tdewpoint’]]y = gl[‘Appliances’]

# Entrenar prueba dividir X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) # Ajustar el modelo lr_cycle = RandomForestRegressor().fit(X_train, y_train) # Puntuación print(f’Score: {lr_cycle.score(X_train, y_train)}’) # Prueba RMSE y_pred = lr_cycle.predict(X_test) rmse = root_mean_squared_error(y_test, y_pred) print(f’RMSE: {rmse}’)

Y los resultados. Estamos viendo una mejora del 1% en la puntuación y de 1 punto en el RMSE.

Puntuación: 0,9416365489096074 RMSE: 62,87008070927842

Estoy seguro de que esto no parece mucho, pero recordemos que este ejemplo de juguete utiliza un modelo simple y listo para usar sin ningún tratamiento o limpieza de datos. Estamos viendo principalmente el efecto de la transformación seno y coseno.

Lo que realmente está sucediendo aquí es que, en la vida real, la demanda de electricidad no se restablece a medianoche. Y ahora su modelo finalmente ve esa continuidad.

Por qué necesitas tanto el seno como el coseno

No caigas en la tentación de usar solo seno, ya que te parece suficiente. Una columna en lugar de dos. Limpiador, ¿verdad?

Desafortunadamente, rompe la simetría. En un reloj de 24 horas, las 6 a. m. y las 6 p. m. pueden producir el mismo valor sinusoidal. Diferentes horas con codificación idéntica pueden ser malas porque el modelo ahora confunde la hora pico de la mañana con la hora pico de la tarde. Por lo tanto, no es ideal a menos que disfrutes de predicciones confusas.

Usar tanto el seno como el coseno soluciona este problema. Juntos, le dan a cada hora una huella digital única en el círculo. Piense en ello como la latitud y la longitud. Necesitas ambos para saber dónde estás.

Impacto y resultados en el mundo real

Entonces, ¿esto realmente ayuda a los modelos? Sí. Especialmente ciertos.

Modelos basados ​​en distancia

KNN y SVM dependen en gran medida de los cálculos de distancia. La codificación cíclica evita falsas “largas distancias” en los límites. De hecho, tus vecinos vuelven a ser vecinos.

Redes neuronales

Las redes neuronales aprenden más rápido con espacios de funciones fluidos. La codificación cíclica elimina discontinuidades marcadas a medianoche. Esto suele significar una convergencia más rápida y una mayor estabilidad.

Modelos basados ​​en árboles

Los árboles potenciados por gradiente como XGBoost o LightGBM eventualmente pueden aprender estos patrones. La codificación cíclica les da una ventaja. Si le importa el rendimiento y la interpretabilidad, vale la pena.

7. ¿Cuándo debería utilizar esto?

Hágase siempre la pregunta: ¿Esta característica se repite en un ciclo? En caso afirmativo, considere la codificación cíclica.

Ejemplos comunes son:

Hora del día Día de la semana Mes del año Dirección del viento (grados) Si se repite, puede intentar codificarlo como un bucle.

Antes de ir

El tiempo no es sólo un número. Es una coordenada en un círculo.

Si lo trata como una línea recta, su modelo puede tropezar con los límites y tener dificultades para entender esa variable como un ciclo, algo que se repite y tiene un patrón.

La codificación cíclica con seno y coseno soluciona este problema de manera elegante, preservando la proximidad, reduciendo los artefactos y ayudando a los modelos a aprender más rápido.

Así que la próxima vez que tus predicciones parezcan extrañas en torno a los cambios de día, prueba esta nueva herramienta que has aprendido y deja que haga que tu modelo brille como debería.

Si te gustó este contenido, encuentra más de mi trabajo y mis contactos en mi sitio web.

https://gustavorsantos.me

Repositorio GitHub

Aquí está el código completo de este ejercicio.

https://github.com/gurezende/Time-Series/tree/main/Sine%20Cosine%20Time%20Encode

Referencias y lecturas adicionales

[1. Encoding hours Stack Exchange]: https://stats.stackexchange.com/questions/451295/encoding-cyclal-feature-minutos-y-horas

[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-cyclal-features-for-deep-learning

[4. Appliances Energy Prediction Dataset] https://archive.ics.uci.edu/dataset/374/appliances+energy+prediction