Tracy André commited on
Commit
f084836
·
1 Parent(s): 48479a2
Files changed (4) hide show
  1. .gitignore +4 -1
  2. GUIDE_REQUETES_HERBICIDES.md +177 -0
  3. app.py +300 -0
  4. herbicide_analyzer.py +520 -0
.gitignore CHANGED
@@ -1,3 +1,6 @@
1
  .env
2
  __pycache__/
3
- sample_data/
 
 
 
 
1
  .env
2
  __pycache__/
3
+ sample_data/
4
+ .venv/
5
+ mcp/
6
+ mcp-example/
GUIDE_REQUETES_HERBICIDES.md ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🔍 Guide des Requêtes Herbicides
2
+
3
+ ## Nouvelles Fonctionnalités Intégrées
4
+
5
+ L'application d'analyse des adventices agricoles a été enrichie avec un système de requêtes avancées pour l'analyse des herbicides. Voici comment utiliser ces nouvelles fonctionnalités :
6
+
7
+ ## 📋 Accès aux Fonctionnalités
8
+
9
+ Dans l'interface Gradio, vous trouverez un nouvel onglet **"🔍 Requêtes Herbicides"** qui contient 4 sous-onglets spécialisés :
10
+
11
+ ---
12
+
13
+ ## 🏆 Top IFT par Année
14
+
15
+ **Objectif :** Identifier les parcelles avec les Indices de Fréquence de Traitement (IFT) herbicides les plus élevés pour une année donnée.
16
+
17
+ ### Utilisation :
18
+ 1. Sélectionnez l'année à analyser dans la liste déroulante
19
+ 2. Choisissez le nombre de parcelles à afficher (5 à 50)
20
+ 3. Cliquez sur "🔍 Analyser les IFT"
21
+
22
+ ### Résultats :
23
+ - **Graphique en barres** : Classement visuel des parcelles par IFT décroissant
24
+ - **Tableau détaillé** : Numéro de parcelle, surface, IFT, quantité totale, nombre de produits
25
+
26
+ ### Cas d'usage :
27
+ - **Surveillance prioritaire** : Identifier les parcelles nécessitant une attention particulière
28
+ - **Benchmarking** : Comparer les pratiques entre parcelles
29
+ - **Audit réglementaire** : Suivre les parcelles à forte intensité de traitement
30
+
31
+ ---
32
+
33
+ ## 📖 Historique Parcelle
34
+
35
+ **Objectif :** Tracer l'historique complet des herbicides utilisés sur une parcelle spécifique.
36
+
37
+ ### Utilisation :
38
+ 1. Saisissez le numéro de la parcelle (ex: 21, 22, etc.)
39
+ 2. Définissez le nombre d'années à analyser (1 à 15)
40
+ 3. Cliquez sur "🔍 Analyser l'historique"
41
+
42
+ ### Résultats :
43
+ - **Graphique chronologique** : Évolution des produits et quantités par année
44
+ - **Tableau historique** : Détail par année avec produits, quantités et dates
45
+
46
+ ### Cas d'usage :
47
+ - **Planification de rotation** : Voir l'historique avant d'implanter une culture sensible
48
+ - **Traçabilité** : Documenter les pratiques passées
49
+ - **Analyse tendances** : Observer l'évolution des pratiques sur une parcelle
50
+
51
+ ---
52
+
53
+ ## 🎯 Recherche de Produits
54
+
55
+ **Objectif :** Trouver les parcelles ayant reçu (ou n'ayant PAS reçu) des produits spécifiques.
56
+
57
+ ### Utilisation :
58
+ 1. Saisissez les noms de produits séparés par des virgules
59
+ - Exemple : `Minarex, Chardex, Aligator`
60
+ - Supporte la recherche partielle (ex: "Chardex" trouve "Chardex 500")
61
+ 2. Définissez la période d'analyse (1 à 15 années)
62
+ 3. Choisissez votre type de recherche :
63
+ - **"✅ Parcelles AVEC ces produits"** : Trouve les parcelles qui ont reçu au moins un des produits
64
+ - **"❌ Parcelles SANS ces produits"** : Trouve les parcelles qui n'ont reçu aucun de ces produits
65
+
66
+ ### Résultats :
67
+ - **Message de statut** : Nombre de parcelles trouvées
68
+ - **Tableau des résultats** : Liste des parcelles avec détails (années, quantités, dates)
69
+
70
+ ### Cas d'usage :
71
+ - **Rotation Chardex** : `Chardex` → Trouver les parcelles sans Chardex pour cultures sensibles
72
+ - **Évitement de résidus** : Identifier les parcelles sans produits persistants
73
+ - **Conformité réglementaire** : Tracer l'usage de produits spécifiques
74
+
75
+ ### Exemples de Requêtes Utiles :
76
+
77
+ #### Pour cultures sensibles (pois, haricot) :
78
+ ```
79
+ Recherche "SANS" : Chardex, Minarex
80
+ → Trouve les parcelles adaptées aux légumineuses
81
+ ```
82
+
83
+ #### Pour audit d'un produit :
84
+ ```
85
+ Recherche "AVEC" : Aligator
86
+ → Trace tous les usages d'Aligator
87
+ ```
88
+
89
+ #### Pour éviter les résistances :
90
+ ```
91
+ Recherche "AVEC" : Glyphosate, Round
92
+ → Identifie les parcelles à forte pression glyphosate
93
+ ```
94
+
95
+ ---
96
+
97
+ ## 🗓️ Périodes d'Intervention
98
+
99
+ **Objectif :** Analyser les patterns temporels d'application des herbicides par parcelle.
100
+
101
+ ### Utilisation :
102
+ 1. Définissez la période d'analyse (1 à 15 années)
103
+ 2. Cliquez sur "🔍 Analyser les périodes"
104
+
105
+ ### Résultats :
106
+ - **Graphique scatter** : Position des interventions dans l'année vs intensité
107
+ - X = Mois de début d'intervention
108
+ - Y = Nombre d'interventions
109
+ - Taille = Quantité totale utilisée
110
+ - Couleur = Nombre de produits différents
111
+
112
+ - **Heatmap** : Répartition mensuelle des interventions par année
113
+ - Visualise les pics d'activité saisonniers
114
+ - Compare l'évolution temporelle entre années
115
+
116
+ - **Tableau détaillé** : Analyse par parcelle avec :
117
+ - Période d'activité (année début/fin)
118
+ - Nombre total d'interventions
119
+ - Mois de début/fin des traitements
120
+ - Liste des mois d'intervention
121
+ - Nombre de produits uniques
122
+ - Quantité totale
123
+
124
+ ### Cas d'usage :
125
+ - **Optimisation temporelle** : Identifier les meilleures fenêtres d'intervention
126
+ - **Planification cultural** : Éviter les conflits avec les périodes de traitement
127
+ - **Analyse climatique** : Corréler les interventions avec les conditions météo
128
+ - **Benchmarking temporel** : Comparer les strategies temporelles entre parcelles
129
+
130
+ ---
131
+
132
+ ## 💡 Conseils d'Utilisation
133
+
134
+ ### Recherche de Produits Efficace :
135
+ - Utilisez des **noms partiels** : "Chardex" trouve "Chardex 500", "Chardex WG", etc.
136
+ - **Combinez plusieurs produits** : "Chardex, Minarex" pour une recherche large
137
+ - **Soyez spécifique** : "Round" vs "Roundup" selon vos besoins
138
+
139
+ ### Interprétation des IFT :
140
+ - **IFT < 1** : Usage faible, parcelles intéressantes pour cultures sensibles
141
+ - **IFT 1-3** : Usage modéré, surveillance recommandée
142
+ - **IFT > 3** : Usage intensif, rotation conseillée avant cultures sensibles
143
+
144
+ ### Analyse Temporelle :
145
+ - **Mars-Avril** : Période classique de désherbage de printemps
146
+ - **Septembre-Octobre** : Traitements d'automne (colza, céréales)
147
+ - **Heatmap** : Rouge = forte activité, Bleu = période calme
148
+
149
+ ---
150
+
151
+ ## 🔧 Support Technique
152
+
153
+ - **Données manquantes** : Les requêtes s'adaptent automatiquement aux colonnes disponibles
154
+ - **Recherche insensible à la casse** : "chardex" = "CHARDEX"
155
+ - **Gestion des erreurs** : Messages explicites en cas de problème
156
+ - **Performance** : Optimisé pour des datasets de plusieurs milliers de lignes
157
+
158
+ ---
159
+
160
+ ## 📞 Questions Fréquentes
161
+
162
+ **Q: Que faire si aucune parcelle n'est trouvée ?**
163
+ R: Vérifiez l'orthographe des produits et élargissez la période de recherche.
164
+
165
+ **Q: Comment interpréter un IFT élevé ?**
166
+ R: IFT élevé = forte pression herbicide. Considérez une rotation ou des techniques alternatives.
167
+
168
+ **Q: Puis-je rechercher plusieurs produits à la fois ?**
169
+ R: Oui, séparez-les par des virgules. La recherche trouve les parcelles avec AU MOINS UN des produits.
170
+
171
+ **Q: Comment identifier les parcelles pour pois/haricot ?**
172
+ R: Recherchez les parcelles "SANS" Chardex sur 10 ans, puis vérifiez leur historique.
173
+
174
+ ---
175
+
176
+ *Développé pour le Hackathon CRA Bretagne 🏆*
177
+ *Application d'aide à la décision pour une agriculture durable*
app.py CHANGED
@@ -16,6 +16,7 @@ from datasets import load_dataset
16
  import pandas as pd
17
  from huggingface_hub import HfApi, hf_hub_download
18
  import urllib.parse
 
19
  warnings.filterwarnings('ignore')
20
 
21
  # Configuration Hugging Face
@@ -644,6 +645,9 @@ analyzer = AgricultureAnalyzer()
644
  analyzer.load_data()
645
  analyzer.analyze_data() # Analyse des données après chargement
646
 
 
 
 
647
  # Interface Gradio
648
  def create_interface():
649
  with gr.Blocks(title="🌾 Analyse Adventices Agricoles CRA", theme=gr.themes.Soft()) as demo:
@@ -708,6 +712,264 @@ def create_interface():
708
  - **Biostimulants**: Renforcement naturel des cultures
709
  """)
710
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
711
  with gr.TabItem("ℹ️ À propos"):
712
  gr.Markdown("""
713
  ## 🎯 Méthodologie
@@ -730,6 +992,34 @@ def create_interface():
730
  - **Période**: Campagne 2025
731
  - **Variables**: Interventions, produits, quantités, surfaces
732
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
733
  ---
734
 
735
  **Développé pour le Hackathon CRA Bretagne** 🏆
@@ -743,6 +1033,16 @@ def create_interface():
743
  def refresh_data():
744
  analyzer.load_data()
745
  analyzer.analyze_data() # Recalculer l'analyse après rechargement
 
 
 
 
 
 
 
 
 
 
746
  return (
747
  analyzer.get_summary_stats(),
748
  analyzer.create_culture_analysis(),
 
16
  import pandas as pd
17
  from huggingface_hub import HfApi, hf_hub_download
18
  import urllib.parse
19
+ from herbicide_analyzer import HerbicideAnalyzer
20
  warnings.filterwarnings('ignore')
21
 
22
  # Configuration Hugging Face
 
645
  analyzer.load_data()
646
  analyzer.analyze_data() # Analyse des données après chargement
647
 
648
+ # Initialisation de l'analyseur d'herbicides
649
+ herbicide_analyzer = HerbicideAnalyzer(analyzer.df)
650
+
651
  # Interface Gradio
652
  def create_interface():
653
  with gr.Blocks(title="🌾 Analyse Adventices Agricoles CRA", theme=gr.themes.Soft()) as demo:
 
712
  - **Biostimulants**: Renforcement naturel des cultures
713
  """)
714
 
715
+ with gr.TabItem("📅 Analyse par Année"):
716
+ gr.Markdown("## Visualisation des données par année")
717
+
718
+ # Liste déroulante pour sélectionner l'année
719
+ available_years = analyzer.get_available_years()
720
+ if available_years:
721
+ default_year = available_years[-1] # Dernière année par défaut
722
+ year_dropdown = gr.Dropdown(
723
+ choices=available_years,
724
+ value=default_year,
725
+ label="🗓️ Sélectionner une année",
726
+ info="Choisissez l'année à analyser dans la liste déroulante"
727
+ )
728
+ else:
729
+ year_dropdown = gr.Dropdown(
730
+ choices=[],
731
+ value=None,
732
+ label="🗓️ Sélectionner une année",
733
+ info="Aucune année disponible"
734
+ )
735
+
736
+ # Statistiques pour l'année sélectionnée
737
+ year_stats = gr.Markdown("")
738
+
739
+ # Graphiques pour l'année sélectionnée
740
+ with gr.Row():
741
+ year_culture_plot = gr.Plot()
742
+ year_timeline_plot = gr.Plot()
743
+
744
+ with gr.Row():
745
+ year_herbicide_plot = gr.Plot()
746
+
747
+ # Fonction de mise à jour quand l'année change
748
+ def update_year_analysis(selected_year):
749
+ if selected_year is None:
750
+ empty_fig = px.bar(title="❌ Aucune année sélectionnée")
751
+ return (
752
+ "❌ Veuillez sélectionner une année",
753
+ empty_fig, empty_fig, empty_fig
754
+ )
755
+
756
+ stats = analyzer.get_year_summary_stats(selected_year)
757
+ culture_fig = analyzer.create_year_culture_analysis(selected_year)
758
+ timeline_fig = analyzer.create_year_interventions_timeline(selected_year)
759
+ herbicide_fig = analyzer.create_year_herbicide_analysis(selected_year)
760
+
761
+ return stats, culture_fig, timeline_fig, herbicide_fig
762
+
763
+ # Connecter la liste déroulante aux mises à jour
764
+ year_dropdown.change(
765
+ update_year_analysis,
766
+ inputs=[year_dropdown],
767
+ outputs=[year_stats, year_culture_plot, year_timeline_plot, year_herbicide_plot]
768
+ )
769
+
770
+ # Initialiser avec l'année par défaut
771
+ if available_years:
772
+ initial_stats, initial_culture, initial_timeline, initial_herbicide = update_year_analysis(default_year)
773
+ year_stats.value = initial_stats
774
+ year_culture_plot.value = initial_culture
775
+ year_timeline_plot.value = initial_timeline
776
+ year_herbicide_plot.value = initial_herbicide
777
+
778
+ gr.Markdown("""
779
+ **Informations sur cet onglet:**
780
+ - 📊 **Statistiques**: Résumé pour l'année sélectionnée
781
+ - 🌱 **Cultures**: Répartition des types de cultures
782
+ - 📅 **Timeline**: Répartition temporelle des interventions
783
+ - 🧪 **Herbicides**: Top 10 des herbicides utilisés
784
+ """)
785
+
786
+ with gr.TabItem("🔍 Requêtes Herbicides"):
787
+ gr.Markdown("## Requêtes avancées pour l'analyse des herbicides")
788
+
789
+ with gr.Tabs():
790
+ with gr.TabItem("🏆 Top IFT par Année"):
791
+ gr.Markdown("### Parcelles avec les IFT herbicides les plus élevés")
792
+
793
+ with gr.Row():
794
+ with gr.Column():
795
+ year_select_ift = gr.Dropdown(
796
+ choices=analyzer.get_available_years(),
797
+ value=analyzer.get_available_years()[-1] if analyzer.get_available_years() else None,
798
+ label="📅 Année à analyser"
799
+ )
800
+ n_parcels_ift = gr.Slider(
801
+ minimum=5,
802
+ maximum=50,
803
+ value=10,
804
+ step=5,
805
+ label="🔢 Nombre de parcelles à afficher"
806
+ )
807
+ btn_ift = gr.Button("🔍 Analyser les IFT", variant="primary")
808
+
809
+ with gr.Column():
810
+ ift_chart = gr.Plot()
811
+
812
+ ift_table = gr.Dataframe(
813
+ headers=["Parcelle", "Surface", "IFT Herbicide", "Quantité Totale", "Nb Produits"],
814
+ label="📊 Détail des parcelles avec IFT élevé"
815
+ )
816
+
817
+ def analyze_ift(year, n_parcels):
818
+ if year is None:
819
+ return None, "❌ Veuillez sélectionner une année"
820
+
821
+ data, message = herbicide_analyzer.get_top_ift_parcels_by_year(year, n_parcels)
822
+ chart = herbicide_analyzer.create_ift_ranking_chart(year, n_parcels)
823
+
824
+ return chart, data
825
+
826
+ btn_ift.click(
827
+ analyze_ift,
828
+ inputs=[year_select_ift, n_parcels_ift],
829
+ outputs=[ift_chart, ift_table]
830
+ )
831
+
832
+ with gr.TabItem("📖 Historique Parcelle"):
833
+ gr.Markdown("### Produits utilisés sur une parcelle spécifique")
834
+
835
+ with gr.Row():
836
+ with gr.Column():
837
+ parcel_id_input = gr.Textbox(
838
+ label="🏠 Numéro de parcelle",
839
+ placeholder="Ex: 21, 22, etc."
840
+ )
841
+ n_years_history = gr.Slider(
842
+ minimum=1,
843
+ maximum=15,
844
+ value=5,
845
+ step=1,
846
+ label="📅 Nombre d'années à analyser"
847
+ )
848
+ btn_history = gr.Button("🔍 Analyser l'historique", variant="primary")
849
+
850
+ with gr.Column():
851
+ history_chart = gr.Plot()
852
+
853
+ history_table = gr.Dataframe(
854
+ headers=["Année", "Produit", "Quantité", "Date début", "Date fin"],
855
+ label="📊 Historique des produits utilisés"
856
+ )
857
+
858
+ def analyze_parcel_history(parcel_id, n_years):
859
+ if not parcel_id or parcel_id.strip() == "":
860
+ return None, "❌ Veuillez entrer un numéro de parcelle"
861
+
862
+ data, message = herbicide_analyzer.get_parcel_products_history(parcel_id.strip(), n_years)
863
+ chart = herbicide_analyzer.create_product_timeline_chart(parcel_id.strip(), n_years)
864
+
865
+ return chart, data
866
+
867
+ btn_history.click(
868
+ analyze_parcel_history,
869
+ inputs=[parcel_id_input, n_years_history],
870
+ outputs=[history_chart, history_table]
871
+ )
872
+
873
+ with gr.TabItem("🎯 Recherche de Produits"):
874
+ gr.Markdown("### Parcelles ayant reçu des produits spécifiques")
875
+
876
+ with gr.Row():
877
+ with gr.Column():
878
+ products_input = gr.Textbox(
879
+ label="🧪 Noms de produits (séparés par des virgules)",
880
+ placeholder="Ex: Minarex, Chardex, Aligator",
881
+ lines=2
882
+ )
883
+ n_years_search = gr.Slider(
884
+ minimum=1,
885
+ maximum=15,
886
+ value=10,
887
+ step=1,
888
+ label="📅 Période d'analyse (années)"
889
+ )
890
+ with gr.Row():
891
+ btn_with_products = gr.Button("✅ Parcelles AVEC ces produits", variant="primary")
892
+ btn_without_products = gr.Button("❌ Parcelles SANS ces produits", variant="secondary")
893
+
894
+ search_results_table = gr.Dataframe(
895
+ label="📊 Résultats de la recherche"
896
+ )
897
+ search_message = gr.Markdown("")
898
+
899
+ def search_with_products(products_text, n_years):
900
+ if not products_text or products_text.strip() == "":
901
+ return "❌ Veuillez entrer au moins un nom de produit", None
902
+
903
+ products_list = [p.strip() for p in products_text.split(",") if p.strip()]
904
+ data, message = herbicide_analyzer.find_parcels_with_products(products_list, n_years)
905
+
906
+ return message, data
907
+
908
+ def search_without_products(products_text, n_years):
909
+ if not products_text or products_text.strip() == "":
910
+ return "❌ Veuillez entrer au moins un nom de produit", None
911
+
912
+ products_list = [p.strip() for p in products_text.split(",") if p.strip()]
913
+ data, message = herbicide_analyzer.find_parcels_without_products(products_list, n_years)
914
+
915
+ return message, data
916
+
917
+ btn_with_products.click(
918
+ search_with_products,
919
+ inputs=[products_input, n_years_search],
920
+ outputs=[search_message, search_results_table]
921
+ )
922
+
923
+ btn_without_products.click(
924
+ search_without_products,
925
+ inputs=[products_input, n_years_search],
926
+ outputs=[search_message, search_results_table]
927
+ )
928
+
929
+ with gr.TabItem("🗓️ Périodes d'Intervention"):
930
+ gr.Markdown("### Analyse des périodes d'interventions herbicides")
931
+
932
+ with gr.Row():
933
+ with gr.Column():
934
+ n_years_periods = gr.Slider(
935
+ minimum=1,
936
+ maximum=15,
937
+ value=10,
938
+ step=1,
939
+ label="📅 Période d'analyse (années)"
940
+ )
941
+ btn_periods = gr.Button("🔍 Analyser les périodes", variant="primary")
942
+
943
+ with gr.Column():
944
+ periods_chart = gr.Plot()
945
+
946
+ with gr.Row():
947
+ heatmap_chart = gr.Plot()
948
+
949
+ periods_table = gr.Dataframe(
950
+ label="📊 Analyse des périodes par parcelle"
951
+ )
952
+
953
+ def analyze_periods(n_years):
954
+ data, message = herbicide_analyzer.analyze_intervention_periods(n_years)
955
+ chart = herbicide_analyzer.create_intervention_periods_chart(n_years)
956
+ heatmap = herbicide_analyzer.create_monthly_interventions_heatmap(n_years)
957
+
958
+ return chart, heatmap, data
959
+
960
+ btn_periods.click(
961
+ analyze_periods,
962
+ inputs=[n_years_periods],
963
+ outputs=[periods_chart, heatmap_chart, periods_table]
964
+ )
965
+
966
+ gr.Markdown("""
967
+ **Guide d'interprétation :**
968
+ - 🎯 **Graphique scatter** : Position des interventions dans l'année vs intensité
969
+ - 🔥 **Heatmap** : Répartition mensuelle des interventions par année
970
+ - 📊 **Tableau** : Détail par parcelle avec périodes et quantités
971
+ """)
972
+
973
  with gr.TabItem("ℹ️ À propos"):
974
  gr.Markdown("""
975
  ## 🎯 Méthodologie
 
992
  - **Période**: Campagne 2025
993
  - **Variables**: Interventions, produits, quantités, surfaces
994
 
995
+ ## 🔍 Nouvelles Fonctionnalités - Requêtes Herbicides
996
+
997
+ ### 🏆 Top IFT par Année
998
+ - Identifie les parcelles avec les IFT herbicides les plus élevés
999
+ - Aide à cibler les parcelles à surveiller prioritairement
1000
+ - Graphique de classement et tableau détaillé
1001
+
1002
+ ### 📖 Historique Parcelle
1003
+ - Trace l'historique complet des herbicides sur une parcelle
1004
+ - Permet de voir l'évolution des pratiques
1005
+ - Graphique chronologique des utilisations
1006
+
1007
+ ### 🎯 Recherche de Produits
1008
+ - **Recherche positive**: Trouve les parcelles ayant reçu des produits spécifiques
1009
+ - **Recherche négative**: Identifie les parcelles n'ayant PAS reçu certains produits
1010
+ - Supporte la recherche partielle (ex: "Chardex" trouve "Chardex 500")
1011
+
1012
+ ### 🗓️ Périodes d'Intervention
1013
+ - Analyse les patterns temporels d'application des herbicides
1014
+ - Graphique scatter des périodes vs intensité
1015
+ - Heatmap mensuelle/annuelle des interventions
1016
+
1017
+ ### 💡 Exemples d'utilisation
1018
+ - **Rotation des cultures**: Identifier les parcelles sans Chardex pour y planter des cultures sensibles
1019
+ - **Suivi réglementaire**: Tracer l'usage de produits spécifiques
1020
+ - **Optimisation temporelle**: Analyser les meilleures périodes d'intervention
1021
+ - **Benchmarking**: Comparer les IFT entre parcelles similaires
1022
+
1023
  ---
1024
 
1025
  **Développé pour le Hackathon CRA Bretagne** 🏆
 
1033
  def refresh_data():
1034
  analyzer.load_data()
1035
  analyzer.analyze_data() # Recalculer l'analyse après rechargement
1036
+
1037
+ # Mettre à jour l'analyseur d'herbicides avec les nouvelles données
1038
+ herbicide_analyzer.set_data(analyzer.df)
1039
+
1040
+ # Mettre à jour la liste des années disponibles
1041
+ updated_years = analyzer.get_available_years()
1042
+ year_dropdown.choices = updated_years
1043
+ if updated_years:
1044
+ year_dropdown.value = updated_years[-1]
1045
+
1046
  return (
1047
  analyzer.get_summary_stats(),
1048
  analyzer.create_culture_analysis(),
herbicide_analyzer.py ADDED
@@ -0,0 +1,520 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module d'analyse avancée des herbicides
3
+ Contient les fonctions de requête spécialisées pour l'analyse des herbicides
4
+ """
5
+ import pandas as pd
6
+ import plotly.express as px
7
+ import plotly.graph_objects as go
8
+ from plotly.subplots import make_subplots
9
+ import numpy as np
10
+ from datetime import datetime, timedelta
11
+
12
+
13
+ class HerbicideAnalyzer:
14
+ """Classe spécialisée dans l'analyse avancée des herbicides"""
15
+
16
+ def __init__(self, data=None):
17
+ self.df = data
18
+
19
+ def set_data(self, data):
20
+ """Définit les données à analyser"""
21
+ self.df = data
22
+
23
+ def get_top_ift_parcels_by_year(self, year, n_parcels=10):
24
+ """
25
+ Retourne les N parcelles avec les IFT herbicides les plus élevés pour une année donnée
26
+
27
+ Args:
28
+ year (int): Année à analyser
29
+ n_parcels (int): Nombre de parcelles à retourner (défaut: 10)
30
+
31
+ Returns:
32
+ tuple: (DataFrame des résultats, message de statut)
33
+ """
34
+ try:
35
+ if self.df is None or len(self.df) == 0:
36
+ return None, "❌ Aucune donnée disponible"
37
+
38
+ # Filtrer par année et herbicides
39
+ year_data = self.df[
40
+ (self.df['millesime'] == year) &
41
+ (self.df['familleprod'] == 'Herbicides')
42
+ ].copy()
43
+
44
+ if len(year_data) == 0:
45
+ return None, f"❌ Aucune donnée d'herbicides pour l'année {year}"
46
+
47
+ # Calculer l'IFT par parcelle
48
+ if 'quantitetot' not in year_data.columns:
49
+ return None, "❌ Colonne 'quantitetot' manquante pour le calcul de l'IFT"
50
+
51
+ # Grouper par parcelle et calculer l'IFT approximatif
52
+ group_cols = ['numparcell', 'surfparc']
53
+ if 'nomparc' in year_data.columns:
54
+ group_cols.append('nomparc')
55
+ if 'libelleusag' in year_data.columns:
56
+ group_cols.append('libelleusag')
57
+
58
+ ift_data = year_data.groupby(group_cols).agg({
59
+ 'quantitetot': 'sum',
60
+ 'produit': 'count',
61
+ 'produit': lambda x: len(x.unique()) # Nombre de produits différents
62
+ }).reset_index()
63
+
64
+ # Renommer les colonnes pour plus de clarté
65
+ ift_data.columns = list(group_cols) + ['quantite_totale', 'nb_produits_uniques']
66
+
67
+ # Calculer l'IFT approximatif (quantité / surface)
68
+ ift_data['IFT_herbicide'] = (ift_data['quantite_totale'] / ift_data['surfparc']).round(2)
69
+
70
+ # Trier par IFT décroissant et prendre les N premiers
71
+ top_parcels = ift_data.sort_values('IFT_herbicide', ascending=False).head(n_parcels)
72
+
73
+ return top_parcels, f"✅ Top {len(top_parcels)} parcelles avec IFT herbicide le plus élevé en {year}"
74
+
75
+ except Exception as e:
76
+ return None, f"❌ Erreur lors du calcul des IFT: {str(e)}"
77
+
78
+ def get_parcel_products_history(self, parcel_id, n_years=5):
79
+ """
80
+ Retourne tous les produits utilisés sur une parcelle sur les N dernières années
81
+
82
+ Args:
83
+ parcel_id (str): Identifiant de la parcelle
84
+ n_years (int): Nombre d'années à analyser (défaut: 5)
85
+
86
+ Returns:
87
+ tuple: (DataFrame des résultats, message de statut)
88
+ """
89
+ try:
90
+ if self.df is None or len(self.df) == 0:
91
+ return None, "❌ Aucune donnée disponible"
92
+
93
+ # Filtrer par parcelle et herbicides
94
+ parcel_data = self.df[
95
+ (self.df['numparcell'] == parcel_id) &
96
+ (self.df['familleprod'] == 'Herbicides')
97
+ ].copy()
98
+
99
+ if len(parcel_data) == 0:
100
+ return None, f"❌ Aucune donnée d'herbicides pour la parcelle {parcel_id}"
101
+
102
+ # Calculer les N dernières années disponibles
103
+ max_year = parcel_data['millesime'].max()
104
+ min_year = max_year - n_years + 1
105
+
106
+ recent_data = parcel_data[parcel_data['millesime'] >= min_year].copy()
107
+
108
+ if len(recent_data) == 0:
109
+ return None, f"❌ Aucune donnée pour la parcelle {parcel_id} sur les {n_years} dernières années"
110
+
111
+ # Grouper par année et produit
112
+ products_history = recent_data.groupby(['millesime', 'produit']).agg({
113
+ 'quantitetot': 'sum',
114
+ 'datedebut': 'first',
115
+ 'datefin': 'first'
116
+ }).reset_index()
117
+
118
+ # Trier par année et produit
119
+ products_history = products_history.sort_values(['millesime', 'produit'])
120
+
121
+ return products_history, f"✅ Historique des produits pour la parcelle {parcel_id} sur {n_years} ans"
122
+
123
+ except Exception as e:
124
+ return None, f"❌ Erreur lors de la récupération de l'historique: {str(e)}"
125
+
126
+ def find_parcels_with_products(self, product_names, n_years=10):
127
+ """
128
+ Trouve toutes les parcelles qui ont reçu des produits spécifiques sur les N dernières années
129
+
130
+ Args:
131
+ product_names (list): Liste des noms de produits à rechercher
132
+ n_years (int): Nombre d'années à analyser (défaut: 10)
133
+
134
+ Returns:
135
+ tuple: (DataFrame des résultats, message de statut)
136
+ """
137
+ try:
138
+ if self.df is None or len(self.df) == 0:
139
+ return None, "❌ Aucune donnée disponible"
140
+
141
+ if not product_names or len(product_names) == 0:
142
+ return None, "❌ Aucun nom de produit fourni"
143
+
144
+ # Filtrer par herbicides et période
145
+ max_year = self.df['millesime'].max()
146
+ min_year = max_year - n_years + 1
147
+
148
+ herbicides_data = self.df[
149
+ (self.df['familleprod'] == 'Herbicides') &
150
+ (self.df['millesime'] >= min_year)
151
+ ].copy()
152
+
153
+ if len(herbicides_data) == 0:
154
+ return None, f"❌ Aucune donnée d'herbicides sur les {n_years} dernières années"
155
+
156
+ # Recherche des produits (recherche insensible à la casse et partielle)
157
+ pattern = '|'.join([f".*{name}.*" for name in product_names])
158
+ matching_data = herbicides_data[
159
+ herbicides_data['produit'].str.contains(pattern, na=False, regex=True, case=False)
160
+ ].copy()
161
+
162
+ if len(matching_data) == 0:
163
+ return None, f"❌ Aucune parcelle trouvée avec les produits: {', '.join(product_names)}"
164
+
165
+ # Grouper par parcelle et produit
166
+ group_cols = ['numparcell', 'millesime', 'produit']
167
+ if 'nomparc' in matching_data.columns:
168
+ group_cols.insert(1, 'nomparc')
169
+ if 'surfparc' in matching_data.columns:
170
+ group_cols.insert(-2, 'surfparc')
171
+
172
+ results = matching_data.groupby(group_cols).agg({
173
+ 'quantitetot': 'sum',
174
+ 'datedebut': 'first',
175
+ 'datefin': 'first'
176
+ }).reset_index()
177
+
178
+ # Trier par parcelle et année
179
+ results = results.sort_values(['numparcell', 'millesime'])
180
+
181
+ return results, f"✅ {len(results)} utilisations trouvées pour les produits: {', '.join(product_names)}"
182
+
183
+ except Exception as e:
184
+ return None, f"❌ Erreur lors de la recherche de produits: {str(e)}"
185
+
186
+ def find_parcels_without_products(self, product_names, n_years=10):
187
+ """
188
+ Trouve toutes les parcelles qui N'ONT PAS reçu des produits spécifiques sur les N dernières années
189
+
190
+ Args:
191
+ product_names (list): Liste des noms de produits à exclure
192
+ n_years (int): Nombre d'années à analyser (défaut: 10)
193
+
194
+ Returns:
195
+ tuple: (DataFrame des résultats, message de statut)
196
+ """
197
+ try:
198
+ if self.df is None or len(self.df) == 0:
199
+ return None, "❌ Aucune donnée disponible"
200
+
201
+ if not product_names or len(product_names) == 0:
202
+ return None, "❌ Aucun nom de produit fourni"
203
+
204
+ # Filtrer par herbicides et période
205
+ max_year = self.df['millesime'].max()
206
+ min_year = max_year - n_years + 1
207
+
208
+ recent_data = self.df[self.df['millesime'] >= min_year].copy()
209
+
210
+ if len(recent_data) == 0:
211
+ return None, f"❌ Aucune donnée sur les {n_years} dernières années"
212
+
213
+ # Obtenir toutes les parcelles de la période
214
+ all_parcels = recent_data[['numparcell']].drop_duplicates()
215
+ if 'nomparc' in recent_data.columns:
216
+ all_parcels = recent_data[['numparcell', 'nomparc']].drop_duplicates()
217
+ if 'surfparc' in recent_data.columns:
218
+ parcels_info = recent_data[['numparcell', 'nomparc', 'surfparc']].drop_duplicates() if 'nomparc' in recent_data.columns else recent_data[['numparcell', 'surfparc']].drop_duplicates()
219
+ all_parcels = parcels_info
220
+
221
+ # Trouver les parcelles qui ONT reçu les produits
222
+ pattern = '|'.join([f".*{name}.*" for name in product_names])
223
+ herbicides_data = recent_data[recent_data['familleprod'] == 'Herbicides']
224
+
225
+ parcels_with_products = set()
226
+ if len(herbicides_data) > 0:
227
+ matching_data = herbicides_data[
228
+ herbicides_data['produit'].str.contains(pattern, na=False, regex=True, case=False)
229
+ ]
230
+ parcels_with_products = set(matching_data['numparcell'].unique())
231
+
232
+ # Parcelles qui N'ONT PAS reçu les produits
233
+ all_parcel_ids = set(all_parcels['numparcell'].unique())
234
+ parcels_without = all_parcel_ids - parcels_with_products
235
+
236
+ if len(parcels_without) == 0:
237
+ return None, f"❌ Toutes les parcelles ont reçu au moins un des produits: {', '.join(product_names)}"
238
+
239
+ # Créer le DataFrame des résultats
240
+ results = all_parcels[all_parcels['numparcell'].isin(parcels_without)].copy()
241
+
242
+ # Ajouter des informations sur l'usage
243
+ if 'libelleusag' in recent_data.columns:
244
+ usage_info = recent_data.groupby('numparcell')['libelleusag'].first().reset_index()
245
+ results = results.merge(usage_info, on='numparcell', how='left')
246
+
247
+ # Trier par numéro de parcelle
248
+ results = results.sort_values('numparcell')
249
+
250
+ return results, f"✅ {len(results)} parcelles trouvées sans les produits: {', '.join(product_names)}"
251
+
252
+ except Exception as e:
253
+ return None, f"❌ Erreur lors de la recherche de parcelles sans produits: {str(e)}"
254
+
255
+ def analyze_intervention_periods(self, n_years=10):
256
+ """
257
+ Analyse les périodes d'interventions herbicides par parcelle sur les N dernières années
258
+
259
+ Args:
260
+ n_years (int): Nombre d'années à analyser (défaut: 10)
261
+
262
+ Returns:
263
+ tuple: (DataFrame des résultats, message de statut)
264
+ """
265
+ try:
266
+ if self.df is None or len(self.df) == 0:
267
+ return None, "❌ Aucune donnée disponible"
268
+
269
+ # Filtrer par herbicides et période
270
+ max_year = self.df['millesime'].max()
271
+ min_year = max_year - n_years + 1
272
+
273
+ herbicides_data = self.df[
274
+ (self.df['familleprod'] == 'Herbicides') &
275
+ (self.df['millesime'] >= min_year)
276
+ ].copy()
277
+
278
+ if len(herbicides_data) == 0:
279
+ return None, f"❌ Aucune donnée d'herbicides sur les {n_years} dernières années"
280
+
281
+ # Convertir les dates en format datetime
282
+ herbicides_data['datedebut_parsed'] = pd.to_datetime(
283
+ herbicides_data['datedebut'],
284
+ format='%d/%m/%y',
285
+ errors='coerce'
286
+ )
287
+
288
+ # Filtrer les données avec des dates valides
289
+ valid_dates = herbicides_data.dropna(subset=['datedebut_parsed'])
290
+
291
+ if len(valid_dates) == 0:
292
+ return None, "❌ Aucune date d'intervention valide trouvée"
293
+
294
+ # Extraire le mois et analyser les patterns
295
+ valid_dates['mois'] = valid_dates['datedebut_parsed'].dt.month
296
+ valid_dates['mois_nom'] = valid_dates['datedebut_parsed'].dt.strftime('%B')
297
+
298
+ # Grouper par parcelle et analyser les périodes
299
+ group_cols = ['numparcell']
300
+ if 'nomparc' in valid_dates.columns:
301
+ group_cols.append('nomparc')
302
+
303
+ periods_analysis = valid_dates.groupby(group_cols).agg({
304
+ 'millesime': ['min', 'max', 'count'],
305
+ 'mois': ['min', 'max'],
306
+ 'mois_nom': lambda x: ', '.join(sorted(x.unique())),
307
+ 'produit': 'nunique',
308
+ 'quantitetot': 'sum'
309
+ }).round(2)
310
+
311
+ # Aplatir les colonnes multi-niveaux
312
+ periods_analysis.columns = [
313
+ 'annee_debut', 'annee_fin', 'nb_interventions',
314
+ 'mois_debut', 'mois_fin', 'mois_interventions',
315
+ 'nb_produits_uniques', 'quantite_totale'
316
+ ]
317
+
318
+ periods_analysis = periods_analysis.reset_index()
319
+
320
+ # Trier par nombre d'interventions décroissant
321
+ periods_analysis = periods_analysis.sort_values('nb_interventions', ascending=False)
322
+
323
+ return periods_analysis, f"✅ Analyse des périodes d'interventions pour {len(periods_analysis)} parcelles"
324
+
325
+ except Exception as e:
326
+ return None, f"❌ Erreur lors de l'analyse des périodes: {str(e)}"
327
+
328
+ def create_ift_ranking_chart(self, year, n_parcels=10):
329
+ """Crée un graphique des parcelles avec les IFT les plus élevés"""
330
+ try:
331
+ data, message = self.get_top_ift_parcels_by_year(year, n_parcels)
332
+
333
+ if data is None or len(data) == 0:
334
+ fig = px.bar(title=f"❌ {message}")
335
+ fig.add_annotation(text=message, xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False)
336
+ return fig
337
+
338
+ # Créer le nom d'affichage pour les parcelles
339
+ if 'nomparc' in data.columns:
340
+ data['parcelle_display'] = data['numparcell'].astype(str) + ' (' + data['nomparc'].astype(str) + ')'
341
+ else:
342
+ data['parcelle_display'] = data['numparcell'].astype(str)
343
+
344
+ fig = px.bar(
345
+ data,
346
+ x='IFT_herbicide',
347
+ y='parcelle_display',
348
+ orientation='h',
349
+ title=f"🏆 Top {n_parcels} Parcelles - IFT Herbicide {year}",
350
+ labels={
351
+ 'IFT_herbicide': 'IFT Herbicide',
352
+ 'parcelle_display': 'Parcelle'
353
+ },
354
+ hover_data=['quantite_totale', 'nb_produits_uniques', 'surfparc'] if 'surfparc' in data.columns else ['quantite_totale', 'nb_produits_uniques']
355
+ )
356
+
357
+ fig.update_layout(
358
+ width=800,
359
+ height=600,
360
+ yaxis={'categoryorder': 'total ascending'}
361
+ )
362
+
363
+ return fig
364
+
365
+ except Exception as e:
366
+ fig = px.bar(title=f"❌ Erreur lors de la création du graphique")
367
+ fig.add_annotation(text=str(e)[:100], xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False)
368
+ return fig
369
+
370
+ def create_product_timeline_chart(self, parcel_id, n_years=5):
371
+ """Crée un graphique chronologique des produits utilisés sur une parcelle"""
372
+ try:
373
+ data, message = self.get_parcel_products_history(parcel_id, n_years)
374
+
375
+ if data is None or len(data) == 0:
376
+ fig = px.timeline(title=f"❌ {message}")
377
+ fig.add_annotation(text=message, xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False)
378
+ return fig
379
+
380
+ # Créer un graphique en barres empilées par année
381
+ fig = px.bar(
382
+ data,
383
+ x='millesime',
384
+ y='quantitetot',
385
+ color='produit',
386
+ title=f"📈 Historique des Herbicides - Parcelle {parcel_id}",
387
+ labels={
388
+ 'millesime': 'Année',
389
+ 'quantitetot': 'Quantité utilisée',
390
+ 'produit': 'Produit'
391
+ }
392
+ )
393
+
394
+ fig.update_layout(width=800, height=500)
395
+ return fig
396
+
397
+ except Exception as e:
398
+ fig = px.bar(title=f"❌ Erreur lors de la création du graphique")
399
+ fig.add_annotation(text=str(e)[:100], xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False)
400
+ return fig
401
+
402
+ def create_intervention_periods_chart(self, n_years=10):
403
+ """Crée un graphique des périodes d'interventions herbicides"""
404
+ try:
405
+ data, message = self.analyze_intervention_periods(n_years)
406
+
407
+ if data is None or len(data) == 0:
408
+ fig = px.scatter(title=f"❌ {message}")
409
+ fig.add_annotation(text=message, xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False)
410
+ return fig
411
+
412
+ # Créer le nom d'affichage pour les parcelles
413
+ if 'nomparc' in data.columns:
414
+ data['parcelle_display'] = data['numparcell'].astype(str) + ' (' + data['nomparc'].astype(str) + ')'
415
+ else:
416
+ data['parcelle_display'] = data['numparcell'].astype(str)
417
+
418
+ # Graphique scatter avec le nombre d'interventions vs période d'intervention
419
+ fig = px.scatter(
420
+ data.head(20), # Limiter à 20 parcelles pour la lisibilité
421
+ x='mois_debut',
422
+ y='nb_interventions',
423
+ size='quantite_totale',
424
+ color='nb_produits_uniques',
425
+ hover_name='parcelle_display',
426
+ title=f"🗓️ Périodes d'Interventions Herbicides (Top 20 parcelles)",
427
+ labels={
428
+ 'mois_debut': 'Mois de début d\'intervention',
429
+ 'nb_interventions': 'Nombre d\'interventions',
430
+ 'quantite_totale': 'Quantité totale',
431
+ 'nb_produits_uniques': 'Nb produits différents'
432
+ }
433
+ )
434
+
435
+ # Ajouter les noms des mois sur l'axe X
436
+ fig.update_xaxis(
437
+ tickvals=list(range(1, 13)),
438
+ ticktext=['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun',
439
+ 'Jul', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc']
440
+ )
441
+
442
+ fig.update_layout(width=800, height=600)
443
+ return fig
444
+
445
+ except Exception as e:
446
+ fig = px.scatter(title=f"❌ Erreur lors de la création du graphique")
447
+ fig.add_annotation(text=str(e)[:100], xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False)
448
+ return fig
449
+
450
+ def create_monthly_interventions_heatmap(self, n_years=10):
451
+ """Crée une heatmap des interventions herbicides par mois et par année"""
452
+ try:
453
+ if self.df is None or len(self.df) == 0:
454
+ fig = go.Figure()
455
+ fig.add_annotation(text="❌ Aucune donnée disponible", x=0.5, y=0.5)
456
+ return fig
457
+
458
+ # Filtrer par herbicides et période
459
+ max_year = self.df['millesime'].max()
460
+ min_year = max_year - n_years + 1
461
+
462
+ herbicides_data = self.df[
463
+ (self.df['familleprod'] == 'Herbicides') &
464
+ (self.df['millesime'] >= min_year)
465
+ ].copy()
466
+
467
+ if len(herbicides_data) == 0:
468
+ fig = go.Figure()
469
+ fig.add_annotation(text="❌ Aucune donnée d'herbicides", x=0.5, y=0.5)
470
+ return fig
471
+
472
+ # Convertir les dates et extraire le mois
473
+ herbicides_data['datedebut_parsed'] = pd.to_datetime(
474
+ herbicides_data['datedebut'],
475
+ format='%d/%m/%y',
476
+ errors='coerce'
477
+ )
478
+
479
+ valid_dates = herbicides_data.dropna(subset=['datedebut_parsed'])
480
+ if len(valid_dates) == 0:
481
+ fig = go.Figure()
482
+ fig.add_annotation(text="❌ Aucune date valide", x=0.5, y=0.5)
483
+ return fig
484
+
485
+ valid_dates['mois'] = valid_dates['datedebut_parsed'].dt.month
486
+
487
+ # Créer la matrice mois x année
488
+ heatmap_data = valid_dates.groupby(['millesime', 'mois']).size().reset_index()
489
+ heatmap_data.columns = ['annee', 'mois', 'nb_interventions']
490
+
491
+ # Pivoter pour créer la matrice
492
+ heatmap_matrix = heatmap_data.pivot(index='mois', columns='annee', values='nb_interventions').fillna(0)
493
+
494
+ # Créer la heatmap
495
+ fig = px.imshow(
496
+ heatmap_matrix,
497
+ title="🗓️ Heatmap des Interventions Herbicides par Mois et Année",
498
+ labels={
499
+ 'x': 'Année',
500
+ 'y': 'Mois',
501
+ 'color': 'Nb interventions'
502
+ },
503
+ aspect="auto"
504
+ )
505
+
506
+ # Personnaliser les étiquettes des mois
507
+ month_labels = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun',
508
+ 'Jul', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc']
509
+ fig.update_yaxis(
510
+ tickvals=list(range(1, 13)),
511
+ ticktext=[month_labels[i-1] for i in range(1, 13) if i in heatmap_matrix.index]
512
+ )
513
+
514
+ fig.update_layout(width=800, height=500)
515
+ return fig
516
+
517
+ except Exception as e:
518
+ fig = go.Figure()
519
+ fig.add_annotation(text=f"❌ Erreur: {str(e)[:100]}", x=0.5, y=0.5)
520
+ return fig