{ "cells": [ { "metadata": {}, "cell_type": "markdown", "source": [ "\n", " \"Open\n", "" ], "id": "996e097cf14e03d7" }, { "metadata": {}, "cell_type": "markdown", "source": [ "# Exercise: RNN to predict SST\n", "\n", "Download a monthly sea surface temperature (SST) time series for the Mediterranean Sea from [surftemp.net](https://surftemp.net/timeseries/index.html) in CSV format. Choose a geographic area of your choice.\n", "\n", "Once the data is loaded:\n", "\n", "- Visualise the series and identify the trend and seasonality.\n", "- Prepare the data using a sliding window and sequential train/test split.\n", "- Train an RNN with PyTorch to predict the SST of the following month.\n", "- Evaluate the model with MAE, RMSE, and MAPE and visualise the predictions.\n", "\n", "\n", "[Download Sample data](https://github.com/bmalcover/AppOC/blob/main/docs/_static/03/SST_ABSO_002.00E_004.00E_38.00N_40.00N_19800101_20251231_timeseries.csv)" ], "id": "55067d73a3de9a1d" }, { "cell_type": "code", "id": "initial_id", "metadata": { "collapsed": true }, "source": [ "import torch\n", "import torch.nn as nn\n", "import pandas as pd\n", "import numpy as np\n", "\n", "\n", "from sklearn.preprocessing import MinMaxScaler\n", "from sklearn.metrics import mean_absolute_error\n", "\n", "import matplotlib.pyplot as plt\n" ], "outputs": [], "execution_count": null }, { "metadata": {}, "cell_type": "markdown", "source": "En primer lloc carregarem les dades. Com sempre que llegim un CSV: `read_csv`", "id": "aca459925a55a40e" }, { "metadata": {}, "cell_type": "code", "source": [ "df = pd.read_csv(\"PATH/TO/FILE\") # Segurament necessitem un poc d'ajuda del paràmetre header\n", "print(df.columns)\n", "print(df.shape)" ], "id": "ee9fee1ca517eb07", "outputs": [], "execution_count": null }, { "metadata": {}, "cell_type": "markdown", "source": [ "A continuació agafarem les característiques que ens interessen per inferir la temperatura. Descartarem la característica `mean temperature kelvin` (seria fer trampes) i `fraction of sea-ice-covered ocean` ja que el conjunt de dades és afagat del mar mediterrani.\n", "\n", "La variable objectiu serà `mean temperature deg C`." ], "id": "2a14c644470e0c83" }, { "metadata": {}, "cell_type": "code", "source": [ "features = #TODO seleccionar features\n", "target = #TODO selecionar variable objectiu" ], "id": "bb7cded38dbfbd6d", "outputs": [], "execution_count": null }, { "metadata": {}, "cell_type": "markdown", "source": [ "Escalarem les variables al rang [0,1], ja que això ajuda a la xarxa.\n", "\n", "Tindrem dos Scalers independents, un per les característiques i un per la variable objectiu, això ens permetrà simplificar el procés de donar resultat (truco de programador vell)." ], "id": "d67522afdb5773cc" }, { "metadata": {}, "cell_type": "code", "source": [ "#Scalers independents\n", "scaler_features = MinMaxScaler()\n", "scaler_target = MinMaxScaler()\n", "\n", "features_scaled = # TODO\n", "target_scaled = # TODO" ], "id": "2154d9074dc3811f", "outputs": [], "execution_count": null }, { "metadata": {}, "cell_type": "markdown", "source": [ "Ara ja podem dividir les dades entre entrenament i test. Hem d'aconseguir 4 variables:\n", "\n", "- `features_train` i `features_test`\n", "- `target_train` i `features_train`\n", "\n", "\n", "**NOTA**: Estem emprant sèries temporals, això implica que la partició ha de seguir el principi de seqüèncialitat:" ], "id": "4089c7a8cc6ef1f1" }, { "metadata": {}, "cell_type": "code", "source": [ "n_train = int(df.shape[0]*0.8) #nombre de mostres al conjunt d'entrenament.\n", "\n", "features_train = #TODO\n", "features_test = #TODO\n", "\n", "target_train = #TODO\n", "target_test = #TODO\n", "\n", "# Comprovem que el que hem fet és correcte\n", "print(n_train)\n", "print(features_train.shape, target_train.shape)\n", "print(features_test.shape, target_test.shape)\n" ], "id": "4ccd1bb615d4cff0", "outputs": [], "execution_count": null }, { "metadata": {}, "cell_type": "markdown", "source": [ "El següent codi és una mica més complex; Ens permet generar petites seqüències de mida `WINDOW_SIZE` que permetran entrenar la nostra RNN. Si comparam la funció amb la de la part teòrica ha canviat una mica ja que ara la variable objectiu és diferent de les característiques que empram per entrenar.\n", "\n", "L'objectiu és que entenguem el que fa el codi:" ], "id": "418bfdf37ee1e34b" }, { "metadata": {}, "cell_type": "code", "source": [ "def create_sequences(features, target, window_size):\n", " X, y = [], []\n", " for i in range(window_size, len(features)):\n", " X.append(features[i-window_size:i])\n", " y.append(target[i])\n", " return np.array(X), np.array(y)\n", "\n", "WINDOW_SIZE = 12\n", "\n", "X_train, y_train = create_sequences(features_train, target_train, WINDOW_SIZE)\n", "\n", "X_test, y_test = create_sequences(np.concatenate([features_train[-WINDOW_SIZE:], features_test]),\n", " np.concatenate([target_train[-WINDOW_SIZE:], target_test]),\n", " WINDOW_SIZE)\n", "\n", "print(X_train.shape)\n", "print(y_train.shape)" ], "id": "3329e38872ea3734", "outputs": [], "execution_count": null }, { "metadata": {}, "cell_type": "markdown", "source": "La darrera passa és transformar-ho en un `tensor`, ja que és l'estructura de dades que empra `Pytorch`", "id": "190848bc6f2865bf" }, { "metadata": {}, "cell_type": "code", "source": "", "id": "d4fe0ae27f209592", "outputs": [], "execution_count": null }, { "metadata": {}, "cell_type": "markdown", "source": [ "**Creació del model**\n", "\n", "Ara toca crear el model, en problemes \"senzills\" no cal complicar-se massa en pensar l'arquitectura, podem reutilitzar la mateixa del bloc de teoria:" ], "id": "49d8f38cac8172f2" }, { "metadata": {}, "cell_type": "code", "source": [ "class RNNModel(nn.Module):\n", " def __init__(self, input_size=¿?, hidden_size=32, num_layers=1):\n", " super(RNNModel, self).__init__()\n", " self.rnn = nn.RNN(\n", " input_size=input_size,\n", " hidden_size=hidden_size,\n", " num_layers=num_layers,\n", " batch_first=True\n", " )\n", " self.fc = nn.Linear(hidden_size, 1)\n", "\n", " def forward(self, x):\n", " out, _ = self.rnn(x)\n", " out = self.fc(out[:, -1, :]) # agafem l'últim pas temporal i el processam per una capa tipus MLP\n", " return out.squeeze()\n" ], "id": "ade906cb736763fc", "outputs": [], "execution_count": null }, { "metadata": {}, "cell_type": "markdown", "source": [ "El bloc d'entrenament també és bastant genèric. Hi ha una millora que es pot fer que consisteix a afegir una passa d'avaluació al final de cada iteració, així mentre s'entrena es pot tenir una estimació de com de bé la xarxa treballa amb dades noves.\n", "\n", "**EXTRA**: Afegir una avaluació després de cada cicle d'entrenament." ], "id": "c27e79b4415bd2e0" }, { "metadata": {}, "cell_type": "code", "source": [ "model = RNNModel() # És molt important crear el model en la mateixa cel·la que entrenem. En saps el motiu?\n", "\n", "criterion = nn.MSELoss() # En aquest cas la funció de pèrdua és de regressió\n", "optimizer = torch.optim.Adam(model.parameters(), lr=¿?)\n", "\n", "epochs = ¿?\n", "for epoch in range(epochs):\n", " model.train()\n", " y_pred = model(X_train)\n", " loss = criterion(y_pred, y_train)\n", "\n", " optimizer.zero_grad()\n", " loss.backward()\n", " optimizer.step()\n", "\n", " if (epoch + 1) % 50 == 0:\n", " print(f\"Epoch {epoch+1}/{epochs} - Loss: {loss.item():.6f}\")" ], "id": "c9d32511df5796e4", "outputs": [], "execution_count": null }, { "metadata": {}, "cell_type": "markdown", "source": [ "Finallment ens queda avaluar els resultats. Això implica realitzar les següents passes_:\n", "\n", "- Fer una predicció del conjunt de test.\n", "- Desfer la passa de normalització tant del conjunt de test com de la predicció de la xarxa emprant el mètode `inverse_transform` de la classe `MinMaxScaler`.\n", "- Calcular les mètriques `MAE`, `RMSE` i `MAPE`." ], "id": "251e739126c81047" }, { "metadata": {}, "cell_type": "code", "source": [ "# Avaluació del conjunt de test\n", "\n", "\n" ], "id": "2fc1f1841b1f6bbf", "outputs": [], "execution_count": null }, { "metadata": {}, "cell_type": "code", "outputs": [], "execution_count": null, "source": [ "# Desnormalització\n", "\n" ], "id": "bda11e08c62bdf06" }, { "metadata": {}, "cell_type": "code", "outputs": [], "execution_count": null, "source": [ "# Càlcul de mètriques\n", "\n" ], "id": "622cecf76af74405" }, { "metadata": {}, "cell_type": "markdown", "source": "A més de mostrar les mètriques, sempre està bé fer un gràfic que mostra com s'ajusten les prediccions al conjunt de test:", "id": "25756051937c17a3" }, { "metadata": { "ExecuteTime": { "end_time": "2026-04-16T11:09:23.025957600Z", "start_time": "2026-04-16T11:09:22.956250800Z" } }, "cell_type": "code", "source": [ "# Construcció de l'eix temporal a partir de les columnes year, month, day\n", "dates = pd.to_datetime(df[['year', 'month', 'day']])\n", "\n", "plt.figure(figsize=(12, 4))\n", "plt.plot(dates[n_train:], ¿?, label='Actual SST', color='steelblue')\n", "plt.plot(dates[n_train:], ¿?, label='Predicted SST', color='tomato', linestyle='--')\n", "plt.xlabel('Data')\n", "plt.ylabel('Temperatura (°C)')\n", "plt.title('Prediccions vs Valors Reals')\n", "plt.legend()\n", "plt.tight_layout()\n", "plt.show()" ], "id": "15924658969e311b", "outputs": [], "execution_count": null } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 2 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython2", "version": "2.7.6" } }, "nbformat": 4, "nbformat_minor": 5 }