{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":56537,"databundleVersionId":8877088,"sourceType":"competition"}],"dockerImageVersionId":31192,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\n\n# --- 0. Configuration ---\n# Chemin vers les fichiers de données (DOIT ÊTRE LE MÊME QUE PRÉCÉDEMMENT)\nROOT = '/kaggle/input/leap-atmospheric-physics-ai-climsim' \n\nSAMPLE_SUBMISSION_FILE = f'{ROOT}/sample_submission.csv' \n\nprint(f\"Chargement du masque de pondération depuis : {SAMPLE_SUBMISSION_FILE}\")\n\ntry:\n    # Charger UNIQUEMENT la première ligne (le masque W)\n    df_weights = pd.read_csv(SAMPLE_SUBMISSION_FILE, nrows=1)\n    \n    # Isoler les colonnes cibles (sans 'sample_id')\n    target_cols = [col for col in df_weights.columns if col != 'sample_id']\n    W_vector = df_weights[target_cols].iloc[0].values\n    \n    # --- 1. Analyse et Noms des Colonnes Masquées (W=0) ---\n    \n    nombre_total = len(W_vector)\n    \n    # Obtenir les noms des colonnes avec W = 0\n    cols_a_masquer = [target_cols[i] for i, w in enumerate(W_vector) if w == 0]\n    nombre_zeros = len(cols_a_masquer)\n    nombre_evaluees = nombre_total - nombre_zeros\n    \n    print(\"-\" * 50)\n    print(f\"Dimensions du Masque W : {W_vector.shape}\")\n    print(f\"Nombre total de cibles : {nombre_total}\")\n    print(f\"Cibles évaluées (W>0) : {nombre_evaluees}\")\n    print(f\"Cibles masquées (W=0) : {nombre_zeros}\")\n    \n    # --- 2. Affichage des Noms Spécifiques ---\n\n    if nombre_zeros > 0:\n        print(f\"\\n✅ Les colonnes suivantes NE SONT PAS évaluées par le score final (W=0) :\")\n        \n        # Afficher le décompte et les noms des colonnes 'ptend_' masquées\n        ptend_masquees = [col for col in cols_a_masquer if col.startswith('ptend_')]\n        if ptend_masquees:\n            print(f\"   - **ptend_** ({len(ptend_masquees)} colonnes) :\")\n            print(f\"     {ptend_masquees}\")\n\n        # Afficher le décompte et les noms des colonnes 'cam_out_' masquées\n        cam_masquees = [col for col in cols_a_masquer if col.startswith('cam_out_')]\n        if cam_masquees:\n            print(f\"   - **cam_out_** ({len(cam_masquees)} colonnes) :\")\n            # Pour la lisibilité, n'affichons que les premières et dernières colonnes si la liste est longue\n            if len(cam_masquees) > 10:\n                print(f\"     {cam_masquees[:5]} ... {cam_masquees[-5:]}\")\n            else:\n                print(f\"     {cam_masquees}\")\n                \n        print(\"\\n=> Nous pouvons potentiellement supprimer ces colonnes de Y et réduire la taille d'OUTPUT_UNITS.\")\n        \n    else:\n        print(\"Conclusion : Tous les poids sont 1, ou il y a une erreur de lecture. Le modèle doit prédire les 368 colonnes.\")\n        \nexcept FileNotFoundError:\n    print(f\"❌ Erreur : Le fichier {SAMPLE_SUBMISSION_FILE} n'a pas été trouvé. Vérifiez le chemin ROOT.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:42:42.153104Z","iopub.execute_input":"2025-12-03T12:42:42.153445Z","iopub.status.idle":"2025-12-03T12:42:44.701089Z","shell.execute_reply.started":"2025-12-03T12:42:42.153411Z","shell.execute_reply":"2025-12-03T12:42:44.699969Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\n# --- Configuration et Chargement des Données ---\n\n# Chemin racine fourni (inchangé)\nROOT = '/kaggle/input/leap-atmospheric-physics-ai-climsim'\nTRAIN_FILE = f'{ROOT}/train.csv'\n\n# Charger un petit échantillon pour l'inspection (1000 premières lignes)\nN_ROWS = 1000\ntry:\n    df_train = pd.read_csv(TRAIN_FILE, nrows=N_ROWS)\nexcept FileNotFoundError:\n    print(f\"Erreur : Le fichier {TRAIN_FILE} n'a pas été trouvé.\")\n    exit()\n\n# Correction des noms de variables réels (basés sur le diagnostic)\nINPUT_COL_BASE_CORRECT = 'state_t' # Entrée: Température (OK)\nTARGET_COL_BASE_CORRECT = 'ptend_t' # Cible: Tendance au chauffage (CORRIGÉ, était 'ptend_the')\n\nprint(\"### 1. 📏 Inspection de Base et de la Structure des Colonnes (VERSION FINALE) ###\")\nprint(f\"Forme du DataFrame échantillonné: {df_train.shape}\")\nprint(f\"Nombre total de colonnes: {df_train.shape[1]}\")\nprint(\"-\" * 50)\n\n# --- 2. Validation de la Dimension Verticale (60 Niveaux) ---\n\n# Identification des colonnes de profil avec les noms CORRIGÉS\ninput_cols_60 = [col for col in df_train.columns if col.startswith(f'{INPUT_COL_BASE_CORRECT}_')]\ntarget_cols_60 = [col for col in df_train.columns if col.startswith(f'{TARGET_COL_BASE_CORRECT}_')]\n\nprint(f\"Variable d'entrée (Température) '{INPUT_COL_BASE_CORRECT}' : {len(input_cols_60)} colonnes.\")\nprint(f\"Variable cible (Chauffage) '{TARGET_COL_BASE_CORRECT}' : {len(target_cols_60)} colonnes.\")\n\nif len(input_cols_60) == 60 and len(target_cols_60) == 60:\n    print(\"\\n✅ SUCCÈS : 60 colonnes trouvées pour l'entrée ET la cible. La nature du PROFIL VERTICAL est confirmée.\")\n    print(f\"Indices des 5 premières colonnes (Entrée): {input_cols_60[:5]}\")\n    print(f\"Indices des 5 dernières colonnes (Cible): {target_cols_60[-5:]}\")\nelse:\n    print(\"\\n❌ ATTENTION : Échec persistant. Il faut vérifier manuellement le nom de la variable cible de chauffage.\")\n    # Afficher les 20 dernières colonnes pour une inspection manuelle si le problème persistait.\n    print(f\"20 dernières colonnes: {df_train.columns[-20:].tolist()}\")\n    exit()\n    \nprint(\"-\" * 50)\n\n# --- 3. Visualisation des Profils (Preuve Graphique de la Verticalité) ---\n\n# Sélectionner le premier exemple de la ligne 0\nexample_row = df_train.iloc[0]\n\n# Extraction des profils avec les noms de colonnes CORRIGÉS\ntemp_profile = example_row[[f'{INPUT_COL_BASE_CORRECT}_{i}' for i in range(60)]].values\nheat_tendency_profile = example_row[[f'{TARGET_COL_BASE_CORRECT}_{i}' for i in range(60)]].values\n\n# Définir les niveaux verticaux.\nlevels = np.arange(60)\naltitude_proxy = 59 - levels # Inverser les indices pour que le niveau 59 (près du sol) soit en bas du graphique.\n\nprint(\"### 3. Visualisation du Profil Vertical pour un Point de Grille ###\")\n\nplt.figure(figsize=(12, 6))\n\n# --- Graphique 1 : Profil de Température (Entrée) ---\nplt.subplot(1, 2, 1)\nplt.plot(temp_profile, altitude_proxy, marker='o', markersize=3, linestyle='-', color='blue')\nplt.title(f'Profil Vertical de Température ({INPUT_COL_BASE_CORRECT})')\nplt.xlabel('Température (K)')\nplt.ylabel('Niveau Vertical (Proxy Altitude)')\nplt.yticks(np.arange(0, 60, 10), labels=np.arange(59, -1, -10))\nplt.grid(True, linestyle='--', alpha=0.6)\nplt.gca().invert_yaxis() \n\n# --- Graphique 2 : Profil de Tendance au Chauffage (Cible) ---\nplt.subplot(1, 2, 2)\nplt.plot(heat_tendency_profile, altitude_proxy, marker='x', markersize=3, linestyle='-', color='red')\nplt.title(f'Profil Vertical de Tendance au Chauffage ({TARGET_COL_BASE_CORRECT})')\nplt.xlabel('Tendance au Chauffage (K/s)')\nplt.ylabel('Niveau Vertical (Proxy Altitude)')\nplt.yticks(np.arange(0, 60, 10), labels=np.arange(59, -1, -10))\nplt.grid(True, linestyle='--', alpha=0.6)\nplt.gca().invert_yaxis() \n\nplt.tight_layout()\nplt.show()\n\nprint(\"\\n=> La vérification est réussie et le graphique est affiché. Le problème est bien une régression de profil à profil (60D).\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\n\n# --- Config ---\nROOT = '/kaggle/input/leap-atmospheric-physics-ai-climsim'\nTRAIN_FILE = f'{ROOT}/train.csv'\nN_ROWS = 50000\nLEVELS = 60\nINPUT_COL_BASE = 'state_t'\nTARGET_COL_BASE = 'ptend_t'\n\n# Construire la liste des colonnes attendues\ntemp_cols = [f'{INPUT_COL_BASE}_{i}' for i in range(LEVELS)]\nptend_cols = [f'{TARGET_COL_BASE}_{i}' for i in range(LEVELS)]\nusecols = temp_cols + ptend_cols  # charger uniquement ces colonnes\n\n# Charger en gérant l'absence éventuelle de colonnes\ntry:\n    df_train = pd.read_csv(TRAIN_FILE, usecols=usecols, nrows=N_ROWS)\n    print(f\"Échantillon chargé: {df_train.shape[0]} lignes, {df_train.shape[1]} colonnes.\")\nexcept ValueError as e:\n    # pd.read_csv lève ValueError si certaines colonnes dans usecols sont manquantes\n    present_cols = pd.read_csv(TRAIN_FILE, nrows=0).columns.tolist()\n    missing = [c for c in usecols if c not in present_cols]\n    raise RuntimeError(f\"Colonnes manquantes dans le fichier: {missing}\") from e\n\n# 1) Statistiques par niveau\ntemp_stats = df_train[temp_cols].agg(['mean', 'std']).T\ntemp_stats.index = [f'Niveau_{i}' for i in range(LEVELS)]\ntemp_stats.columns = ['Température Moyenne (K)', 'Écart-Type Température (K)']\nprint(pd.concat([temp_stats.head(), temp_stats.tail()]).to_markdown(floatfmt=\".2f\"))\n\n# 2) Corrélations niveau-à-niveau (plus robuste et plus rapide)\ncorrs = []\nfor t_col, p_col in zip(temp_cols, ptend_cols):\n    x = df_train[t_col]\n    y = df_train[p_col]\n    # Si std == 0 => corr = NaN ; on gère explicitement\n    if x.std() == 0 or y.std() == 0:\n        corrs.append(np.nan)\n    else:\n        corrs.append(x.corr(y))  # équivalent à pearsonr sans import\ndf_corr = pd.Series(corrs, index=[f'Niveau_{i}' for i in range(LEVELS)], name='Corrélation')\n\n# 3) Plot\nplt.figure(figsize=(14,10))\naltitude_proxy = 59 - np.arange(LEVELS)\n\nplt.subplot(1,2,1)\nplt.plot(temp_stats['Température Moyenne (K)'], altitude_proxy, marker='o', markersize=3, label='Moyenne (K)')\nplt.plot(temp_stats['Écart-Type Température (K)'], altitude_proxy, marker='x', markersize=3, linestyle='--', label='Écart-Type (K)')\nplt.gca().invert_yaxis()\nplt.title('Profil Vertical: Température Moyenne et Variabilité')\nplt.xlabel('Valeur')\nplt.ylabel('Niveau Vertical (Proxy Altitude)')\nplt.legend()\nplt.grid(True, linestyle='--', alpha=0.6)\nplt.yticks(np.arange(0,60,10), labels=np.arange(59,-1,-10))\n\nplt.subplot(1,2,2)\nplt.plot(df_corr.values, altitude_proxy, marker='s', markersize=3)\nplt.gca().invert_yaxis()\nplt.title('Corrélation Pearson (state_t vs ptend_t) par Niveau')\nplt.xlabel('Coefficient de corrélation')\nplt.ylabel('Niveau Vertical (Proxy Altitude)')\nplt.axvline(0, color='red', linewidth=0.8)\nplt.grid(True, linestyle='--', alpha=0.6)\nplt.yticks(np.arange(0,60,10), labels=np.arange(59,-1,-10))\n\nplt.tight_layout()\nplt.show()\n\n# 4) Diagnostic rapide des corrélations\nprint(\"\\nNombre de niveaux où la corrélation est NaN (std=0):\", np.sum(np.isnan(df_corr)))\nprint(\"Corrélation moyenne (excl. NaN):\", np.nanmean(df_corr))\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\n# IMPORTANT : Assurez-vous que le chemin ROOT est défini comme dans les autres cellules\nROOT = '/kaggle/input/leap-atmospheric-physics-ai-climsim' \nTRAIN_FILE = f'{ROOT}/train.csv'\n\nprint(f\"Chargement du fichier d'entraînement depuis : {TRAIN_FILE}\")\n\n# Chargement d'un sous-ensemble pour des raisons de mémoire et de temps\n# Si vous avez assez de mémoire, vous pouvez retirer 'nrows'\ntry:\n    df_train = pd.read_csv(TRAIN_FILE, nrows=100000) \n    print(f\"✅ df_train chargé. Dimensions : {df_train.shape}\")\nexcept FileNotFoundError:\n    print(f\"❌ Erreur : Le fichier {TRAIN_FILE} n'a pas été trouvé. Veuillez vérifier le chemin ROOT.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T12:57:08.154972Z","iopub.execute_input":"2025-12-03T12:57:08.155402Z","iopub.status.idle":"2025-12-03T12:58:06.219252Z","shell.execute_reply.started":"2025-12-03T12:57:08.155371Z","shell.execute_reply":"2025-12-03T12:58:06.218163Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import StandardScaler\nimport tensorflow as tf\nfrom tensorflow.keras import backend as K\n\n# --- 0. Configuration et Constantes ---\nROOT = '/kaggle/input/leap-atmospheric-physics-ai-climsim' \nSAMPLE_SUBMISSION_FILE = f'{ROOT}/sample_submission.csv' \nTRAIN_FILE = f'{ROOT}/train.csv'\nLEVELS = 60\nid_col = 'sample_id'\n\n# Liste des 60 colonnes identifiées comme ayant un Poids W = 0 (NON ÉVALUÉES)\nCOLS_TO_DROP = [\n    'ptend_q0001_0', 'ptend_q0001_1', 'ptend_q0001_2', 'ptend_q0001_3', 'ptend_q0001_4', 'ptend_q0001_5', \n    'ptend_q0001_6', 'ptend_q0001_7', 'ptend_q0001_8', 'ptend_q0001_9', 'ptend_q0001_10', 'ptend_q0001_11', \n    'ptend_q0002_0', 'ptend_q0002_1', 'ptend_q0002_2', 'ptend_q0002_3', 'ptend_q0002_4', 'ptend_q0002_5', \n    'ptend_q0002_6', 'ptend_q0002_7', 'ptend_q0002_8', 'ptend_q0002_9', 'ptend_q0002_10', 'ptend_q0002_11', \n    'ptend_q0003_0', 'ptend_q0003_1', 'ptend_q0003_2', 'ptend_q0003_3', 'ptend_q0003_4', 'ptend_q0003_5', \n    'ptend_q0003_6', 'ptend_q0003_7', 'ptend_q0003_8', 'ptend_q0003_9', 'ptend_q0003_10', 'ptend_q0003_11', \n    'ptend_u_0', 'ptend_u_1', 'ptend_u_2', 'ptend_u_3', 'ptend_u_4', 'ptend_u_5', \n    'ptend_u_6', 'ptend_u_7', 'ptend_u_8', 'ptend_u_9', 'ptend_u_10', 'ptend_u_11', \n    'ptend_v_0', 'ptend_v_1', 'ptend_v_2', 'ptend_v_3', 'ptend_v_4', 'ptend_v_5', \n    'ptend_v_6', 'ptend_v_7', 'ptend_v_8', 'ptend_v_9', 'ptend_v_10', 'ptend_v_11'\n]\n\n# --- CHARGEMENT DU FICHIER D'ENTRAINEMENT (Ajustez nrows si nécessaire) ---\nN_ROWS_TO_LOAD = 50000 \ntry:\n    df_train = pd.read_csv(TRAIN_FILE, nrows=N_ROWS_TO_LOAD)\n    print(f\"Chargement de {df_train.shape[0]} lignes d'entraînement.\")\nexcept FileNotFoundError:\n    print(f\"❌ Erreur: Fichier d'entraînement non trouvé à {TRAIN_FILE}\")\n    exit()\n    \n# --- 1. Séparation des Entrées (X) et Cibles (Y) ---\n\nall_cols = df_train.columns.tolist()\ntarget_cols_original = [col for col in all_cols if col.startswith('ptend_') or col.startswith('cam_out_')]\n# Y filtré : la liste des 308 cibles à prédire\ntarget_cols_filtered = [col for col in target_cols_original if col not in COLS_TO_DROP]\ninput_cols = [col for col in all_cols if col not in target_cols_original and col != id_col]\n\nX = df_train[input_cols].values\nY = df_train[target_cols_filtered].values\n\n# Définition des variables globales (Indispensables pour les cellules suivantes)\nglobals()['OUTPUT_UNITS'] = Y.shape[1] \nglobals()['input_cols'] = input_cols\nglobals()['target_cols_filtered'] = target_cols_filtered\nglobals()['COLS_TO_DROP'] = COLS_TO_DROP\nglobals()['LEVELS'] = LEVELS\n\nprint(\"### 1. Séparation et Filtrage des Cibles (Y) ###\")\nprint(f\"X (Entrées) dimensions brutes : {X.shape} ({X.shape[1]} caractéristiques)\")\nprint(f\"Y (Cibles FILTRÉES) dimensions brutes : {Y.shape} ({globals()['OUTPUT_UNITS']} cibles)\")\nprint(\"-\" * 50)\n\n\n# --- 2. Standardisation des Données ---\nX_train, X_val, Y_train, Y_val = train_test_split(X, Y, test_size=0.1, random_state=42)\n\nscaler_X = StandardScaler()\nX_train_scaled = scaler_X.fit_transform(X_train)\nX_val_scaled = scaler_X.transform(X_val)\n\nscaler_Y = StandardScaler()\nY_train_scaled = scaler_Y.fit_transform(Y_train)\nY_val_scaled = scaler_Y.transform(Y_val)\n\nglobals()['scaler_Y'] = scaler_Y \nglobals()['scaler_X'] = scaler_X\n\n\n# --- 3. Mise en Forme 3D pour le CNN 1D ---\nN_FEATURES_60D = 9 \nN_SCALAR_FEATURES = X.shape[1] - (N_FEATURES_60D * LEVELS) \n\nglobals()['N_FEATURES_60D'] = N_FEATURES_60D\nglobals()['N_SCALAR_FEATURES'] = N_SCALAR_FEATURES\n\n# Séparation des profils (CNN)\nX_train_profile = X_train_scaled[:, :N_FEATURES_60D * LEVELS]\nX_val_profile = X_val_scaled[:, :N_FEATURES_60D * LEVELS]\nX_train_CNN = X_train_profile.reshape(-1, LEVELS, N_FEATURES_60D)\nX_val_CNN = X_val_profile.reshape(-1, LEVELS, N_FEATURES_60D)\n\n# Séparation des scalaires (MLP)\nX_train_SCALAR = X_train_scaled[:, N_FEATURES_60D * LEVELS:]\nX_val_SCALAR = X_val_scaled[:, N_FEATURES_60D * LEVELS:]\n\nglobals()['X_train_CNN'] = X_train_CNN\nglobals()['X_val_CNN'] = X_val_CNN\nglobals()['X_train_SCALAR'] = X_train_SCALAR\nglobals()['X_val_SCALAR'] = X_val_SCALAR\nglobals()['Y_train_scaled'] = Y_train_scaled\nglobals()['Y_val_scaled'] = Y_val_scaled\n\nprint(\"### 3. Reforme pour le CNN 1D Multi-Input ###\")\nprint(f\"X_train reformé pour CNN (Profils) : {X_train_CNN.shape}\")\nprint(\"-\" * 50)\n\n\n# --- 4. Création du Masque W Filtré pour la Perte (CORRIGÉ) ---\n\ndf_weights_original = pd.read_csv(SAMPLE_SUBMISSION_FILE, nrows=1)\ntarget_cols_all = [col for col in df_weights_original.columns if col != id_col]\n\n# Création d'un mapping Colonne -> Poids W pour un alignement GARANTI\nweights_map = df_weights_original[target_cols_all].iloc[0].to_dict()\n\n# Créer le vecteur W filtré dans l'ordre EXACT de Y (target_cols_filtered)\nW_vector_filtered = np.array([weights_map[col] for col in target_cols_filtered], dtype=np.float32)\n\nW_tensor_filtered = tf.constant(W_vector_filtered, dtype=tf.float32)\n\nglobals()['W_tensor'] = W_tensor_filtered\n\n\n# --- 5. Vérifications Finales ---\nprint(\"\\n### 5. Vérifications Cruciales du Tenseur W et de l'Ordre des Cibles ###\")\n\n# Vérification 5.1: Alignement de W avec Y\nassert W_tensor_filtered.shape[0] == globals()['OUTPUT_UNITS']\nprint(f\"✅ Alignement W : W_tensor de taille {W_tensor_filtered.shape[0]} correspond à {globals()['OUTPUT_UNITS']} sorties.\")\n\n# Vérification 5.2: Conservation (ptend_t est bien au début de Y)\nptend_t_cols = [col for col in target_cols_filtered if col.startswith('ptend_t_')]\nif len(ptend_t_cols) == LEVELS and ptend_t_cols == target_cols_filtered[:LEVELS]:\n    print(f\"✅ Ordre Y : Les {LEVELS} colonnes 'ptend_t' sont en tête de Y. La perte de conservation est sécurisée.\")\nelse:\n    print(\"❌ ERREUR CRITIQUE: L'ordre des cibles (Y) n'a pas les 60 colonnes 'ptend_t' en tête. La perte de conservation sera fausse.\")\n    print(\"Premières colonnes de Y :\", target_cols_filtered[:LEVELS])\n    \nprint(\"\\n🎉 Toutes les variables globales nécessaires sont définies. Vous pouvez lancer la cellule d'entraînement V4.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T13:09:24.245451Z","iopub.execute_input":"2025-12-03T13:09:24.246624Z","iopub.status.idle":"2025-12-03T13:09:27.100088Z","shell.execute_reply.started":"2025-12-03T13:09:24.246591Z","shell.execute_reply":"2025-12-03T13:09:27.099129Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import tensorflow as tf\nfrom tensorflow.keras import backend as K\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras.layers import Input, Conv1D, Dense, Flatten, Concatenate, Dropout \nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau\n\n# --- 1. Fonction de Perte PIML Robuste (CORRIGÉE) ---\n\n# W_tensor est récupéré depuis les variables globales (W_tensor_filtered, taille 308)\nW = globals().get('W_tensor')\n\ndef weighted_mse_with_conservation_loss(y_true, y_pred):\n    \"\"\"\n    Fonction de Perte PIML (Physically Informed ML) robuste et sécurisée.\n    Inclut : MSE Pondérée (simplifiée) et Pénalité de Conservation.\n    \"\"\"\n    \n    # 1. Terme MSE Pondérée (la perte principale)\n    err = y_pred - y_true\n    # Multiplier l'erreur au carré par le masque W\n    weighted_sq_err = K.square(err) * W \n    \n    # Calculer la moyenne sur TOUTES les prédictions pondérées\n    weighted_mse_loss = K.mean(weighted_sq_err) \n\n    # 2. Terme de Pénalité de Conservation (sur les 60 premières sorties : ptend_t)\n    LEVELS = globals().get('LEVELS', 60)\n    # Nous utilisons ici la TENDANCE, car la propriété de conservation est ptend_t_sum = 0\n    # Cependant, forcer (y_pred - y_true) à être zéro est plus robuste en pratique.\n    ptend_pred = y_pred[:, :LEVELS]\n    ptend_true = y_true[:, :LEVELS]\n\n    # Pénalité basée sur la non-conservation de la TENDANCE\n    cons_residual = K.sum(ptend_pred - ptend_true, axis=1)\n    cons_penalty = K.mean(K.square(cons_residual))\n\n    # 3. Combinaison\n    # LAMBDA ajusté à une petite valeur pour éviter que la perte physique ne domine la MSE\n    LAMBDA = 1e-4 \n    total_loss = weighted_mse_loss + LAMBDA * cons_penalty\n    return total_loss\n\n# --- 2. Définition du Modèle Multi-Input (Avec Régularisation L2 et Dropout) ---\n\n# Récupération des variables globales\nLEVELS = globals().get('LEVELS', 60)\nN_FEATURES_60D = globals().get('N_FEATURES_60D', 9)\nN_SCALAR_FEATURES = globals().get('N_SCALAR_FEATURES', 16)\nOUTPUT_UNITS = globals().get('OUTPUT_UNITS', 308)\n\n# Définition d'un régularisateur L2 léger\nregularizer = tf.keras.regularizers.l2(1e-5) \n\n# Branche des Profils (CNN 1D)\nprofile_input = Input(shape=(LEVELS, N_FEATURES_60D), name='profile_input')\nx = Conv1D(filters=64, kernel_size=3, activation='relu', padding='same', kernel_regularizer=regularizer)(profile_input)\nx = Conv1D(filters=32, kernel_size=3, activation='relu', padding='same', kernel_regularizer=regularizer)(x)\nx = Flatten()(x)\nx = Dropout(0.1)(x) \n\n# Branche des Scalaires (MLP)\nscalar_input = Input(shape=(N_SCALAR_FEATURES,), name='scalar_input')\ny = Dense(32, activation='relu', kernel_regularizer=regularizer)(scalar_input)\ny = Dense(16, activation='relu', kernel_regularizer=regularizer)(y) \ny = Dropout(0.1)(y)\n\n# Combinaison\ncombined = Concatenate()([x, y])\nz = Dense(64, activation='relu', kernel_regularizer=regularizer)(combined)\noutput = Dense(OUTPUT_UNITS, activation='linear', name='final_output')(z) \n\nmodel_optimized = Model(inputs=[profile_input, scalar_input], outputs=output)\n\n\n# --- 3. Compilation et Callbacks Avancés ---\n\n# Définition du nom du fichier\n# Note: Le ModelCheckpoint ci-dessous utilise le format .h5 pour la compatibilité\nMODEL_FILENAME = 'best_model_PIML_V5_CORRECT.keras' \n\n# Callbacks avancés\nearly_stopping = EarlyStopping(\n    monitor='val_loss', \n    patience=10, \n    restore_best_weights=True\n)\n# Le ModelCheckpoint ne sauvegarde que le MEILLEUR modèle (pour l'architecture V4)\ncheckpoint = ModelCheckpoint(\n    'best_model_PIML_V4.h5', \n    monitor='val_loss', \n    save_best_only=True, \n    save_weights_only=False\n)\nreduce_lr = ReduceLROnPlateau(\n    monitor='val_loss', \n    factor=0.5, \n    patience=5, \n    min_lr=1e-7\n)\n\nmodel_optimized.compile(\n    optimizer=Adam(learning_rate=1e-4), \n    loss=weighted_mse_with_conservation_loss,\n    metrics=[] \n)\n\nprint(\"\\n### Résumé du Modèle PIML Multi-Input V5 Sécurisé ###\")\nmodel_optimized.summary()\nprint(\"-\" * 50)\n\nprint(\"Démarrage de l'entraînement V5...\")\n\n# Récupération des tenseurs d'entraînement (supposés globaux)\nX_train_CNN = globals().get('X_train_CNN')\nX_train_SCALAR = globals().get('X_train_SCALAR')\nY_train_scaled = globals().get('Y_train_scaled')\nX_val_CNN = globals().get('X_val_CNN')\nX_val_SCALAR = globals().get('X_val_SCALAR')\nY_val_scaled = globals().get('Y_val_scaled')\n\nhistory_optimized = model_optimized.fit(\n    [X_train_CNN, X_train_SCALAR], \n    Y_train_scaled, \n    epochs=100, \n    batch_size=1024,\n    validation_data=([X_val_CNN, X_val_SCALAR], Y_val_scaled),\n    callbacks=[early_stopping, checkpoint, reduce_lr],\n    verbose=1\n)\n\n# -----------------------------------------------------------\n# 🌟 AJOUT CRITIQUE DE SÉCURITÉ : SAUVEGARDE MANUELLE DU MEILLEUR MODÈLE\n# -----------------------------------------------------------\n# Assurez-vous d'abord de restaurer les meilleurs poids trouvés pendant l'entraînement\nmodel_optimized.set_weights(early_stopping.best_weights)\n\n# Sauvegarder dans le nouveau format .keras pour une compatibilité maximale\nmodel_optimized.save(MODEL_FILENAME)\n\nprint(f\"\\n🎉 Entraînement terminé. Le MEILLEUR modèle (architecture et poids) est sauvegardé dans '{MODEL_FILENAME}'.\")\nprint(\"-\" * 50)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T13:10:15.688878Z","iopub.execute_input":"2025-12-03T13:10:15.689713Z","iopub.status.idle":"2025-12-03T13:15:13.696875Z","shell.execute_reply.started":"2025-12-03T13:10:15.689659Z","shell.execute_reply":"2025-12-03T13:15:13.694992Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport tensorflow as tf\nfrom tensorflow.keras.models import load_model \nfrom tensorflow.keras import backend as K\nimport os\n\n# --- 0. Configuration et Variables Globales ---\nROOT = globals().get('ROOT', '/kaggle/input/leap-atmospheric-physics-ai-climsim')\nTEST_FILE = f'{ROOT}/test.csv'\nSAMPLE_SUBMISSION_FILE = f'{ROOT}/sample_submission.csv'\nMODEL_PATH = 'best_model_PIML_V5_CORRECT.keras' \n\n# Récupération des objets cruciaux\nscaler_X = globals().get('scaler_X')\nscaler_Y = globals().get('scaler_Y')\ninput_cols = globals().get('input_cols')\ntarget_cols_filtered = globals().get('target_cols_filtered')\nid_col = 'sample_id'\nLEVELS = globals().get('LEVELS', 60)\nN_FEATURES_60D = globals().get('N_FEATURES_60D', 9)\n\n# ⚠️ VÉRIFICATION CRITIQUE ⚠️\nif scaler_X is None or scaler_Y is None or target_cols_filtered is None:\n    print(\"❌ ERREUR FATALE: Les objets de normalisation (scaler_X/Y) ou les colonnes cibles sont manquants.\")\n    print(\"Veuillez vous assurer que la cellule d'entraînement a été exécutée et que les scalers sont dans globals().\")\n    exit()\n\n# --- 1. FONCTIONS CUSTOMISÉES ---\ndef weighted_mse_with_conservation_loss(y_true, y_pred):\n    return K.mean(K.square(y_pred - y_true))\n\n# --- 2. Chargement du Modèle et Prédictions ---\nprint(f\"Chargement du modèle depuis : {MODEL_PATH}\")\ncustom_objects = {'weighted_mse_with_conservation_loss': weighted_mse_with_conservation_loss }\ntry:\n    model_optimized = tf.keras.models.load_model(\n        MODEL_PATH, \n        custom_objects=custom_objects,\n        compile=False\n    )\nexcept Exception as e:\n    print(f\"❌ ERREUR FATALE : Le modèle est illisible. {e}\")\n    exit()\n\nprint(\"Chargement des données de test...\")\ndf_test = pd.read_csv(TEST_FILE)\ndf_submission_sample = pd.read_csv(SAMPLE_SUBMISSION_FILE)\n\n# Préparation de X_test\nX_test_brut = df_test[input_cols].values\nX_test_scaled = scaler_X.transform(X_test_brut)\nX_test_profile = X_test_scaled[:, :N_FEATURES_60D * LEVELS]\nX_test_CNN = X_test_profile.reshape(-1, LEVELS, N_FEATURES_60D)\nX_test_SCALAR = X_test_scaled[:, N_FEATURES_60D * LEVELS:]\n\nprint(\"Démarrage des prédictions...\")\nY_pred_scaled = model_optimized.predict([X_test_CNN, X_test_SCALAR], batch_size=1024)\n\n# 🚀 V12 NOUVEAUTÉ 1 : CLIPPING DANS L'ESPACE NORMALISÉ\n# Une valeur > 10 dans l'espace normalisé est déjà trop grande.\nSCALED_CLIP_VALUE = 10.0\nY_pred_scaled_clipped = np.clip(Y_pred_scaled, a_min=-SCALED_CLIP_VALUE, a_max=SCALED_CLIP_VALUE)\nprint(f\"✅ Clipping dans l'espace normalisé appliqué (min/max: +/- {SCALED_CLIP_VALUE}).\")\n\n# Dénormalisation\nY_pred_denormalized = scaler_Y.inverse_transform(Y_pred_scaled_clipped)\n\n# 🚀 V12 NOUVEAUTÉ 2 : CLIPPING AGRESSIF (REDONDANT MAIS SÉCURISÉ)\nCLIP_VALUE = 1e+5 # 100 000.0 K/s \nY_pred_denormalized_clipped = np.clip(Y_pred_denormalized, a_min=-CLIP_VALUE, a_max=CLIP_VALUE)\nprint(f\"✅ Re-Clipping de sécurité AGRESSIF appliqué (min/max: +/- {CLIP_VALUE} K/s).\")\n\n\n# --- 3. POST-TRAITEMENT PHYSIQUE (Ptend Trick) ---\ncol_source = 'ptend_q0002_2' \ncol_target = 'ptend_q0002_26' \ntry:\n    idx_source = target_cols_filtered.index(col_source)\n    idx_target = target_cols_filtered.index(col_target)\n    Y_pred_denormalized_clipped[:, idx_target] = Y_pred_denormalized_clipped[:, idx_source]\n    print(f\"✅ Correction du Ptend Trick appliquée ({col_source} -> {col_target}).\")\nexcept:\n    pass # Ne pas interrompre si le Ptend Trick échoue\n\n# --- 4. CONSTRUCTION DU FICHIER DE SOUMISSION (MÉTHODE V10 CORRECTE) ---\ndf_pred_filtered = pd.DataFrame(Y_pred_denormalized_clipped, columns=target_cols_filtered)\n\nprint(\"Démarrage de la construction (Méthode V10: Multiplication par W)...\")\nsubmission_cols = [col for col in df_submission_sample.columns if col != id_col]\ndf_submission_final = pd.DataFrame(0.0, index=df_test.index, columns=[id_col] + submission_cols) \ndf_submission_final[id_col] = df_test[id_col] \ndf_submission_final[target_cols_filtered] = df_pred_filtered[target_cols_filtered]\n\n# 4.3 Récupérer la MATRICE DES POIDS W \nW_matrix_full = df_submission_sample[submission_cols].values \n\n# 4.4 L'ÉTAPE CRITIQUE : MULTIPLIER PAR W (Conversion en W/m²)\ndf_submission_final.iloc[:, 1:] = df_submission_final.iloc[:, 1:].values * W_matrix_full\n\n# 🚀 V12 NOUVEAUTÉ 3 : CLIPPING DU RÉSULTAT FINAL (en unités W/m²)\nFINAL_CLIP_VALUE = 1e+4 # 10 000.0 W/m² (Limite ultra-stricte)\ndf_submission_final.iloc[:, 1:] = np.clip(\n    df_submission_final.iloc[:, 1:].values,\n    a_min=-FINAL_CLIP_VALUE,\n    a_max=FINAL_CLIP_VALUE\n)\nprint(f\"✅ Clipping FINAL appliqué (min/max: +/- {FINAL_CLIP_VALUE} W/m²).\")\n\n\n# 4.5 Sauvegarde\nsubmission_filename = 'submission_PIML_V12_TRIPLE_CLIPPED.csv'\ndf_submission_final.to_csv(submission_filename, index=False)\n\nprint(\"-\" * 50)\nprint(f\"🎉 Soumission finalisée et sauvegardée sous : {submission_filename} (TRIPLE SÉCURITÉ)\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T13:17:33.507248Z","iopub.execute_input":"2025-12-03T13:17:33.508654Z","iopub.status.idle":"2025-12-03T13:17:34.90771Z","shell.execute_reply.started":"2025-12-03T13:17:33.508617Z","shell.execute_reply":"2025-12-03T13:17:34.906574Z"}},"outputs":[],"execution_count":null}]}