import pandas as pd import numpy as np from config import CITIES_COORDS def apply_feature_engineering(df_input): df = df_input.copy() # 1. Nettoyage des types (Évite l'erreur TypeError str vs int) df['time'] = pd.to_datetime(df['time']) numeric_cols = [ 'temperature_2m_max', 'temperature_2m_min', 'temperature_2m_mean', 'precipitation_sum', 'wind_speed_10m_max', 'wind_gusts_10m_max', 'shortwave_radiation_sum', 'et0_fao_evapotranspiration', 'sunshine_duration', 'daylight_duration' ] for col in numeric_cols: if col in df.columns: df[col] = pd.to_numeric(df[col], errors='coerce').fillna(0) # 2. Tri impératif pour les calculs de séries temporelles df = df.sort_values(['city', 'time']).reset_index(drop=True) # 3. Calendrier df['mois'] = df['time'].dt.month df['annee'] = df['time'].dt.year df['quarter'] = df['time'].dt.quarter df['day_of_year'] = df['time'].dt.dayofyear df['month_sin'] = np.sin(2 * np.pi * df['mois'] / 12) df['month_cos'] = np.cos(2 * np.pi * df['mois'] / 12) df['day_sin'] = np.sin(2 * np.pi * df['day_of_year'] / 365) df['day_cos'] = np.cos(2 * np.pi * df['day_of_year'] / 365) # 4. Physique et Indices df['amplitude_thermique'] = df['temperature_2m_max'] - df['temperature_2m_min'] df['ecart_ressenti'] = df['apparent_temperature_mean'] - df['temperature_2m_mean'] df['bilan_hydrique'] = df['precipitation_sum'] - df['et0_fao_evapotranspiration'] df['sunshine_ratio'] = df['sunshine_duration'] / (df['daylight_duration'] + 1e-6) df['gust_ratio'] = df['wind_gusts_10m_max'] / (df['wind_speed_10m_max'] + 1e-6) df['heat_stress'] = df['temperature_2m_mean'] * df['et0_fao_evapotranspiration'] df['temp_per_radiation'] = df['temperature_2m_mean'] / (df['shortwave_radiation_sum'] + 1e-6) df['is_dry_season'] = df['mois'].isin([11, 12, 1, 2, 3]).astype(int) df['is_weekend'] = df['time'].dt.dayofweek.isin([5, 6]).astype(int) df['is_no_rain'] = (df['precipitation_sum'] < 0.1).astype(int) df['is_no_wind'] = (df['wind_speed_10m_max'] < 5).astype(int) df['stagnation_index'] = ((df['wind_speed_10m_max'] < 15) & (df['precipitation_sum'] == 0)).astype(int) df['is_hot_day'] = (df['temperature_2m_max'] >= 35).astype(int) df['is_heavy_rain'] = (df['precipitation_sum'] >= 20).astype(int) # 5. Groupement par ville pour les Lags et Rolling # On utilise groupby().transform pour garder la taille originale du DF grouped = df.groupby('city') for lag in [1, 3, 7]: df[f'temp_lag{lag}'] = grouped['temperature_2m_mean'].shift(lag) df[f'wind_lag{lag}'] = grouped['wind_speed_10m_max'].shift(lag) df[f'precip_lag{lag}'] = grouped['precipitation_sum'].shift(lag) df[f'wind_dir_lag{lag}'] = grouped['wind_direction_10m_dominant'].shift(lag) df[f'sunshine_lag{lag}'] = grouped['sunshine_duration'].shift(lag) df['temp_roll7'] = grouped['temperature_2m_mean'].transform(lambda x: x.rolling(7, min_periods=1).mean()) df['precip_roll7'] = grouped['precipitation_sum'].transform(lambda x: x.rolling(7, min_periods=1).mean()) df['wind_roll7'] = grouped['wind_speed_10m_max'].transform(lambda x: x.rolling(7, min_periods=1).mean()) df['temp_roll30'] = grouped['temperature_2m_mean'].transform(lambda x: x.rolling(30, min_periods=1).mean()) df['temp_anomaly'] = df['temperature_2m_mean'] - df['temp_roll30'] df['precip_cumul7'] = grouped['precipitation_sum'].transform(lambda x: x.rolling(7, min_periods=1).sum()) df['precip_cumul3'] = grouped['precipitation_sum'].transform(lambda x: x.rolling(3, min_periods=1).sum()) def get_region_name(city): return CITIES_COORDS.get(city, {}).get('region', 'Littoral') df['region'] = df['city'].apply(get_region_name) def categorize_weather(code): if code == 0: return 'Ciel dégagé' elif code in [1, 2]: return 'Nuageux' elif code == 3: return 'Couvert' else: return 'Pluie/Bruine' df['weather_categorie'] = df['weather_code'].apply(categorize_weather) df['weather_encoded'] = df['weather_code'].astype(float) cols_to_fix = [c for c in df.columns if 'lag' in c or 'roll' in c] for col in cols_to_fix: df[col] = df.groupby('city')[col].ffill().bfill() return df.fillna(0)