Spaces:
Sleeping
Sleeping
| """ | |
| 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 | |