""" Module d'analyse avancée des herbicides Contient les fonctions de requête spécialisées pour l'analyse des herbicides """ import pandas as pd import plotly.express as px import plotly.graph_objects as go from plotly.subplots import make_subplots import numpy as np from datetime import datetime, timedelta class HerbicideAnalyzer: """Classe spécialisée dans l'analyse avancée des herbicides""" def __init__(self, data=None): self.df = data def set_data(self, data): """Définit les données à analyser""" self.df = data def get_top_ift_parcels_by_year(self, year, n_parcels=10): """ Retourne les N parcelles avec les IFT herbicides les plus élevés pour une année donnée Args: year (int): Année à analyser n_parcels (int): Nombre de parcelles à retourner (défaut: 10) Returns: tuple: (DataFrame des résultats, message de statut) """ try: if self.df is None or len(self.df) == 0: return None, "❌ Aucune donnée disponible" # Filtrer par année et herbicides year_data = self.df[ (self.df['millesime'] == year) & (self.df['familleprod'] == 'Herbicides') ].copy() if len(year_data) == 0: return None, f"❌ Aucune donnée d'herbicides pour l'année {year}" # Calculer l'IFT par parcelle if 'quantitetot' not in year_data.columns: return None, "❌ Colonne 'quantitetot' manquante pour le calcul de l'IFT" # Grouper par parcelle et calculer l'IFT approximatif group_cols = ['numparcell', 'surfparc'] if 'nomparc' in year_data.columns: group_cols.append('nomparc') if 'libelleusag' in year_data.columns: group_cols.append('libelleusag') ift_data = year_data.groupby(group_cols).agg({ 'quantitetot': 'sum', 'produit': 'count', 'produit': lambda x: len(x.unique()) # Nombre de produits différents }).reset_index() # Renommer les colonnes pour plus de clarté ift_data.columns = list(group_cols) + ['quantite_totale', 'nb_produits_uniques'] # Calculer l'IFT approximatif (quantité / surface) ift_data['IFT_herbicide'] = (ift_data['quantite_totale'] / ift_data['surfparc']).round(2) # Trier par IFT décroissant et prendre les N premiers top_parcels = ift_data.sort_values('IFT_herbicide', ascending=False).head(n_parcels) return top_parcels, f"✅ Top {len(top_parcels)} parcelles avec IFT herbicide le plus élevé en {year}" except Exception as e: return None, f"❌ Erreur lors du calcul des IFT: {str(e)}" def get_parcel_products_history(self, parcel_id, n_years=5): """ Retourne tous les produits utilisés sur une parcelle sur les N dernières années Args: parcel_id (str): Identifiant de la parcelle n_years (int): Nombre d'années à analyser (défaut: 5) Returns: tuple: (DataFrame des résultats, message de statut) """ try: if self.df is None or len(self.df) == 0: return None, "❌ Aucune donnée disponible" # Filtrer par parcelle et herbicides parcel_data = self.df[ (self.df['numparcell'] == parcel_id) & (self.df['familleprod'] == 'Herbicides') ].copy() if len(parcel_data) == 0: return None, f"❌ Aucune donnée d'herbicides pour la parcelle {parcel_id}" # Calculer les N dernières années disponibles max_year = parcel_data['millesime'].max() min_year = max_year - n_years + 1 recent_data = parcel_data[parcel_data['millesime'] >= min_year].copy() if len(recent_data) == 0: return None, f"❌ Aucune donnée pour la parcelle {parcel_id} sur les {n_years} dernières années" # Grouper par année et produit products_history = recent_data.groupby(['millesime', 'produit']).agg({ 'quantitetot': 'sum', 'datedebut': 'first', 'datefin': 'first' }).reset_index() # Trier par année et produit products_history = products_history.sort_values(['millesime', 'produit']) return products_history, f"✅ Historique des produits pour la parcelle {parcel_id} sur {n_years} ans" except Exception as e: return None, f"❌ Erreur lors de la récupération de l'historique: {str(e)}" def find_parcels_with_products(self, product_names, n_years=10): """ Trouve toutes les parcelles qui ont reçu des produits spécifiques sur les N dernières années Args: product_names (list): Liste des noms de produits à rechercher n_years (int): Nombre d'années à analyser (défaut: 10) Returns: tuple: (DataFrame des résultats, message de statut) """ try: if self.df is None or len(self.df) == 0: return None, "❌ Aucune donnée disponible" if not product_names or len(product_names) == 0: return None, "❌ Aucun nom de produit fourni" # Filtrer par herbicides et période max_year = self.df['millesime'].max() min_year = max_year - n_years + 1 herbicides_data = self.df[ (self.df['familleprod'] == 'Herbicides') & (self.df['millesime'] >= min_year) ].copy() if len(herbicides_data) == 0: return None, f"❌ Aucune donnée d'herbicides sur les {n_years} dernières années" # Recherche des produits (recherche insensible à la casse et partielle) pattern = '|'.join([f".*{name}.*" for name in product_names]) matching_data = herbicides_data[ herbicides_data['produit'].str.contains(pattern, na=False, regex=True, case=False) ].copy() if len(matching_data) == 0: return None, f"❌ Aucune parcelle trouvée avec les produits: {', '.join(product_names)}" # Grouper par parcelle et produit group_cols = ['numparcell', 'millesime', 'produit'] if 'nomparc' in matching_data.columns: group_cols.insert(1, 'nomparc') if 'surfparc' in matching_data.columns: group_cols.insert(-2, 'surfparc') results = matching_data.groupby(group_cols).agg({ 'quantitetot': 'sum', 'datedebut': 'first', 'datefin': 'first' }).reset_index() # Trier par parcelle et année results = results.sort_values(['numparcell', 'millesime']) return results, f"✅ {len(results)} utilisations trouvées pour les produits: {', '.join(product_names)}" except Exception as e: return None, f"❌ Erreur lors de la recherche de produits: {str(e)}" def find_parcels_without_products(self, product_names, n_years=10): """ Trouve toutes les parcelles qui N'ONT PAS reçu des produits spécifiques sur les N dernières années Args: product_names (list): Liste des noms de produits à exclure n_years (int): Nombre d'années à analyser (défaut: 10) Returns: tuple: (DataFrame des résultats, message de statut) """ try: if self.df is None or len(self.df) == 0: return None, "❌ Aucune donnée disponible" if not product_names or len(product_names) == 0: return None, "❌ Aucun nom de produit fourni" # Filtrer par herbicides et période max_year = self.df['millesime'].max() min_year = max_year - n_years + 1 recent_data = self.df[self.df['millesime'] >= min_year].copy() if len(recent_data) == 0: return None, f"❌ Aucune donnée sur les {n_years} dernières années" # Obtenir toutes les parcelles de la période all_parcels = recent_data[['numparcell']].drop_duplicates() if 'nomparc' in recent_data.columns: all_parcels = recent_data[['numparcell', 'nomparc']].drop_duplicates() if 'surfparc' in recent_data.columns: parcels_info = recent_data[['numparcell', 'nomparc', 'surfparc']].drop_duplicates() if 'nomparc' in recent_data.columns else recent_data[['numparcell', 'surfparc']].drop_duplicates() all_parcels = parcels_info # Trouver les parcelles qui ONT reçu les produits pattern = '|'.join([f".*{name}.*" for name in product_names]) herbicides_data = recent_data[recent_data['familleprod'] == 'Herbicides'] parcels_with_products = set() if len(herbicides_data) > 0: matching_data = herbicides_data[ herbicides_data['produit'].str.contains(pattern, na=False, regex=True, case=False) ] parcels_with_products = set(matching_data['numparcell'].unique()) # Parcelles qui N'ONT PAS reçu les produits all_parcel_ids = set(all_parcels['numparcell'].unique()) parcels_without = all_parcel_ids - parcels_with_products if len(parcels_without) == 0: return None, f"❌ Toutes les parcelles ont reçu au moins un des produits: {', '.join(product_names)}" # Créer le DataFrame des résultats results = all_parcels[all_parcels['numparcell'].isin(parcels_without)].copy() # Ajouter des informations sur l'usage if 'libelleusag' in recent_data.columns: usage_info = recent_data.groupby('numparcell')['libelleusag'].first().reset_index() results = results.merge(usage_info, on='numparcell', how='left') # Trier par numéro de parcelle results = results.sort_values('numparcell') return results, f"✅ {len(results)} parcelles trouvées sans les produits: {', '.join(product_names)}" except Exception as e: return None, f"❌ Erreur lors de la recherche de parcelles sans produits: {str(e)}" def analyze_intervention_periods(self, n_years=10): """ Analyse les périodes d'interventions herbicides par parcelle sur les N dernières années Args: n_years (int): Nombre d'années à analyser (défaut: 10) Returns: tuple: (DataFrame des résultats, message de statut) """ try: if self.df is None or len(self.df) == 0: return None, "❌ Aucune donnée disponible" # Filtrer par herbicides et période max_year = self.df['millesime'].max() min_year = max_year - n_years + 1 herbicides_data = self.df[ (self.df['familleprod'] == 'Herbicides') & (self.df['millesime'] >= min_year) ].copy() if len(herbicides_data) == 0: return None, f"❌ Aucune donnée d'herbicides sur les {n_years} dernières années" # Convertir les dates en format datetime herbicides_data['datedebut_parsed'] = pd.to_datetime( herbicides_data['datedebut'], format='%d/%m/%y', errors='coerce' ) # Filtrer les données avec des dates valides valid_dates = herbicides_data.dropna(subset=['datedebut_parsed']) if len(valid_dates) == 0: return None, "❌ Aucune date d'intervention valide trouvée" # Extraire le mois et analyser les patterns valid_dates['mois'] = valid_dates['datedebut_parsed'].dt.month valid_dates['mois_nom'] = valid_dates['datedebut_parsed'].dt.strftime('%B') # Grouper par parcelle et analyser les périodes group_cols = ['numparcell'] if 'nomparc' in valid_dates.columns: group_cols.append('nomparc') periods_analysis = valid_dates.groupby(group_cols).agg({ 'millesime': ['min', 'max', 'count'], 'mois': ['min', 'max'], 'mois_nom': lambda x: ', '.join(sorted(x.unique())), 'produit': 'nunique', 'quantitetot': 'sum' }).round(2) # Aplatir les colonnes multi-niveaux periods_analysis.columns = [ 'annee_debut', 'annee_fin', 'nb_interventions', 'mois_debut', 'mois_fin', 'mois_interventions', 'nb_produits_uniques', 'quantite_totale' ] periods_analysis = periods_analysis.reset_index() # Trier par nombre d'interventions décroissant periods_analysis = periods_analysis.sort_values('nb_interventions', ascending=False) return periods_analysis, f"✅ Analyse des périodes d'interventions pour {len(periods_analysis)} parcelles" except Exception as e: return None, f"❌ Erreur lors de l'analyse des périodes: {str(e)}" def create_ift_ranking_chart(self, year, n_parcels=10): """Crée un graphique des parcelles avec les IFT les plus élevés""" try: data, message = self.get_top_ift_parcels_by_year(year, n_parcels) if data is None or len(data) == 0: fig = px.bar(title=f"❌ {message}") fig.add_annotation(text=message, xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False) return fig # Créer le nom d'affichage pour les parcelles if 'nomparc' in data.columns: data['parcelle_display'] = data['numparcell'].astype(str) + ' (' + data['nomparc'].astype(str) + ')' else: data['parcelle_display'] = data['numparcell'].astype(str) fig = px.bar( data, x='IFT_herbicide', y='parcelle_display', orientation='h', title=f"🏆 Top {n_parcels} Parcelles - IFT Herbicide {year}", labels={ 'IFT_herbicide': 'IFT Herbicide', 'parcelle_display': 'Parcelle' }, hover_data=['quantite_totale', 'nb_produits_uniques', 'surfparc'] if 'surfparc' in data.columns else ['quantite_totale', 'nb_produits_uniques'] ) fig.update_layout( width=800, height=600, yaxis={'categoryorder': 'total ascending'} ) return fig except Exception as e: fig = px.bar(title=f"❌ Erreur lors de la création du graphique") fig.add_annotation(text=str(e)[:100], xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False) return fig def create_product_timeline_chart(self, parcel_id, n_years=5): """Crée un graphique chronologique des produits utilisés sur une parcelle""" try: data, message = self.get_parcel_products_history(parcel_id, n_years) if data is None or len(data) == 0: fig = px.timeline(title=f"❌ {message}") fig.add_annotation(text=message, xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False) return fig # Créer un graphique en barres empilées par année fig = px.bar( data, x='millesime', y='quantitetot', color='produit', title=f"📈 Historique des Herbicides - Parcelle {parcel_id}", labels={ 'millesime': 'Année', 'quantitetot': 'Quantité utilisée', 'produit': 'Produit' } ) fig.update_layout(width=800, height=500) return fig except Exception as e: fig = px.bar(title=f"❌ Erreur lors de la création du graphique") fig.add_annotation(text=str(e)[:100], xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False) return fig def create_intervention_periods_chart(self, n_years=10): """Crée un graphique des périodes d'interventions herbicides""" try: data, message = self.analyze_intervention_periods(n_years) if data is None or len(data) == 0: fig = px.scatter(title=f"❌ {message}") fig.add_annotation(text=message, xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False) return fig # Créer le nom d'affichage pour les parcelles if 'nomparc' in data.columns: data['parcelle_display'] = data['numparcell'].astype(str) + ' (' + data['nomparc'].astype(str) + ')' else: data['parcelle_display'] = data['numparcell'].astype(str) # Graphique scatter avec le nombre d'interventions vs période d'intervention fig = px.scatter( data.head(20), # Limiter à 20 parcelles pour la lisibilité x='mois_debut', y='nb_interventions', size='quantite_totale', color='nb_produits_uniques', hover_name='parcelle_display', title=f"🗓️ Périodes d'Interventions Herbicides (Top 20 parcelles)", labels={ 'mois_debut': 'Mois de début d\'intervention', 'nb_interventions': 'Nombre d\'interventions', 'quantite_totale': 'Quantité totale', 'nb_produits_uniques': 'Nb produits différents' } ) # Ajouter les noms des mois sur l'axe X fig.update_xaxis( tickvals=list(range(1, 13)), ticktext=['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun', 'Jul', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc'] ) fig.update_layout(width=800, height=600) return fig except Exception as e: fig = px.scatter(title=f"❌ Erreur lors de la création du graphique") fig.add_annotation(text=str(e)[:100], xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False) return fig def create_monthly_interventions_heatmap(self, n_years=10): """Crée une heatmap des interventions herbicides par mois et par année""" try: if self.df is None or len(self.df) == 0: fig = go.Figure() fig.add_annotation(text="❌ Aucune donnée disponible", x=0.5, y=0.5) return fig # Filtrer par herbicides et période max_year = self.df['millesime'].max() min_year = max_year - n_years + 1 herbicides_data = self.df[ (self.df['familleprod'] == 'Herbicides') & (self.df['millesime'] >= min_year) ].copy() if len(herbicides_data) == 0: fig = go.Figure() fig.add_annotation(text="❌ Aucune donnée d'herbicides", x=0.5, y=0.5) return fig # Convertir les dates et extraire le mois herbicides_data['datedebut_parsed'] = pd.to_datetime( herbicides_data['datedebut'], format='%d/%m/%y', errors='coerce' ) valid_dates = herbicides_data.dropna(subset=['datedebut_parsed']) if len(valid_dates) == 0: fig = go.Figure() fig.add_annotation(text="❌ Aucune date valide", x=0.5, y=0.5) return fig valid_dates['mois'] = valid_dates['datedebut_parsed'].dt.month # Créer la matrice mois x année heatmap_data = valid_dates.groupby(['millesime', 'mois']).size().reset_index() heatmap_data.columns = ['annee', 'mois', 'nb_interventions'] # Pivoter pour créer la matrice heatmap_matrix = heatmap_data.pivot(index='mois', columns='annee', values='nb_interventions').fillna(0) # Créer la heatmap fig = px.imshow( heatmap_matrix, title="🗓️ Heatmap des Interventions Herbicides par Mois et Année", labels={ 'x': 'Année', 'y': 'Mois', 'color': 'Nb interventions' }, aspect="auto" ) # Personnaliser les étiquettes des mois month_labels = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun', 'Jul', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc'] fig.update_yaxis( tickvals=list(range(1, 13)), ticktext=[month_labels[i-1] for i in range(1, 13) if i in heatmap_matrix.index] ) fig.update_layout(width=800, height=500) return fig except Exception as e: fig = go.Figure() fig.add_annotation(text=f"❌ Erreur: {str(e)[:100]}", x=0.5, y=0.5) return fig