Spaces:
Sleeping
Sleeping
Download src/module/Features.py from jaimalklyde/ProjetData: direct link, hf CLI and curl.
- Browser
- Download file 25.6 kB
-
https://huggingface.co/spaces/jaimalklyde/ProjetData/resolve/main/src/module/Features.py
- Command line
-
hf download hf://spaces/jaimalklyde/ProjetData/src/module/Features.py
-
curl -L -o Features.py https://huggingface.co/spaces/jaimalklyde/ProjetData/resolve/main/src/module/Features.py
25.6 kB
| """ | |
| MODULE: FEATURE ENGINEERING (ML PREP) - VERSION OPTIMISÉE | |
| ========================================================== | |
| Responsabilité : Transformation des données CLEAN (de l'ETL) | |
| en vecteurs mathématiques pour le Machine Learning. | |
| Style : Gotham UI | |
| Pipeline : | |
| 1. Récupération des données CLEAN depuis DataExplorer (ETL) | |
| 2. Génération de features temporelles | |
| 3. Création de lags et rolling statistics | |
| 4. Features d'interaction | |
| 5. Stockage du dataset ML final | |
| CHANGEMENTS vs Version Précédente : | |
| - ✅ Utilise DataExplorer.dataframes_clean (données déjà nettoyées) | |
| - ✅ Suppression de _clean_for_ml() (redondant avec ETL) | |
| - ✅ Pas de dropna() agressif (déjà fait par ETL) | |
| - ✅ Fusion correcte avec admissions_jour | |
| - ✅ Gestion robuste des colonnes manquantes | |
| """ | |
| import pandas as pd | |
| import numpy as np | |
| from datetime import datetime, timedelta | |
| import streamlit as st | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| # Intégration de l'ETL Engine | |
| from module.Validation_Contract import ETL_Engine | |
| # ===================================================== | |
| # ============= FEATURE ENGINEERING =================== | |
| # ===================================================== | |
| class FeatureEngineer: | |
| """ | |
| Moteur de génération de features ML à partir de données CLEAN. | |
| Prend en entrée les données déjà nettoyées par l'ETL. | |
| """ | |
| def __init__(self, data_explorer): | |
| """ | |
| Initialisation avec DataExplorer (contient RAW et CLEAN) | |
| Args: | |
| data_explorer: Instance de DataExplorer avec dataframes_clean | |
| """ | |
| # IMPORTANT : On utilise les données CLEAN, pas RAW | |
| self.clean_data = data_explorer.dataframes_clean | |
| self.ml_dataset = None | |
| def create_temporal_features(self, df, date_col='date'): | |
| """ | |
| Génération des cycles temporels et périodes hospitalières. | |
| Args: | |
| df: DataFrame avec colonne date | |
| date_col: Nom de la colonne date | |
| Returns: | |
| DataFrame enrichi avec features temporelles | |
| """ | |
| df = df.copy() | |
| df[date_col] = pd.to_datetime(df[date_col], errors='coerce') | |
| # Cycles Humains | |
| df['jour_semaine'] = df[date_col].dt.dayofweek # 0=Lundi, 6=Dimanche | |
| df['est_weekend'] = (df['jour_semaine'] >= 5).astype(int) | |
| df['mois'] = df[date_col].dt.month | |
| df['trimestre'] = df[date_col].dt.quarter | |
| df['jour_annee'] = df[date_col].dt.dayofyear | |
| # Cycles Saisonniers (Encodage cyclique pour le ML) | |
| # Permet au modèle de comprendre que Décembre (12) est proche de Janvier (1) | |
| df['mois_sin'] = np.sin(2 * np.pi * df['mois'] / 12) | |
| df['mois_cos'] = np.cos(2 * np.pi * df['mois'] / 12) | |
| # Encodage cyclique du jour de la semaine | |
| df['jour_semaine_sin'] = np.sin(2 * np.pi * df['jour_semaine'] / 7) | |
| df['jour_semaine_cos'] = np.cos(2 * np.pi * df['jour_semaine'] / 7) | |
| # Périodes Hospitalières Critiques (basées sur épidémiologie française) | |
| df['periode_grippe'] = ((df['mois'] >= 11) | (df['mois'] <= 3)).astype(int) | |
| df['periode_canicule'] = ((df['mois'] >= 6) & (df['mois'] <= 8)).astype(int) | |
| df['periode_gastro'] = ((df['mois'] >= 11) | (df['mois'] <= 2)).astype(int) | |
| df['periode_bronchiolite'] = ((df['mois'] >= 10) & (df['mois'] <= 2)).astype(int) | |
| # Jours fériés approximatifs (1er janvier, 1er mai, 14 juillet, 25 décembre) | |
| df['jour_ferie_approx'] = ( | |
| ((df['mois'] == 1) & (df[date_col].dt.day == 1)) | | |
| ((df['mois'] == 5) & (df[date_col].dt.day == 1)) | | |
| ((df['mois'] == 7) & (df[date_col].dt.day == 14)) | | |
| ((df['mois'] == 12) & (df[date_col].dt.day == 25)) | |
| ).astype(int) | |
| return df | |
| def create_lags_and_rolling(self, df, value_col, group_col='service_code', | |
| lags=[1, 7, 14], windows=[7, 14, 30]): | |
| """ | |
| Création de features de mémoire (lags) et statistiques mobiles (rolling). | |
| Args: | |
| df: DataFrame trié par date | |
| value_col: Colonne pour laquelle créer les lags | |
| group_col: Colonne de groupement (ex: service_code) | |
| lags: Liste des périodes de lag en jours | |
| windows: Liste des fenêtres pour rolling stats | |
| Returns: | |
| DataFrame enrichi avec lags et rolling | |
| """ | |
| df = df.copy() | |
| # Tri par date et groupe (ESSENTIEL pour séries temporelles) | |
| if group_col in df.columns: | |
| df = df.sort_values(['date', group_col]) | |
| else: | |
| df = df.sort_values('date') | |
| # Vérification que la colonne existe | |
| if value_col not in df.columns: | |
| st.warning(f"⚠️ Colonne '{value_col}' non trouvée, lags ignorés") | |
| return df | |
| # ===== LAGS (J-1, J-7, J-14, etc.) ===== | |
| for lag in lags: | |
| col_name = f'{value_col}_lag_{lag}' | |
| if group_col in df.columns: | |
| df[col_name] = df.groupby(group_col)[value_col].shift(lag) | |
| else: | |
| df[col_name] = df[value_col].shift(lag) | |
| # ===== ROLLING STATISTICS ===== | |
| for window in windows: | |
| # Moyenne mobile | |
| col_mean = f'{value_col}_mean_{window}j' | |
| if group_col in df.columns: | |
| df[col_mean] = df.groupby(group_col)[value_col].transform( | |
| lambda x: x.rolling(window=window, min_periods=1).mean() | |
| ) | |
| else: | |
| df[col_mean] = df[value_col].rolling(window=window, min_periods=1).mean() | |
| # Écart-type mobile (Volatilité) | |
| col_std = f'{value_col}_std_{window}j' | |
| if group_col in df.columns: | |
| df[col_std] = df.groupby(group_col)[value_col].transform( | |
| lambda x: x.rolling(window=window, min_periods=1).std() | |
| ).fillna(0) | |
| else: | |
| df[col_std] = df[value_col].rolling(window=window, min_periods=1).std().fillna(0) | |
| # Min mobile | |
| col_min = f'{value_col}_min_{window}j' | |
| if group_col in df.columns: | |
| df[col_min] = df.groupby(group_col)[value_col].transform( | |
| lambda x: x.rolling(window=window, min_periods=1).min() | |
| ) | |
| else: | |
| df[col_min] = df[value_col].rolling(window=window, min_periods=1).min() | |
| # Max mobile | |
| col_max = f'{value_col}_max_{window}j' | |
| if group_col in df.columns: | |
| df[col_max] = df.groupby(group_col)[value_col].transform( | |
| lambda x: x.rolling(window=window, min_periods=1).max() | |
| ) | |
| else: | |
| df[col_max] = df[value_col].rolling(window=window, min_periods=1).max() | |
| return df | |
| def create_interaction_features(self, df): | |
| """ | |
| Création de features d'interaction entre variables. | |
| """ | |
| df = df.copy() | |
| # Interaction tension/occupation (Indice de stress hospitalier) | |
| if 'taux_occupation' in df.columns and 'tension_personnel' in df.columns: | |
| df['indice_stress'] = df['taux_occupation'] * df['tension_personnel'] | |
| df['stress_eleve'] = (df['indice_stress'] > 1.0).astype(int) | |
| # Interaction scénario/weekend | |
| if 'scenario_intensity' in df.columns and 'est_weekend' in df.columns: | |
| df['intensity_x_weekend'] = df['scenario_intensity'] * df['est_weekend'] | |
| # Interaction période critique/occupation | |
| if 'periode_grippe' in df.columns and 'taux_occupation' in df.columns: | |
| df['grippe_x_occupation'] = df['periode_grippe'] * df['taux_occupation'] | |
| # Ratio personnel disponible / requis | |
| if 'personnel_disponible' in df.columns and 'personnel_requis' in df.columns: | |
| # On ajoute +1e-6 pour éviter la division par zéro | |
| df['ratio_personnel'] = df['personnel_disponible'] / (df['personnel_requis'] + 1e-6) | |
| df['deficit_personnel'] = (df['ratio_personnel'] < 1.0).astype(int) | |
| # --- CORRECTION OPTIMISÉE ICI --- | |
| # Capacité restante (Calcul direct car l'ETL a déjà typé les données) | |
| if 'lits_total' in df.columns and 'lits_occupes' in df.columns: | |
| df['capacite_restante'] = df['lits_total'] - df['lits_occupes'] | |
| df['saturation'] = (df['capacite_restante'] <= 5).astype(int) | |
| # -------------------------------- | |
| return df | |
| def build_full_dataset(self): | |
| """ | |
| Assemblage complet du Dataset ML Final. | |
| Pipeline : | |
| 1. Récupération données CLEAN (ressources + admissions + scénarios) | |
| 2. Fusion intelligente | |
| 3. Enrichissement temporel | |
| 4. Lags & Rolling | |
| 5. Features d'interaction | |
| 6. Nettoyage final léger (seulement NaN des lags) | |
| Returns: | |
| Tuple (DataFrame ML, message) | |
| """ | |
| # ===== 1. RÉCUPÉRATION DONNÉES CLEAN ===== | |
| df_res = self.clean_data.get('ressources_jour') | |
| df_adm = self.clean_data.get('admissions_jour') | |
| df_scenarios = self.clean_data.get('scenarios_calendrier') | |
| # Vérification données essentielles | |
| if df_res is None or df_res.empty: | |
| return None, "❌ Données 'ressources_jour' manquantes ou vides" | |
| # ===== 2. BASE : RESSOURCES JOURNALIÈRES ===== | |
| master = df_res.copy() | |
| # Conversion date si nécessaire | |
| if 'date' in master.columns: | |
| master['date'] = pd.to_datetime(master['date'], errors='coerce') | |
| else: | |
| return None, "❌ Colonne 'date' manquante dans ressources_jour" | |
| # ===== 3. FUSION AVEC ADMISSIONS ===== | |
| if df_adm is not None and not df_adm.empty: | |
| # Conversion date admissions | |
| if 'date' in df_adm.columns: | |
| df_adm['date'] = pd.to_datetime(df_adm['date'], errors='coerce') | |
| # Sélection colonnes clés (éviter doublons) | |
| cols_adm = ['date', 'service_code', 'nb_admissions'] | |
| cols_adm = [c for c in cols_adm if c in df_adm.columns] | |
| # Détermination clés de fusion | |
| if 'service_code' in df_adm.columns and 'service_code' in master.columns: | |
| merge_keys = ['date', 'service_code'] | |
| else: | |
| merge_keys = ['date'] | |
| # Fusion | |
| master = master.merge(df_adm[cols_adm], on=merge_keys, how='left') | |
| # Remplacement NaN par 0 (pas d'admission = 0) | |
| if 'nb_admissions' in master.columns: | |
| master['nb_admissions'] = master['nb_admissions'].fillna(0) | |
| else: | |
| st.warning("⚠️ Données 'admissions_jour' non disponibles, nb_admissions manquera") | |
| # ===== 4. ENRICHISSEMENT TEMPOREL ===== | |
| master = self.create_temporal_features(master) | |
| # ===== 5. FUSION AVEC SCÉNARIOS ===== | |
| if df_scenarios is not None and not df_scenarios.empty: | |
| # Conversion date scénarios | |
| if 'date' in df_scenarios.columns: | |
| df_scenarios['date'] = pd.to_datetime(df_scenarios['date'], errors='coerce') | |
| # Sélection colonnes | |
| cols_to_take = ['date', 'intensity'] | |
| # Ajout scénario si pas déjà présent | |
| if 'scenario' not in master.columns and 'scenario' in df_scenarios.columns: | |
| cols_to_take.append('scenario') | |
| # Filtrage colonnes existantes | |
| cols_to_take = [c for c in cols_to_take if c in df_scenarios.columns] | |
| # Rename intensity pour éviter conflit | |
| scen_clean = df_scenarios[cols_to_take].copy() | |
| if 'intensity' in scen_clean.columns: | |
| scen_clean = scen_clean.rename(columns={'intensity': 'scenario_intensity'}) | |
| # Fusion | |
| master = master.merge(scen_clean, on='date', how='left') | |
| # Remplissage NaN intensity par 0 (scénario normal) | |
| if 'scenario_intensity' in master.columns: | |
| master['scenario_intensity'] = master['scenario_intensity'].fillna(0) | |
| # Feature binaire : scénario actif ou non | |
| if 'scenario' in master.columns: | |
| master['scenario_is_normal'] = (master['scenario'] == 'Normal').astype(int) | |
| else: | |
| st.warning("⚠️ Données 'scenarios_calendrier' non disponibles") | |
| # ===== 6. LAGS & ROLLING STATISTICS ===== | |
| # Tri chronologique AVANT lags | |
| if 'service_code' in master.columns: | |
| master = master.sort_values(['date', 'service_code']).reset_index(drop=True) | |
| group_col = 'service_code' | |
| else: | |
| master = master.sort_values('date').reset_index(drop=True) | |
| group_col = None | |
| # Lags sur taux_occupation | |
| if 'taux_occupation' in master.columns: | |
| master = self.create_lags_and_rolling( | |
| master, | |
| 'taux_occupation', | |
| group_col=group_col, | |
| lags=[1, 7, 14], | |
| windows=[7, 14, 30] | |
| ) | |
| # Lags sur nb_admissions | |
| if 'nb_admissions' in master.columns: | |
| master = self.create_lags_and_rolling( | |
| master, | |
| 'nb_admissions', | |
| group_col=group_col, | |
| lags=[1, 7, 14], | |
| windows=[7, 14] | |
| ) | |
| # Lags sur tension_personnel | |
| if 'tension_personnel' in master.columns: | |
| master = self.create_lags_and_rolling( | |
| master, | |
| 'tension_personnel', | |
| group_col=group_col, | |
| lags=[1, 7], | |
| windows=[7] | |
| ) | |
| # ===== 7. FEATURES D'INTERACTION ===== | |
| master = self.create_interaction_features(master) | |
| # ===== 8. NETTOYAGE FINAL LÉGER ===== | |
| # On supprime SEULEMENT les lignes où les lags sont NaN (premières lignes de chaque groupe) | |
| rows_before = len(master) | |
| master = master.dropna() | |
| rows_after = len(master) | |
| rows_removed = rows_before - rows_after | |
| if rows_removed > 0: | |
| st.info(f"ℹ️ {rows_removed} lignes supprimées (NaN des lags sur premières dates)") | |
| # ========================================================= | |
| # 🔥 AJOUT CRITIQUE : ENCODAGE POUR COMPATIBILITÉ ML 🔥 | |
| # ========================================================= | |
| # Les modèles (XGBoost, Random Forest) ne savent pas lire le texte (ex: "CHIRURGIE"). | |
| # On convertit toutes les colonnes TEXTE (sauf la date) en CHIFFRES. | |
| # On détecte les colonnes de type 'object' (texte) ou 'category' | |
| categorical_cols = master.select_dtypes(include=['object', 'category']).columns | |
| count_encoded = 0 | |
| for col in categorical_cols: | |
| if col != 'date': # On ne touche SURTOUT PAS à la date (utile pour le split temporel) | |
| # Transformation : "CHIRURGIE" -> 0, "REANIMATION" -> 1 | |
| master[col] = master[col].astype('category').cat.codes | |
| count_encoded += 1 | |
| if count_encoded > 0: | |
| st.caption(f"🔧 {count_encoded} colonnes texte converties en codes numériques pour l'IA.") | |
| # ========================================================= | |
| # Stockage du dataset final | |
| self.ml_dataset = master | |
| return master, f"✅ Dataset ML créé : {len(master):,} échantillons, {len(master.columns)} features" | |
| # ===================================================== | |
| # ============= INTERFACE STREAMLIT =================== | |
| # ===================================================== | |
| def show_feature_engine(client, sheet_name): | |
| """ | |
| Interface Streamlit pour le Feature Engineering. | |
| Args: | |
| client: Client Google Sheets | |
| sheet_name: Nom de la feuille Google Sheets | |
| """ | |
| st.markdown("## FEATURE ENGINEERING CORE") | |
| st.caption("Transformation des données CLEAN (ETL) en vecteurs ML avec lags, rolling stats et interactions.") | |
| # ===== 1. CHARGEMENT VIA ETL ===== | |
| etl = ETL_Engine(client, sheet_name) | |
| # Vérification cache ETL | |
| if 'data_explorer' in st.session_state and hasattr(st.session_state.data_explorer, 'dataframes_clean'): | |
| data_explorer = st.session_state.data_explorer | |
| st.success("✅ Données CLEAN récupérées du Cache ETL") | |
| else: | |
| with st.spinner(" Exécution ETL requis..."): | |
| success, result = etl.run_etl() | |
| if not success: | |
| st.error(f"❌ Erreur ETL : {result}") | |
| return | |
| data_explorer = result | |
| # Affichage info sur les données source | |
| summary = data_explorer.get_summary() | |
| col1, col2, col3 = st.columns(3) | |
| col1.metric("Tables CLEAN disponibles", summary['tables_loaded']) | |
| col2.metric("Lignes CLEAN totales", f"{summary['total_rows_clean']:,}") | |
| col3.metric("Lignes supprimées par ETL", f"{summary['total_rows_removed']:,}") | |
| st.divider() | |
| # ===== 2. GÉNÉRATION AUTOMATIQUE DES FEATURES ===== | |
| if 'ml_features' not in st.session_state: | |
| engine = FeatureEngineer(data_explorer) | |
| with st.spinner(" Génération des features (Lags, Rolling, Saisons, Interactions)..."): | |
| df_final, msg = engine.build_full_dataset() | |
| if df_final is not None and not df_final.empty: | |
| st.session_state['ml_features'] = df_final | |
| st.success(msg) | |
| else: | |
| st.error(f"❌ Échec génération features : {msg}") | |
| return | |
| # ===== 3. AFFICHAGE DU RÉSULTAT ===== | |
| if 'ml_features' in st.session_state: | |
| df = st.session_state['ml_features'] | |
| # En-tête avec bouton de rafraîchissement | |
| col_titre, col_btn = st.columns([4, 1]) | |
| with col_titre: | |
| st.success(f"✅ Dataset ML Prêt ({len(df):,} échantillons)") | |
| with col_btn: | |
| if st.button(" Recalculer", key="recalc_features"): | |
| del st.session_state['ml_features'] | |
| st.rerun() | |
| # ===== KPI BAR ===== | |
| k1, k2, k3, k4 = st.columns(4) | |
| k1.metric("Lignes ML", f"{len(df):,}", delta="Clean Data", help="Données prêtes pour entraînement") | |
| k2.metric("Features Totales", len(df.columns), delta="+Lags & Rolling", help="Nombre total de colonnes") | |
| k3.metric("Période", f"{df['date'].min().strftime('%Y-%m-%d')} → {df['date'].max().strftime('%Y-%m-%d')}") | |
| k4.metric("Services", df['service_code'].nunique() if 'service_code' in df.columns else "N/A") | |
| st.divider() | |
| # ===== ANALYSE DES FEATURES ===== | |
| st.subheader(" Analyse des Features Générées") | |
| # Catégorisation des features | |
| feature_categories = { | |
| 'Temporelles': [c for c in df.columns if any(x in c for x in | |
| ['mois', 'jour', 'semaine', 'weekend', 'periode', 'trimestre', 'ferie'])], | |
| 'Lags': [c for c in df.columns if '_lag_' in c], | |
| 'Rolling': [c for c in df.columns if any(x in c for x in ['_mean_', '_std_', '_min_', '_max_'])], | |
| 'Interaction': [c for c in df.columns if any(x in c for x in | |
| ['indice_', '_x_', 'ratio_', 'deficit_', 'saturation', 'capacite_'])], | |
| 'Scénarios': [c for c in df.columns if 'scenario' in c.lower() or 'intensity' in c], | |
| 'Ressources': [c for c in df.columns if any(x in c for x in | |
| ['lits', 'personnel', 'taux', 'tension'])], | |
| } | |
| # Affichage par catégorie | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| for category in ['Temporelles', 'Lags', 'Rolling']: | |
| features = feature_categories[category] | |
| with st.expander(f"{category} ({len(features)} features)"): | |
| if features: | |
| st.write(features) | |
| else: | |
| st.info("Aucune feature dans cette catégorie") | |
| with col2: | |
| for category in ['Interaction', 'Scénarios', 'Ressources']: | |
| features = feature_categories[category] | |
| with st.expander(f"{category} ({len(features)} features)"): | |
| if features: | |
| st.write(features) | |
| else: | |
| st.info("Aucune feature dans cette catégorie") | |
| st.divider() | |
| # ===== MATRICE DE CORRÉLATION ===== | |
| st.subheader(" Matrice de Corrélation") | |
| numerics = df.select_dtypes(include=[np.number]) | |
| # Sélection intelligente des colonnes à afficher | |
| priority_cols = [] | |
| # Target si disponible | |
| if 'nb_admissions' in numerics.columns: | |
| priority_cols.append('nb_admissions') | |
| # Features importantes | |
| priority_cols.extend([c for c in numerics.columns if any(x in c for x in | |
| ['lag_', 'mean_', 'std_', 'taux', 'tension', 'indice', 'intensity'])]) | |
| # Limitation à 20 pour lisibilité | |
| cols_to_plot = list(dict.fromkeys(priority_cols))[:20] | |
| if cols_to_plot and len(cols_to_plot) > 1: | |
| corr = numerics[cols_to_plot].corr() | |
| fig_corr = px.imshow( | |
| corr, | |
| text_auto=".2f", | |
| color_continuous_scale="RdBu_r", | |
| aspect="auto", | |
| title="Corrélation : Features Clés" | |
| ) | |
| fig_corr.update_layout(height=600) | |
| st.plotly_chart(fig_corr, use_container_width=True) | |
| else: | |
| st.info("Pas assez de features numériques pour la matrice de corrélation") | |
| # ===== STATISTIQUES DESCRIPTIVES ===== | |
| with st.expander(" Statistiques Descriptives"): | |
| st.dataframe(df.describe(), use_container_width=True) | |
| # ===== PREVIEW DATA ===== | |
| with st.expander(" Inspecter le Dataset ML Final"): | |
| # Options de filtrage | |
| col_f1, col_f2 = st.columns(2) | |
| with col_f1: | |
| if 'service_code' in df.columns: | |
| services = ['Tous'] + sorted(df['service_code'].unique().tolist()) | |
| selected_service = st.selectbox("Filtrer par service", services) | |
| if selected_service != 'Tous': | |
| df_display = df[df['service_code'] == selected_service] | |
| else: | |
| df_display = df | |
| else: | |
| df_display = df | |
| with col_f2: | |
| n_rows = st.slider("Nombre de lignes à afficher", 10, 500, 100) | |
| st.dataframe(df_display.head(n_rows), use_container_width=True) | |
| # ===== TÉLÉCHARGEMENT ===== | |
| st.divider() | |
| st.subheader("💾 Export du Dataset ML") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| csv = df.to_csv(index=False).encode('utf-8') | |
| st.download_button( | |
| label="📥 Télécharger Dataset ML (.csv)", | |
| data=csv, | |
| file_name=f'hospital_ml_features_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv', | |
| mime='text/csv', | |
| help="Dataset complet avec toutes les features pour entraînement ML" | |
| ) | |
| with col2: | |
| # Export des métadonnées | |
| metadata = { | |
| 'nombre_lignes': len(df), | |
| 'nombre_features': len(df.columns), | |
| 'date_min': df['date'].min().strftime('%Y-%m-%d'), | |
| 'date_max': df['date'].max().strftime('%Y-%m-%d'), | |
| 'features_temporelles': len(feature_categories['Temporelles']), | |
| 'features_lags': len(feature_categories['Lags']), | |
| 'features_rolling': len(feature_categories['Rolling']), | |
| 'features_interaction': len(feature_categories['Interaction']), | |
| 'timestamp_creation': datetime.now().strftime('%Y-%m-%d %H:%M:%S') | |
| } | |
| metadata_df = pd.DataFrame([metadata]) | |
| metadata_csv = metadata_df.to_csv(index=False).encode('utf-8') | |
| st.download_button( | |
| label="📋 Télécharger Métadonnées (.csv)", | |
| data=metadata_csv, | |
| file_name=f'ml_metadata_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv', | |
| mime='text/csv', | |
| help="Informations sur le dataset généré" | |
| ) | |
| st.success("✅ Feature Engineering terminé ! Vous pouvez maintenant passer au module Forecasting.") |