Cómo equivocarse usando Machine Learning


En el momento de escribir esto estamos a 18 de junio de 2022. En la primera ola de calor del año dicen. Según la AEMET, 34 grados de temperatura máxima en Palma. Yo acabo de usar Machine Learning para predecir que la temperatura máxima de hoy será de 24.8 grados y os voy a explicar cómo lo he hecho. Weee, best post ever. De aquí salís con un máster en como cagarla xD

Los datos

Usaremos datos históricos de la AEMET. En este ejemplo yo los he usado para hacer una mala predicción pero los datos en sí no tienen ningún problema y son totalmente recomendables, de hecho seguramente sean lo mejor que existe sobre meteorología y climatología en España.

Podéis descargar el mismo data set o cualquier otro que os apetezca de https://opendata.aemet.es/centrodedescargas/inicio. Hay que registrarse para obtener una API Key pero es muy sencillo. Yo por proximidad he optado por bajarme los datos climatológicos del aeropuerto de Palma de Mallorca (PMI) desde el 1 de enero de 2018 hasta ahora (que en el momento de descargarlo hace unos días me ha dado hasta el 11 de junio de 2022).

Los datos vienen en JSON, en una lista de diccionarios como este (uno por día, total 1624 en mi caso):

{
  "fecha" : "2018-01-01",
  "indicativo" : "B278",
  "nombre" : "PALMA DE MALLORCA, AEROPUERTO",
  "provincia" : "ILLES BALEARS",
  "altitud" : "8",
  "tmed" : "15,2",
  "prec" : "0,0",
  "tmin" : "12,8",
  "horatmin" : "07:07",
  "tmax" : "17,7",
  "horatmax" : "21:45",
  "dir" : "32",
  "velmedia" : "6,7",
  "racha" : "21,1",
  "horaracha" : "00:50",
  "sol" : "8,1",
  "presMax" : "1029,2",
  "horaPresMax" : "10",
  "presMin" : "1021,1",
  "horaPresMin" : "00"
}

Para cargarlo en una base de datos irá bien tenerlo en CSV y quitar los campos que interesan menos, así que usé lo primero que encontré en Google para hacer la conversión https://tableconvert.com/json-to-csv. No es que recomiende subir ahí vuestros secretos más grandes, pero para formatear datos que ya son públicos va bien 😉

En cuanto a campos, ignoré los que siempre son iguales para este data set (indicativo, nombre, provincia y altitud) y las horas:minutos, porque me daban un problema de tipos con la herramienta MindsDB que veremos más tarde, y era más fácil quitar las horas que arreglarlo, total no pensaba hacer nada con las horas.

Por supuesto hubo que limpiar los datos. Para una carga manual una vez en la vida, lo hice con el vim.

  • Cambié las comas decimales a puntos decimales sin cargarme las comas del CSV con la expresión regular :g/("\d+),(\d+")/s//\1.\2/g (sí, me daba pereza liarme con el locale y jugar con los puntos y las comas, le dí a la BD lo que le molaba y listo)
  • Luego me dí cuenta de que había algunos valores negativos que no había visto y repetí la regexp con un - antes del primer \d+
  • Algunos datos de lluvia que son casi todos numéricos (milímetros) venían como la string “Ip” que al parecer significa “inapreciable” y los cambié por 0
  • Algunos datos de direcciones del viento y horas de máximos venían como “Varias” y los cambié por 0, no porque tuviera sentido sino porque no pensaba usar esas columnas y me estaba cansando xD
  • Otros campos de hora supuestamente de 0 a 23 venían con el valor 99 vete a saber por qué, y los pasé a 0 por la misma razón que las anteriores

Al final me quedé con la siguiente estructura, la fecha y luego todo numéricos (int o float). Como todavía no sabía qué probaría exactamente, dejé todos los campos que no me daban problemas aunque al final solo hice predicciones sobre tmax:

CREATE TABLE datosClimatologicos 
(
    fecha       date,
    tmed        float,
    prec        float,
    tmin        float,
    tmax        float,
    dir         int,
    velmedia    float,
    racha       float,
    sol         float,
    presMax     float,
    horaPresMax int,
    presMin     float,
    horaPresMin int
);

La carga del CSV a la tabla la hice con un job de Dbeaver https://dbeaver.io/ que es un cliente SQL “universal” que está bastante bien.

El software

Usaremos MindsDB, que podéis encontrar aquí https://mindsdb.com/. Es un software bastante ingenioso que tampoco tiene ningún problema, como los datos. La cago yo más adelante cuando lo mezclo todo 😀

Resumiendo mucho es una capa de pseudo-SQL que pones entre tu aplicación (o cliente tipo DBeaver si haces pruebas a pelo como es el caso) y la base de datos relacional convencional, que en esta ocasión es MySQL. MindsDB se puede conectar a todo tipo de bases de datos e incluso combinar datos de varias fuentes en una sola query. Pero además te permite generar, usando un SQL ligeramente ampliado, modelos de predicción que luego puedes consultar como si fueran tablas. Si te da pereza programar o tienes prisa es genial, pero por supuesto toda la complejidad que esconde sigue ahí. En particular se encarga de entrenar diferentes modelos y elegir el que acierta más. Un data scientist de carne y hueso suele hacer eso mejor.

Para hacer la prueba rápida usé las imágenes de Docker estándar tanto de MySQL como de MindsDB:

Arranqué el MySQL con persistencia en un directorio local y publicando su puerto 3306. Arranqué el MindsDB publicando sus puertos 47334 y 47335 (el GUI HTTP y el puerto por el que simula el protocolo de MySQL).

Me conecté a ambos con Dbeaver.

Lo metemos todo en la batidora y la ponemos al máximo

Ahora viene lo divertido, las oportunidades de equivocarse o hacer cosas sin sentido son infinitas 😀

Primero creé la base de datos en MindsDB, lo cual realmente establece una conexión de MindsDB a MySQL:

CREATE DATABASE aemet_opendata
WITH ENGINE = "mysql",
PARAMETERS = { 
  "user": "root",
  "password": "cosa_super_secreta",
  "host": "IP_local_de_la_maquina",
  "port": "3306",
  "database": "aemet-opendata"
}

Mis pobres decisiones en cuanto a guiones medios y bajos hacen que tenga que usar comillas chungas más adelante. Pero yo qué sé, es una prueba 😛

Con esto contra MindsDB se puede comprobar que ve los datos que tenemos en MySQL:

select * from aemet_opendata.`aemet-opendata`.datosClimatologicos;

Pues nada, creamos un predictor. Por supuesto esto lo ejecuté veinte veces probando parámetros diferentes. Para crearlo no puede existir, así que después de la primera vez, acordaos de quitarlo antes de crearlo. Se hace con un drop predictor ... como si fuera una tabla.

create predictor mindsdb.datosClimatologicos
from aemet_opendata (
  select fecha, tmax
  from `aemet-opendata`.datosClimatologicos
)
predict tmax
order by fecha
window 7
horizon 7;

Esto se tira un rato entrenando modelos, para ver el estado se puede hacer:

SELECT * FROM mindsdb.predictors WHERE name='datosClimatologicos';

Que devolverá algo así, indicando el estado creating, training o complete (que yo recuerde, esos son los que yo he visto):

Como podéis ver no he usado casi parámetros, con lo básico ya la podemos liar bastante. Lo de create predictor y un nombre, pues es como un create table de toda la vida. Solo que esta tabla será “mágica” y los datos serán las predicciones del modelo. El from le indica los datos que tiene que usar para entrenar, que es una query a la BD MySQL. Aquí si que ya solo pillo los datos que me interesan: la fecha y la temperatura máxima.

Con predict le indico que quiero predicciones de tmax, y con el order by en realidad le estoy diciendo que le doy una serie temporal por el campo fecha. Son convenciones del “SQL especial” de MindsDB. Finalmente con window le digo que para predecir un valor nos fijamos en los 7 anteriores y con horizon le digo que quiero predecir los próximos 7.

He jugado con esos parámetros? Si, bastante. No con los detalles más internos de los modelos y el entrenamiento, pero sí con window y horizon, sobre todo con window. Si os fijáis, mi modelo tiene una accuracy de casi un 79%. Esto imagino que automáticamente entrenado con parte de los datos que tiene, y automáticamente validado con otra parte. Pinta bien, en principio.

Si amplías la ventana, por ejemplo a 730 días (los últimos 2 años) para predecir la tmax de 1 día, lo que pasa es que el entrenamiento tarda mucho más. Pero la accuracy baja al 42% o algo así. Por qué? Porque la temperatura máxima de un día en un lugar no depende principalmente de la temperatura máxima de los dos últimos años en ese lugar, por eso. En este caso tener muchos datos históricos de la serie temporal que queremos predecir no nos sirve de mucho.

Si fuera así de fácil todo el mundo lo haría para predecir el tiempo, pero NO se hace así, se tienen en cuenta muchas otras cosas, incluyendo (imagino) enormes campos vectoriales de temperatura, viento y presión en todo el mundo. Es decir, muchísimas otras variables que influyen en la temperatura máxima, que no son las temperaturas máximas del pasado. Y esto también es una pista de por qué mi modelo es una caca, incluso con la accuracy del 79%.

Bueno, pues nada, vamos a hacer una predicción. Tenemos datos hasta el 11 de junio y hoy, momento de escribir esto, es 18 de junio, así que pillamos la ventana de 7 días por los pelos. Vamos a ver qué pasa. Le digo…

SELECT m.fecha, m.tmax as predicted_tmax
FROM mindsdb.datosClimatologicos as m 
JOIN aemet_opendata.`aemet-opendata`.datosClimatologicos as t
WHERE t.fecha > LATEST
LIMIT 7;

Y me dice…

En resumen, un éxito. Pienso yo con el aire acondicionado a tope intentando escapar de los 34 grados a la sombra que realmente hace hoy.

Bueno, por hablar un poco de la query, es bastante normal. Se consulta el modelo casi como si fuera una tabla corriente. Lo único “raro” es el > LATEST que, una vez más, es una convención del SQL especialito de MindsDB para trabajar con series temporales.

Qué ha estropeado tanto esta predicción? Entre otras cosas al usar la ventana de 7 días anteriores y no tenerlos, le estoy poniendo las cosas difíciles al modelo. Es decir, tengo datos hasta dia 11, lo he entrenado para mirar los últimos 7 días y le pido día 18. Y ante una pregunta de mierda, te expones a recibir una respuesta de mierda 😀

En fin, espero que os haya resultado interesante y que a lo mejor os haya abierto los ojos para usar MindsDB algún día, pero bien O:)

Ah, y… First post! Es mi blog personal, puedo meter aquí lo que quiera. El próximo post podría ser una receta de cocina. O un rant super largo quejándome de lo mucho que pitan los coches por la zona del Corte Inglés. O yo qué sé.