import os os.environ["GRADIO_ANALYTICS_ENABLED"] = "False" import gradio as gr 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 import pandas as pd from huggingface_hub import HfApi import urllib.parse 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, file_path=None): """Charge les donnĂ©es agricoles UNIQUEMENT depuis Hugging Face""" try: print(f"đŸ€— Chargement du dataset Hugging Face: {dataset_id}") # Chargement du dataset avec configuration CSV robuste try: print(f"🔧 Tentative avec configuration CSV sĂ©curisĂ©e...") # Configuration pour forcer le chargement en string if hf_token: dataset = load_dataset( dataset_id, token=hf_token, data_files="*.csv", # Seulement les CSV sep=",", encoding="utf-8", dtype=str, # Force tout en string na_filter=False, # Pas de conversion NaN automatique keep_default_na=False # Pas de valeurs NA par dĂ©faut ) print(f"🔑 Chargement sĂ©curisĂ© avec token rĂ©ussi") else: dataset = load_dataset( dataset_id, data_files="*.csv", sep=",", encoding="utf-8", dtype=str, na_filter=False, keep_default_na=False ) print(f"🔑 Chargement sĂ©curisĂ© sans token rĂ©ussi") except Exception as parse_error: print(f"⚠ Erreur avec configuration sĂ©curisĂ©e: {str(parse_error)[:100]}...") print(f"🔄 Tentative de chargement standard...") # Fallback: chargement standard try: if hf_token: dataset = load_dataset(dataset_id, token=hf_token) print(f"🔑 Chargement standard avec token rĂ©ussi") else: dataset = load_dataset(dataset_id) print(f"🔑 Chargement standard sans token rĂ©ussi") except Exception as standard_error: print(f"⚠ Erreur de chargement standard: {str(standard_error)[:100]}...") print(f"🔄 Tentative avec chargement CSV manuel...") # Forcer tous les types en string pour Ă©viter les erreurs de parsing try: # Chargement avec configuration CSV personnalisĂ©e from datasets import DatasetDict import pandas as pd # Alternative: utiliser l'API HuggingFace pour lister les fichiers CSV try: api = HfApi(token=hf_token) all_files = api.list_repo_files(dataset_id, repo_type="dataset") # Filtrer pour ne garder que les CSV rĂ©cents csv_files = [f for f in all_files if f.endswith('.csv') and any(year in f for year in ['2020', '2021', '2022', '2023', '2024', '2025'])] csv_files.sort() # Trier par ordre alphabĂ©tique print(f"📁 Fichiers CSV dĂ©tectĂ©s: {len(csv_files)}") for f in csv_files: print(f" - {f}") except Exception as api_error: print(f"⚠ Erreur API HF: {api_error}") # Fallback avec noms corrects csv_files = [ "Interventions-(sortie-excel)-Station_ExpĂ©rimentale_de_KerguĂ©hennec-2020.csv", "Interventions-(sortie-excel)-Station_ExpĂ©rimentale_de_KerguĂ©hennec-2021.csv", "Interventions-(sortie-excel)-Station_ExpĂ©rimentale_de_KerguĂ©hennec-2022.csv", "Interventions-(sortie-excel)-Station_ExpĂ©rimentale_de_KerguĂ©hennec-2023.csv", "Interventions-(sortie-excel)-Station_ExpĂ©rimentale_de_KerguĂ©hennec-2024.csv", "Interventions-(sortie-excel)-Station_ExpĂ©rimentale_de_KerguĂ©hennec-2025.csv" ] print(f"📊 Chargement alternatif: fichiers CSV individuels...") # Charger chaque fichier avec pandas et concatĂ©ner all_dataframes = [] for csv_file in csv_files: try: # URL directe vers le fichier avec encodage URL correct encoded_filename = urllib.parse.quote(csv_file, safe='-()_.') file_url = f"https://huggingface.co/datasets/{dataset_id}/resolve/main/{encoded_filename}" print(f" ⚙ Chargement: {csv_file}") # Charger avec pandas en forçant tout en string et encodage UTF-8 df_temp = pd.read_csv(file_url, dtype=str, na_filter=False, encoding='utf-8') df_temp['source_file'] = csv_file # Ajouter la source all_dataframes.append(df_temp) print(f" ✅ SuccĂšs: {len(df_temp)} lignes") except Exception as file_error: print(f" ⚠ Erreur pour {csv_file}: {str(file_error)[:100]}...") # Essayer avec un autre encodage try: print(f" 🔄 Tentative avec encodage latin-1...") df_temp = pd.read_csv(file_url, dtype=str, na_filter=False, encoding='latin-1') df_temp['source_file'] = csv_file all_dataframes.append(df_temp) print(f" ✅ SuccĂšs avec latin-1: {len(df_temp)} lignes") except Exception as second_error: print(f" ❌ Échec dĂ©finitif: {str(second_error)[:50]}...") continue if all_dataframes: # ConcatĂ©ner tous les DataFrames df_combined = pd.concat(all_dataframes, ignore_index=True) print(f"✅ Chargement alternatif rĂ©ussi: {len(df_combined)} lignes") # Convertir en format Dataset from datasets import Dataset dataset = DatasetDict({ 'train': Dataset.from_pandas(df_combined) }) else: raise Exception("Aucun fichier CSV n'a pu ĂȘtre chargĂ©") except Exception as alt_error: print(f"❌ Échec du chargement alternatif: {str(alt_error)[:100]}...") raise parse_error # Relancer l'erreur originale available_splits = list(dataset.keys()) print(f"📊 Splits disponibles: {available_splits}") # DĂ©terminer quel split utiliser split_to_use = None if 'train' in available_splits: split_to_use = 'train' elif len(available_splits) > 0: split_to_use = available_splits[0] # Prendre le premier split disponible else: raise Exception("Aucun split trouvĂ© dans le dataset") print(f"🎯 Utilisation du split: '{split_to_use}'") # Convertir en DataFrame pandas df_raw = dataset[split_to_use].to_pandas() print(f"✅ Dataset chargĂ©: {len(df_raw)} lignes, {len(df_raw.columns)} colonnes") # Afficher quelques colonnes pour debug print(f"đŸ·ïž Colonnes: {list(df_raw.columns)[:10]}{'...' if len(df_raw.columns) > 10 else ''}") # Filtrer pour exclure les fichiers XLSX # VĂ©rifier les colonnes 'file' ou 'source_file' file_column = None if 'file' in df_raw.columns: file_column = 'file' elif 'source_file' in df_raw.columns: file_column = 'source_file' if file_column: print(f"📁 Types de fichiers dĂ©tectĂ©s: {df_raw[file_column].unique()[:5]}") # Ne garder que les fichiers CSV (exclure XLSX) csv_mask = df_raw[file_column].str.endswith('.csv', na=False) csv_data = df_raw[csv_mask] print(f"📊 Avant filtrage CSV: {len(df_raw)} lignes") if len(csv_data) > 0: df_raw = csv_data print(f"đŸ—‚ïž AprĂšs filtrage CSV: {len(df_raw)} lignes restantes") else: print(f"⚠ Aucun fichier CSV trouvĂ© dans la colonne '{file_column}', conservation de toutes les donnĂ©es") else: print(f"⚠ Pas de colonne de fichier dĂ©tectĂ©e, on garde toutes les donnĂ©es") # Filtrer par annĂ©e si disponible if 'millesime' in df_raw.columns: # Convertir la colonne millesime en numĂ©rique si elle est en string try: df_raw['millesime'] = pd.to_numeric(df_raw['millesime'], errors='coerce') # Supprimer les lignes avec millesime invalide df_raw = df_raw.dropna(subset=['millesime']) df_raw['millesime'] = df_raw['millesime'].astype(int) except Exception as e: print(f"⚠ ProblĂšme conversion millesime: {e}") years = sorted(df_raw['millesime'].unique()) print(f"📅 AnnĂ©es disponibles: {years}") # Prendre les donnĂ©es rĂ©centes (2020+) recent_data = df_raw[df_raw['millesime'] >= 2020] if len(recent_data) > 0: self.df = recent_data print(f"✅ DonnĂ©es filtrĂ©es (2020+): {len(self.df)} lignes") else: self.df = df_raw print(f"✅ Toutes les donnĂ©es utilisĂ©es: {len(self.df)} lignes") else: self.df = df_raw print(f"✅ DonnĂ©es chargĂ©es: {len(self.df)} lignes (pas de colonne millesime)") if len(self.df) == 0: raise Exception("Aucune donnĂ©e disponible aprĂšs filtrage") return self.analyze_data() except Exception as e: print(f"❌ ERREUR lors du chargement du dataset HuggingFace:") print(f" {str(e)[:200]}...") print(f"💡 Solutions:") print(f" 1. VĂ©rifiez l'URL: https://huggingface.co/datasets/{dataset_id}") print(f" 2. Configurez votre token: export HF_TOKEN='votre_token'") print(f" 3. VĂ©rifiez vos permissions d'accĂšs") print(f" 4. ProblĂšme de parsing: donnĂ©es avec types incohĂ©rents") raise Exception(f"Dataset HuggingFace requis: {dataset_id} - Erreur: {str(e)[:100]}...") def create_sample_data(self): """MĂ©thode dĂ©sactivĂ©e - utilisation exclusive du dataset HF""" raise Exception("Cette application nĂ©cessite le dataset HuggingFace HackathonCRA/2024") def analyze_data(self): """Analyse des donnĂ©es et calcul des risques""" if self.df is None: return "Erreur: Aucune donnĂ©e chargĂ©e" # 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 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() } # Calcul de l'analyse des risques self.calculate_risk_analysis() return general_stats, herbicide_stats def calculate_risk_analysis(self): """Calcule l'analyse des risques par parcelle""" # Groupement des donnĂ©es par parcelle risk_analysis = self.df.groupby(['numparcell', 'nomparc', 'libelleusag', 'surfparc']).agg({ 'familleprod': lambda x: (x == 'Herbicides').sum(), # Nb traitements herbicides 'libevenem': lambda x: len(x.unique()), # DiversitĂ© des Ă©vĂ©nements 'produit': lambda x: len(x.unique()), # DiversitĂ© des produits 'quantitetot': 'sum' # QuantitĂ© totale }).round(2) # QuantitĂ©s d'herbicides spĂ©cifiques herbicide_quantities = self.df[self.df['familleprod'] == 'Herbicides'].groupby( ['numparcell', 'nomparc', 'libelleusag', 'surfparc'])['quantitetot'].sum().fillna(0) risk_analysis['Quantite_herbicides'] = herbicide_quantities.reindex(risk_analysis.index, fill_value=0) risk_analysis.columns = ['Nb_herbicides', 'Diversite_evenements', 'Diversite_produits', 'Quantite_totale', 'Quantite_herbicides'] # Calcul de l'IFT approximatif risk_analysis['IFT_herbicide_approx'] = (risk_analysis['Quantite_herbicides'] / risk_analysis.index.get_level_values('surfparc')).round(2) # Classification du risque def classify_risk(row): ift = row['IFT_herbicide_approx'] nb_herb = row['Nb_herbicides'] 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']) def get_summary_stats(self): """Retourne les statistiques de rĂ©sumĂ©""" if self.df is None: return "Aucune donnĂ©e disponible" stats_text = f""" ## 📊 Statistiques GĂ©nĂ©rales - **Nombre total de parcelles**: {self.df['numparcell'].nunique()} - **Nombre d'interventions**: {len(self.df):,} - **Surface totale**: {self.df['surfparc'].sum():.2f} hectares - **Surface moyenne par parcelle**: {self.df['surfparc'].mean():.2f} hectares - **PĂ©riode**: {self.df['millesime'].min()} - {self.df['millesime'].max()} ## đŸ§Ș Analyse Herbicides """ herbicides_df = self.df[self.df['familleprod'] == 'Herbicides'] if len(herbicides_df) > 0: stats_text += f""" - **Interventions herbicides**: {len(herbicides_df)} ({(len(herbicides_df)/len(self.df)*100):.1f}%) - **Parcelles traitĂ©es**: {herbicides_df['numparcell'].nunique()} - **Produits herbicides diffĂ©rents**: {herbicides_df['produit'].nunique()} """ if self.risk_analysis is not None: risk_distribution = self.risk_analysis['Risque_adventice'].value_counts() stats_text += f""" ## 🎯 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 stats_text += f"- **{risk_level}**: {count} parcelles ({pct:.1f}%)\n" return stats_text def get_low_risk_recommendations(self): """Retourne les recommandations pour les parcelles Ă  faible risque""" if self.risk_analysis is None: return "Analyse des risques non disponible" low_risk = self.risk_analysis[ self.risk_analysis['Risque_adventice'].isin(['TRÈS FAIBLE', 'FAIBLE']) ].head(10) recommendations = "## đŸŒŸ TOP 10 - Parcelles RecommandĂ©es pour Cultures Sensibles (Pois, Haricot)\n\n" for idx, row in low_risk.iterrows(): parcelle, nom, culture, surface = idx recommendations += f""" **Parcelle {parcelle}** ({nom}) - Culture actuelle: {culture} - Surface: {surface:.2f} ha - Niveau de risque: {row['Risque_adventice']} - IFT herbicide: {row['IFT_herbicide_approx']:.2f} - Nombre d'herbicides: {row['Nb_herbicides']} --- """ return recommendations def create_risk_visualization(self): """CrĂ©e la visualisation des risques""" if self.risk_analysis is None: return None risk_df = self.risk_analysis.reset_index() fig = px.scatter(risk_df, x='surfparc', y='IFT_herbicide_approx', color='Risque_adventice', size='Nb_herbicides', hover_data=['nomparc', 'libelleusag'], 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.update_layout(width=800, height=600, title_font_size=16) return fig def create_culture_analysis(self): """Analyse par type de culture""" if self.df is None: return None culture_counts = self.df['libelleusag'].value_counts() fig = px.pie(values=culture_counts.values, names=culture_counts.index, title="đŸŒ± RĂ©partition des Cultures") fig.update_layout(width=700, height=500) return fig def create_risk_distribution(self): """Distribution des niveaux de risque""" if self.risk_analysis is None: return None risk_counts = self.risk_analysis['Risque_adventice'].value_counts() fig = px.bar(x=risk_counts.index, y=risk_counts.values, color=risk_counts.index, color_discrete_map={ 'TRÈS FAIBLE': 'green', 'FAIBLE': 'lightgreen', 'MODÉRÉ': 'orange', 'ÉLEVÉ': 'red', 'TRÈS ÉLEVÉ': 'darkred' }, title="📊 Distribution des Niveaux de Risque Adventice", labels={'x': 'Niveau de risque', 'y': 'Nombre de parcelles'}) fig.update_layout(width=700, height=500, showlegend=False) return fig # Initialisation de l'analyseur analyzer = AgricultureAnalyzer() analyzer.load_data() # Interface Gradio def create_interface(): with gr.Blocks(title="đŸŒŸ Analyse Adventices Agricoles CRA", theme=gr.themes.Soft()) as demo: gr.Markdown(""" # đŸŒŸ Analyse des Adventices Agricoles - CRA Bretagne **Objectif**: Anticiper et rĂ©duire la pression des adventices dans les parcelles agricoles bretonnes Cette application analyse les donnĂ©es historiques pour identifier les parcelles les plus adaptĂ©es Ă  la culture de plantes sensibles comme le pois ou le haricot. """) with gr.Tabs(): with gr.TabItem("📊 Vue d'ensemble"): gr.Markdown("## Statistiques gĂ©nĂ©rales des donnĂ©es agricoles") stats_output = gr.Markdown(analyzer.get_summary_stats()) with gr.Row(): culture_plot = gr.Plot(analyzer.create_culture_analysis()) risk_dist_plot = gr.Plot(analyzer.create_risk_distribution()) with gr.TabItem("🎯 Analyse des Risques"): gr.Markdown("## Cartographie des risques adventices par parcelle") risk_plot = gr.Plot(analyzer.create_risk_visualization()) gr.Markdown(""" **InterprĂ©tation du graphique**: - **Axe X**: Surface de la parcelle (hectares) - **Axe Y**: IFT Herbicide approximatif - **Couleur**: Niveau de risque adventice - **Taille**: Nombre d'herbicides utilisĂ©s Les parcelles vertes (risque faible) sont idĂ©ales pour les cultures sensibles. """) with gr.TabItem("đŸŒŸ Recommandations"): gr.Markdown(analyzer.get_low_risk_recommendations()) gr.Markdown(""" ## 💡 Conseils pour la gestion des adventices ### Parcelles Ă  TrĂšs Faible Risque (Vertes) - ✅ **IdĂ©ales pour pois et haricot** - ✅ Historique d'usage herbicide minimal - ✅ Pression adventice faible attendue ### Parcelles Ă  Faible Risque (Vert clair) - ⚠ Surveillance lĂ©gĂšre recommandĂ©e - ✅ Conviennent aux cultures sensibles avec prĂ©cautions ### Parcelles Ă  Risque ModĂ©rĂ©/ÉlevĂ© (Orange/Rouge) - ❌ Éviter pour cultures sensibles - 🔍 Rotation nĂ©cessaire avant implantation - 📈 Surveillance renforcĂ©e des adventices ### StratĂ©gies alternatives - **Rotation longue**: 3-4 ans avant cultures sensibles - **Cultures intermĂ©diaires**: CIPAN pour rĂ©duire la pression - **Techniques mĂ©caniques**: Hersage, binage - **Biostimulants**: Renforcement naturel des cultures """) with gr.TabItem("â„č À propos"): gr.Markdown(""" ## 🎯 MĂ©thodologie Cette analyse se base sur : ### Calcul de l'IFT (Indice de FrĂ©quence de Traitement) - **IFT ≈ QuantitĂ© appliquĂ©e / Surface de parcelle** - Indicateur de l'intensitĂ© des traitements herbicides ### Classification des risques - **TRÈS FAIBLE**: IFT = 0, aucun herbicide - **FAIBLE**: IFT < 1, usage minimal - **MODÉRÉ**: IFT < 3, usage modĂ©rĂ© - **ÉLEVÉ**: IFT < 5, usage important - **TRÈS ÉLEVÉ**: IFT ≄ 5, usage intensif ### DonnĂ©es analysĂ©es - **Source**: Station ExpĂ©rimentale de KerguĂ©hennec - **PĂ©riode**: Campagne 2025 - **Variables**: Interventions, produits, quantitĂ©s, surfaces --- **DĂ©veloppĂ© pour le Hackathon CRA Bretagne** 🏆 *Application d'aide Ă  la dĂ©cision pour une agriculture durable* """) # Bouton de rafraĂźchissement refresh_btn = gr.Button("🔄 Actualiser les donnĂ©es", variant="secondary") def refresh_data(): analyzer.load_data() return ( analyzer.get_summary_stats(), analyzer.create_culture_analysis(), analyzer.create_risk_distribution(), analyzer.create_risk_visualization(), analyzer.get_low_risk_recommendations() ) refresh_btn.click( refresh_data, outputs=[stats_output, culture_plot, risk_dist_plot, risk_plot] ) return demo # Lancement de l'application if __name__ == "__main__": demo = create_interface() # Configuration pour Hugging Face Spaces demo.launch( server_name="0.0.0.0", server_port=7860, share=False # Pas besoin de share sur HF Spaces )