Spaces:
Sleeping
Sleeping
Tracy André
commited on
Commit
·
f084836
1
Parent(s):
48479a2
updated
Browse files- .gitignore +4 -1
- GUIDE_REQUETES_HERBICIDES.md +177 -0
- app.py +300 -0
- 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
|