Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| Application simplifiée d'analyse des adventices agricoles | |
| Compatible avec Python 3.13 - sans Gradio pour éviter les problèmes de dépendances | |
| """ | |
| import os | |
| import pandas as pd | |
| import numpy as np | |
| import matplotlib.pyplot as plt | |
| import seaborn as sns | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| from plotly.subplots import make_subplots | |
| import warnings | |
| from datasets import load_dataset | |
| from huggingface_hub import HfApi, hf_hub_download | |
| warnings.filterwarnings('ignore') | |
| # Configuration Hugging Face | |
| hf_token = os.environ.get("HF_TOKEN") | |
| dataset_id = "HackathonCRA/2024" | |
| # Configuration des graphiques | |
| plt.style.use('default') | |
| sns.set_palette("husl") | |
| class AgricultureAnalyzer: | |
| def __init__(self): | |
| self.df = None | |
| self.risk_analysis = None | |
| def load_data(self): | |
| """Charge les données du dataset Hugging Face""" | |
| print("🔄 Chargement des données depuis Hugging Face...") | |
| print(f"📋 Dataset ID: {dataset_id}") | |
| print(f"📋 Token disponible: {'Oui' if hf_token else 'Non'}") | |
| self.df = None | |
| # 1) Tentative de chargement direct via datasets.load_dataset | |
| try: | |
| dataset = load_dataset( | |
| dataset_id, | |
| split="train", | |
| token=hf_token, | |
| trust_remote_code=True, | |
| ) | |
| print(f"📊 Dataset chargé: {len(dataset)} exemples") | |
| try: | |
| self.df = dataset.to_pandas() | |
| print("✅ Conversion to_pandas() réussie") | |
| except Exception as pandas_error: | |
| print(f"❌ Erreur to_pandas(): {pandas_error}") | |
| print("🔄 Tentative de conversion manuelle...") | |
| data_list = [] | |
| for i, item in enumerate(dataset): | |
| data_list.append(item) | |
| if i < 5: | |
| print(f"📋 Exemple {i}: {list(item.keys())}") | |
| self.df = pd.DataFrame(data_list) | |
| print(f"✅ Conversion manuelle réussie: {len(self.df)} lignes") | |
| except Exception as e: | |
| print(f"❌ Erreur lors du chargement depuis Hugging Face: {str(e)}") | |
| print(f"❌ Type d'erreur: {type(e).__name__}") | |
| # 2) Fallback: récupérer directement les fichiers du repo (csv/parquet/tsv/json) | |
| fallback_msg = self._fallback_load_from_repo_files() | |
| if self.df is None: | |
| print(f"❌ Erreur lors du chargement du dataset : {str(e)} | Fallback: {fallback_msg}") | |
| return | |
| # Si on n'a toujours pas de dataframe, arrêter | |
| if self.df is None: | |
| print("❌ Impossible de charger les données") | |
| return | |
| print(f"📊 Données chargées: {len(self.df)} lignes") | |
| print(f"📊 Colonnes disponibles: {list(self.df.columns)}") | |
| # Nettoyage et validation | |
| required_columns = ["numparcell", "surfparc", "millesime"] | |
| missing_cols = [col for col in required_columns if col not in self.df.columns] | |
| if missing_cols: | |
| print(f"❌ Colonnes manquantes: {missing_cols}") | |
| self.df = None | |
| return | |
| # Nettoyage | |
| initial_len = len(self.df) | |
| self.df = self.df.dropna(subset=required_columns) | |
| print(f"📊 Avant nettoyage: {initial_len} lignes") | |
| print(f"📊 Après nettoyage: {len(self.df)} lignes") | |
| def _fallback_load_from_repo_files(self): | |
| """Fallback pour charger les données en téléchargeant directement les fichiers du repo HF.""" | |
| try: | |
| print("🔄 Tentative de chargement alternatif via fichiers du dépôt Hugging Face...") | |
| api = HfApi() | |
| files = api.list_repo_files(repo_id=dataset_id, repo_type="dataset", token=hf_token) | |
| if not files: | |
| print("❌ Aucun fichier dans le dépôt") | |
| return "Aucun fichier trouvé dans le dépôt." | |
| data_files = [ | |
| f for f in files if f.lower().endswith((".parquet", ".csv", ".tsv", ".json")) | |
| ] | |
| if not data_files: | |
| print("❌ Aucun fichier de données exploitable (csv/tsv/parquet/json)") | |
| return "Aucun fichier exploitable (csv/tsv/parquet/json)." | |
| # Priorité: parquet > csv > tsv > json | |
| for ext in [".parquet", ".csv", ".tsv", ".json"]: | |
| selected = [f for f in data_files if f.lower().endswith(ext)] | |
| if selected: | |
| chosen_ext = ext | |
| selected_files = selected | |
| break | |
| print(f"📂 Fichiers détectés ({chosen_ext}): {selected_files[:5]}{' ...' if len(selected_files) > 5 else ''}") | |
| local_paths = [] | |
| for f in selected_files: | |
| local_path = hf_hub_download( | |
| repo_id=dataset_id, | |
| repo_type="dataset", | |
| filename=f, | |
| token=hf_token, | |
| ) | |
| local_paths.append(local_path) | |
| frames = [] | |
| if chosen_ext == ".parquet": | |
| for p in local_paths: | |
| frames.append(pd.read_parquet(p)) | |
| elif chosen_ext == ".csv": | |
| for p in local_paths: | |
| frames.append(pd.read_csv(p)) | |
| elif chosen_ext == ".tsv": | |
| for p in local_paths: | |
| frames.append(pd.read_csv(p, sep="\t")) | |
| elif chosen_ext == ".json": | |
| for p in local_paths: | |
| try: | |
| frames.append(pd.read_json(p, lines=True)) | |
| except Exception: | |
| frames.append(pd.read_json(p)) | |
| self.df = pd.concat(frames, ignore_index=True) if len(frames) > 1 else frames[0] | |
| print(f"✅ Fallback réussi: {len(self.df)} lignes chargées depuis les fichiers du dépôt") | |
| return None | |
| except Exception as e: | |
| print(f"❌ Fallback échoué: {e}") | |
| # Dernier recours: fichier local d'exemple | |
| sample_path = os.path.join(os.path.dirname(__file__), "sample_data.csv") | |
| if os.path.exists(sample_path): | |
| try: | |
| self.df = pd.read_csv(sample_path) | |
| print(f"✅ Chargement du fichier local 'sample_data.csv' ({len(self.df)} lignes)") | |
| return "Chargement via fichier local de secours." | |
| except Exception as e2: | |
| print(f"❌ Échec du chargement du fichier local: {e2}") | |
| return f"Fallback échoué: {e}" | |
| def analyze_data(self): | |
| """Analyse des données et calcul des risques""" | |
| if self.df is None or len(self.df) == 0: | |
| print("❌ Pas de données à analyser") | |
| return None, None | |
| try: | |
| print(f"🔄 Début de l'analyse sur {len(self.df)} enregistrements...") | |
| # Analyse générale | |
| general_stats = { | |
| 'total_parcelles': self.df['numparcell'].nunique(), | |
| 'total_interventions': len(self.df), | |
| 'surface_totale': self.df['surfparc'].sum(), | |
| 'surface_moyenne': self.df['surfparc'].mean(), | |
| 'periode': f"{self.df['millesime'].min()} - {self.df['millesime'].max()}" | |
| } | |
| # Analyse des herbicides | |
| if 'familleprod' in self.df.columns: | |
| herbicides_df = self.df[self.df['familleprod'] == 'Herbicides'].copy() | |
| herbicide_stats = { | |
| 'nb_interventions_herbicides': len(herbicides_df), | |
| 'pourcentage_herbicides': (len(herbicides_df) / len(self.df)) * 100, | |
| 'parcelles_traitees': herbicides_df['numparcell'].nunique() | |
| } | |
| else: | |
| herbicide_stats = { | |
| 'nb_interventions_herbicides': 0, | |
| 'pourcentage_herbicides': 0, | |
| 'parcelles_traitees': 0 | |
| } | |
| # Calcul de l'analyse des risques | |
| self.calculate_risk_analysis() | |
| print("✅ Analyse terminée avec succès") | |
| return general_stats, herbicide_stats | |
| except Exception as e: | |
| print(f"❌ Erreur lors de l'analyse: {str(e)}") | |
| return None, None | |
| def calculate_risk_analysis(self): | |
| """Calcule l'analyse des risques par parcelle""" | |
| try: | |
| print("🔄 Calcul de l'analyse des risques...") | |
| # Vérifier les colonnes nécessaires | |
| required_group_cols = ['numparcell', 'surfparc'] | |
| optional_group_cols = ['nomparc', 'libelleusag'] | |
| # Construire la liste des colonnes de groupement disponibles | |
| group_cols = [col for col in required_group_cols if col in self.df.columns] | |
| group_cols.extend([col for col in optional_group_cols if col in self.df.columns]) | |
| if len(group_cols) < 2: | |
| print(f"❌ Colonnes insuffisantes pour le groupement: {group_cols}") | |
| self.risk_analysis = pd.DataFrame() | |
| return | |
| # Construire l'agrégation selon les colonnes disponibles | |
| agg_dict = {} | |
| if 'familleprod' in self.df.columns: | |
| agg_dict['familleprod'] = lambda x: (x == 'Herbicides').sum() | |
| if 'libevenem' in self.df.columns: | |
| agg_dict['libevenem'] = lambda x: len(x.unique()) | |
| if 'produit' in self.df.columns: | |
| agg_dict['produit'] = lambda x: len(x.unique()) | |
| if 'quantitetot' in self.df.columns: | |
| agg_dict['quantitetot'] = 'sum' | |
| if not agg_dict: | |
| print("❌ Aucune colonne disponible pour l'agrégation") | |
| self.risk_analysis = pd.DataFrame() | |
| return | |
| # Groupement des données par parcelle | |
| risk_analysis = self.df.groupby(group_cols).agg(agg_dict).round(2) | |
| # Quantités d'herbicides spécifiques (seulement si les colonnes existent) | |
| if 'familleprod' in self.df.columns and 'quantitetot' in self.df.columns: | |
| herbicides_df = self.df[self.df['familleprod'] == 'Herbicides'] | |
| if len(herbicides_df) > 0: | |
| herbicide_quantities = herbicides_df.groupby(group_cols)['quantitetot'].sum().fillna(0) | |
| risk_analysis['Quantite_herbicides'] = herbicide_quantities.reindex(risk_analysis.index, fill_value=0) | |
| else: | |
| risk_analysis['Quantite_herbicides'] = 0 | |
| else: | |
| risk_analysis['Quantite_herbicides'] = 0 | |
| # Renommer les colonnes de façon sécurisée | |
| new_column_names = {} | |
| if 'familleprod' in agg_dict: | |
| new_column_names['familleprod'] = 'Nb_herbicides' | |
| if 'libevenem' in agg_dict: | |
| new_column_names['libevenem'] = 'Diversite_evenements' | |
| if 'produit' in agg_dict: | |
| new_column_names['produit'] = 'Diversite_produits' | |
| if 'quantitetot' in agg_dict: | |
| new_column_names['quantitetot'] = 'Quantite_totale' | |
| risk_analysis = risk_analysis.rename(columns=new_column_names) | |
| # Calcul de l'IFT approximatif | |
| if 'surfparc' in group_cols: | |
| risk_analysis['IFT_herbicide_approx'] = (risk_analysis['Quantite_herbicides'] / | |
| risk_analysis.index.get_level_values('surfparc')).round(2) | |
| else: | |
| risk_analysis['IFT_herbicide_approx'] = 0 | |
| # Classification du risque | |
| def classify_risk(row): | |
| ift = row.get('IFT_herbicide_approx', 0) | |
| nb_herb = row.get('Nb_herbicides', 0) | |
| if ift == 0 and nb_herb == 0: | |
| return 'TRÈS FAIBLE' | |
| elif ift < 1 and nb_herb <= 1: | |
| return 'FAIBLE' | |
| elif ift < 3 and nb_herb <= 3: | |
| return 'MODÉRÉ' | |
| elif ift < 5 and nb_herb <= 5: | |
| return 'ÉLEVÉ' | |
| else: | |
| return 'TRÈS ÉLEVÉ' | |
| risk_analysis['Risque_adventice'] = risk_analysis.apply(classify_risk, axis=1) | |
| # Tri par risque | |
| risk_order = ['TRÈS FAIBLE', 'FAIBLE', 'MODÉRÉ', 'ÉLEVÉ', 'TRÈS ÉLEVÉ'] | |
| risk_analysis['Risk_Score'] = risk_analysis['Risque_adventice'].map({r: i for i, r in enumerate(risk_order)}) | |
| self.risk_analysis = risk_analysis.sort_values(['Risk_Score', 'IFT_herbicide_approx']) | |
| print(f"✅ Analyse des risques terminée: {len(self.risk_analysis)} parcelles analysées") | |
| except Exception as e: | |
| print(f"❌ Erreur lors du calcul des risques: {str(e)}") | |
| self.risk_analysis = pd.DataFrame() | |
| def print_summary_stats(self): | |
| """Affiche les statistiques de résumé""" | |
| if self.df is None: | |
| print("❌ Aucune donnée disponible") | |
| return | |
| print("\n" + "="*60) | |
| print("📊 STATISTIQUES GÉNÉRALES") | |
| print("="*60) | |
| print(f"• Nombre total de parcelles: {self.df['numparcell'].nunique()}") | |
| print(f"• Nombre d'interventions: {len(self.df):,}") | |
| print(f"• Surface totale: {self.df['surfparc'].sum():.2f} hectares") | |
| print(f"• Surface moyenne par parcelle: {self.df['surfparc'].mean():.2f} hectares") | |
| print(f"• Période: {self.df['millesime'].min()} - {self.df['millesime'].max()}") | |
| if 'familleprod' in self.df.columns: | |
| herbicides_df = self.df[self.df['familleprod'] == 'Herbicides'] | |
| if len(herbicides_df) > 0: | |
| print("\n🧪 ANALYSE HERBICIDES") | |
| print(f"• Interventions herbicides: {len(herbicides_df)} ({(len(herbicides_df)/len(self.df)*100):.1f}%)") | |
| print(f"• Parcelles traitées: {herbicides_df['numparcell'].nunique()}") | |
| if 'produit' in herbicides_df.columns: | |
| print(f"• Produits herbicides différents: {herbicides_df['produit'].nunique()}") | |
| if self.risk_analysis is not None and len(self.risk_analysis) > 0: | |
| risk_distribution = self.risk_analysis['Risque_adventice'].value_counts() | |
| print("\n🎯 RÉPARTITION DES RISQUES ADVENTICES") | |
| for risk_level in ['TRÈS FAIBLE', 'FAIBLE', 'MODÉRÉ', 'ÉLEVÉ', 'TRÈS ÉLEVÉ']: | |
| if risk_level in risk_distribution: | |
| count = risk_distribution[risk_level] | |
| pct = (count / len(self.risk_analysis)) * 100 | |
| print(f"• {risk_level}: {count} parcelles ({pct:.1f}%)") | |
| def save_results(self, output_dir="results"): | |
| """Sauvegarde les résultats dans des fichiers""" | |
| if not os.path.exists(output_dir): | |
| os.makedirs(output_dir) | |
| # Sauvegarder les données d'analyse | |
| if self.risk_analysis is not None and len(self.risk_analysis) > 0: | |
| risk_df = self.risk_analysis.reset_index() | |
| risk_df.to_csv(f"{output_dir}/risk_analysis.csv", index=False) | |
| print(f"✅ Analyse des risques sauvegardée dans {output_dir}/risk_analysis.csv") | |
| # Graphique de risque | |
| if len(risk_df) > 0: | |
| fig = px.scatter(risk_df, | |
| x='surfparc', | |
| y='IFT_herbicide_approx', | |
| color='Risque_adventice', | |
| size='Nb_herbicides' if 'Nb_herbicides' in risk_df.columns else None, | |
| color_discrete_map={ | |
| 'TRÈS FAIBLE': 'green', | |
| 'FAIBLE': 'lightgreen', | |
| 'MODÉRÉ': 'orange', | |
| 'ÉLEVÉ': 'red', | |
| 'TRÈS ÉLEVÉ': 'darkred' | |
| }, | |
| title="🎯 Analyse du Risque Adventice par Parcelle", | |
| labels={ | |
| 'surfparc': 'Surface de la parcelle (ha)', | |
| 'IFT_herbicide_approx': 'IFT Herbicide (approximatif)', | |
| 'Risque_adventice': 'Niveau de risque' | |
| }) | |
| fig.write_html(f"{output_dir}/risk_visualization.html") | |
| print(f"✅ Graphique de risque sauvegardé dans {output_dir}/risk_visualization.html") | |
| def main(): | |
| """Fonction principale""" | |
| print("🌾 Analyse des Adventices Agricoles - CRA Bretagne") | |
| print("=" * 60) | |
| # Initialisation de l'analyseur | |
| analyzer = AgricultureAnalyzer() | |
| # Chargement des données | |
| analyzer.load_data() | |
| if analyzer.df is None: | |
| print("❌ Impossible de continuer sans données") | |
| return | |
| # Analyse des données | |
| general_stats, herbicide_stats = analyzer.analyze_data() | |
| if general_stats is None: | |
| print("❌ Échec de l'analyse des données") | |
| return | |
| # Affichage des résultats | |
| analyzer.print_summary_stats() | |
| # Sauvegarde | |
| analyzer.save_results() | |
| print("\n" + "="*60) | |
| print("✅ Analyse terminée avec succès !") | |
| print("📁 Résultats sauvegardés dans le dossier 'results/'") | |
| print("🌐 Ouvrez 'results/risk_visualization.html' dans votre navigateur") | |
| if __name__ == "__main__": | |
| main() | |