Tracy André commited on
Commit
676811f
·
1 Parent(s): 3bde590
Files changed (9) hide show
  1. README.md +80 -2
  2. __init__.py +7 -0
  3. analyzer.py +268 -0
  4. config.py +45 -0
  5. data_loader.py +164 -0
  6. interface.py +192 -0
  7. main.py +27 -0
  8. sample_data.csv +4 -21
  9. visualizations.py +180 -0
README.md CHANGED
@@ -5,7 +5,7 @@ colorFrom: green
5
  colorTo: blue
6
  sdk: gradio
7
  sdk_version: "4.31.0"
8
- app_file: app.py
9
  pinned: false
10
  license: mit
11
  ---
@@ -14,6 +14,61 @@ license: mit
14
 
15
  Application Gradio pour analyser et prédire la pression des adventices dans les parcelles agricoles bretonnes, développée pour le hackathon CRA.
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  ## 🎯 Objectifs
18
 
19
  - Prédire la pression adventice sur chaque parcelle pour les 3 prochaines campagnes
@@ -26,4 +81,27 @@ Application Gradio pour analyser et prédire la pression des adventices dans les
26
  - Calcul de l'IFT herbicides approximatif
27
  - Classification des parcelles par niveau de risque
28
  - Visualisations interactives
29
- - Recommandations pour cultures sensibles
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  colorTo: blue
6
  sdk: gradio
7
  sdk_version: "4.31.0"
8
+ app_file: main.py
9
  pinned: false
10
  license: mit
11
  ---
 
14
 
15
  Application Gradio pour analyser et prédire la pression des adventices dans les parcelles agricoles bretonnes, développée pour le hackathon CRA.
16
 
17
+ ## 🏗️ Architecture du Projet
18
+
19
+ ### Structure des fichiers
20
+
21
+ ```
22
+ data/
23
+ ├── __init__.py # Package Python
24
+ ├── main.py # Point d'entrée principal
25
+ ├── config.py # Configuration et constantes
26
+ ├── data_loader.py # Chargement des données HuggingFace
27
+ ├── analyzer.py # Analyse des données et calcul des risques
28
+ ├── visualizations.py # Création des graphiques et visualisations
29
+ ├── interface.py # Interface utilisateur Gradio
30
+ ├── app.py # [LEGACY] Ancien fichier monolithique
31
+ ├── app_simple.py # Version simplifiée
32
+ ├── requirements.txt # Dépendances Python
33
+ ├── sample_data.csv # Données d'exemple
34
+ └── results/ # Résultats d'analyse
35
+ ├── risk_analysis.csv
36
+ └── risk_visualization.html
37
+ ```
38
+
39
+ ### Modules
40
+
41
+ #### 🔧 `config.py`
42
+ - Configuration centrale (tokens, URLs, constantes)
43
+ - Paramètres des graphiques et de l'interface
44
+ - Messages et textes de l'application
45
+
46
+ #### 📊 `data_loader.py`
47
+ - Classe `DataLoader` pour le chargement des données
48
+ - Gestion des fallbacks (repo HF → fichiers locaux)
49
+ - Nettoyage et validation des données
50
+
51
+ #### 🧮 `analyzer.py`
52
+ - Classe `AgricultureAnalyzer` pour l'analyse des données
53
+ - Calcul des statistiques et de l'IFT herbicides
54
+ - Classification des risques par parcelle
55
+ - Génération des recommandations
56
+
57
+ #### 📈 `visualizations.py`
58
+ - Classe `AgricultureVisualizer` pour les graphiques
59
+ - Visualisations Plotly interactives
60
+ - Graphiques de risques, cultures, distributions
61
+
62
+ #### 🖥️ `interface.py`
63
+ - Classe `AgricultureInterface` pour l'UI Gradio
64
+ - Organisation en onglets
65
+ - Gestion des interactions utilisateur
66
+
67
+ #### 🚀 `main.py`
68
+ - Point d'entrée principal
69
+ - Orchestration des composants
70
+ - Lancement de l'application
71
+
72
  ## 🎯 Objectifs
73
 
74
  - Prédire la pression adventice sur chaque parcelle pour les 3 prochaines campagnes
 
81
  - Calcul de l'IFT herbicides approximatif
82
  - Classification des parcelles par niveau de risque
83
  - Visualisations interactives
84
+ - Recommandations pour cultures sensibles
85
+
86
+ ## 🚀 Utilisation
87
+
88
+ ### Lancement avec la nouvelle architecture
89
+
90
+ ```bash
91
+ python main.py
92
+ ```
93
+
94
+ ### Lancement avec l'ancien fichier (rétrocompatibilité)
95
+
96
+ ```bash
97
+ python app.py
98
+ ```
99
+
100
+ ## 🔄 Migration
101
+
102
+ L'ancienne version monolithique (`app.py`) reste disponible pour la rétrocompatibilité. La nouvelle architecture modulaire offre :
103
+
104
+ - **Meilleure maintenabilité** : Code séparé par responsabilité
105
+ - **Réutilisabilité** : Modules indépendants
106
+ - **Testabilité** : Tests unitaires plus faciles
107
+ - **Extensibilité** : Ajout de nouvelles fonctionnalités simplifié
__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ """
2
+ Package d'analyse des adventices agricoles pour le CRA Bretagne
3
+ """
4
+
5
+ __version__ = "1.0.0"
6
+ __author__ = "CRA Bretagne"
7
+ __description__ = "Application d'analyse des risques adventices pour l'agriculture durable"
analyzer.py ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module d'analyse des données agricoles et calcul des risques
3
+ """
4
+ import pandas as pd
5
+ from config import OPTIONAL_GROUP_COLS, REQUIRED_COLUMNS, RISK_LEVELS
6
+
7
+
8
+ class AgricultureAnalyzer:
9
+ """Classe responsable de l'analyse des données agricoles"""
10
+
11
+ def __init__(self, data=None):
12
+ self.df = data
13
+ self.risk_analysis = None
14
+
15
+ def set_data(self, data):
16
+ """Définit les données à analyser"""
17
+ self.df = data
18
+
19
+ def analyze_data(self):
20
+ """Analyse des données et calcul des risques"""
21
+ if self.df is None or len(self.df) == 0:
22
+ print("❌ Pas de données à analyser")
23
+ return "Erreur: Aucune donnée chargée"
24
+
25
+ try:
26
+ print(f"🔄 Début de l'analyse sur {len(self.df)} enregistrements...")
27
+
28
+ # Analyse générale
29
+ general_stats = self._calculate_general_stats()
30
+
31
+ # Analyse des herbicides
32
+ herbicide_stats = self._calculate_herbicide_stats()
33
+
34
+ # Calcul de l'analyse des risques
35
+ self.calculate_risk_analysis()
36
+
37
+ print("✅ Analyse terminée avec succès")
38
+ return general_stats, herbicide_stats
39
+
40
+ except Exception as e:
41
+ print(f"❌ Erreur lors de l'analyse: {str(e)}")
42
+ return None, None
43
+
44
+ def _calculate_general_stats(self):
45
+ """Calcule les statistiques générales"""
46
+ return {
47
+ 'total_parcelles': self.df['numparcell'].nunique(),
48
+ 'total_interventions': len(self.df),
49
+ 'surface_totale': self.df['surfparc'].sum(),
50
+ 'surface_moyenne': self.df['surfparc'].mean(),
51
+ 'periode': f"{self.df['millesime'].min()} - {self.df['millesime'].max()}"
52
+ }
53
+
54
+ def _calculate_herbicide_stats(self):
55
+ """Calcule les statistiques sur les herbicides"""
56
+ if 'familleprod' in self.df.columns:
57
+ herbicides_df = self.df[self.df['familleprod'] == 'Herbicides'].copy()
58
+ return {
59
+ 'nb_interventions_herbicides': len(herbicides_df),
60
+ 'pourcentage_herbicides': (len(herbicides_df) / len(self.df)) * 100,
61
+ 'parcelles_traitees': herbicides_df['numparcell'].nunique()
62
+ }
63
+ else:
64
+ return {
65
+ 'nb_interventions_herbicides': 0,
66
+ 'pourcentage_herbicides': 0,
67
+ 'parcelles_traitees': 0
68
+ }
69
+
70
+ def calculate_risk_analysis(self):
71
+ """Calcule l'analyse des risques par parcelle"""
72
+ try:
73
+ print("🔄 Calcul de l'analyse des risques...")
74
+
75
+ # Vérifier les colonnes nécessaires
76
+ required_group_cols = ['numparcell', 'surfparc']
77
+
78
+ # Construire la liste des colonnes de groupement disponibles
79
+ group_cols = [col for col in required_group_cols if col in self.df.columns]
80
+ group_cols.extend([col for col in OPTIONAL_GROUP_COLS if col in self.df.columns])
81
+
82
+ if len(group_cols) < 2:
83
+ print(f"❌ Colonnes insuffisantes pour le groupement: {group_cols}")
84
+ self.risk_analysis = pd.DataFrame()
85
+ return
86
+
87
+ # Construire l'agrégation selon les colonnes disponibles
88
+ agg_dict = self._build_aggregation_dict()
89
+
90
+ if not agg_dict:
91
+ print("❌ Aucune colonne disponible pour l'agrégation")
92
+ self.risk_analysis = pd.DataFrame()
93
+ return
94
+
95
+ # Groupement des données par parcelle
96
+ risk_analysis = self.df.groupby(group_cols).agg(agg_dict).round(2)
97
+
98
+ # Ajout des quantités d'herbicides spécifiques
99
+ risk_analysis = self._add_herbicide_quantities(risk_analysis, group_cols)
100
+
101
+ # Renommage des colonnes
102
+ risk_analysis = self._rename_columns(risk_analysis, agg_dict)
103
+
104
+ # Calcul de l'IFT approximatif
105
+ risk_analysis = self._calculate_ift(risk_analysis, group_cols)
106
+
107
+ # Classification du risque
108
+ risk_analysis['Risque_adventice'] = risk_analysis.apply(self._classify_risk, axis=1)
109
+
110
+ # Tri par risque
111
+ risk_analysis = self._sort_by_risk(risk_analysis)
112
+
113
+ self.risk_analysis = risk_analysis
114
+ print(f"✅ Analyse des risques terminée: {len(self.risk_analysis)} parcelles analysées")
115
+
116
+ except Exception as e:
117
+ print(f"❌ Erreur lors du calcul des risques: {str(e)}")
118
+ self.risk_analysis = pd.DataFrame()
119
+
120
+ def _build_aggregation_dict(self):
121
+ """Construit le dictionnaire d'agrégation selon les colonnes disponibles"""
122
+ agg_dict = {}
123
+ if 'familleprod' in self.df.columns:
124
+ agg_dict['familleprod'] = lambda x: (x == 'Herbicides').sum()
125
+ if 'libevenem' in self.df.columns:
126
+ agg_dict['libevenem'] = lambda x: len(x.unique())
127
+ if 'produit' in self.df.columns:
128
+ agg_dict['produit'] = lambda x: len(x.unique())
129
+ if 'quantitetot' in self.df.columns:
130
+ agg_dict['quantitetot'] = 'sum'
131
+ return agg_dict
132
+
133
+ def _add_herbicide_quantities(self, risk_analysis, group_cols):
134
+ """Ajoute les quantités d'herbicides spécifiques"""
135
+ if 'familleprod' in self.df.columns and 'quantitetot' in self.df.columns:
136
+ herbicides_df = self.df[self.df['familleprod'] == 'Herbicides']
137
+ if len(herbicides_df) > 0:
138
+ herbicide_quantities = herbicides_df.groupby(group_cols)['quantitetot'].sum().fillna(0)
139
+ risk_analysis['Quantite_herbicides'] = herbicide_quantities.reindex(risk_analysis.index, fill_value=0)
140
+ else:
141
+ risk_analysis['Quantite_herbicides'] = 0
142
+ else:
143
+ risk_analysis['Quantite_herbicides'] = 0
144
+ return risk_analysis
145
+
146
+ def _rename_columns(self, risk_analysis, agg_dict):
147
+ """Renomme les colonnes de façon sécurisée"""
148
+ new_column_names = {}
149
+ if 'familleprod' in agg_dict:
150
+ new_column_names['familleprod'] = 'Nb_herbicides'
151
+ if 'libevenem' in agg_dict:
152
+ new_column_names['libevenem'] = 'Diversite_evenements'
153
+ if 'produit' in agg_dict:
154
+ new_column_names['produit'] = 'Diversite_produits'
155
+ if 'quantitetot' in agg_dict:
156
+ new_column_names['quantitetot'] = 'Quantite_totale'
157
+
158
+ return risk_analysis.rename(columns=new_column_names)
159
+
160
+ def _calculate_ift(self, risk_analysis, group_cols):
161
+ """Calcule l'IFT approximatif"""
162
+ if 'surfparc' in group_cols:
163
+ risk_analysis['IFT_herbicide_approx'] = (
164
+ risk_analysis['Quantite_herbicides'] /
165
+ risk_analysis.index.get_level_values('surfparc')
166
+ ).round(2)
167
+ else:
168
+ risk_analysis['IFT_herbicide_approx'] = 0
169
+ return risk_analysis
170
+
171
+ def _classify_risk(self, row):
172
+ """Classification du risque pour une parcelle"""
173
+ ift = row.get('IFT_herbicide_approx', 0)
174
+ nb_herb = row.get('Nb_herbicides', 0)
175
+
176
+ if ift == 0 and nb_herb == 0:
177
+ return 'TRÈS FAIBLE'
178
+ elif ift < 1 and nb_herb <= 1:
179
+ return 'FAIBLE'
180
+ elif ift < 3 and nb_herb <= 3:
181
+ return 'MODÉRÉ'
182
+ elif ift < 5 and nb_herb <= 5:
183
+ return 'ÉLEVÉ'
184
+ else:
185
+ return 'TRÈS ÉLEVÉ'
186
+
187
+ def _sort_by_risk(self, risk_analysis):
188
+ """Trie les résultats par niveau de risque"""
189
+ risk_order = {r: i for i, r in enumerate(RISK_LEVELS)}
190
+ risk_analysis['Risk_Score'] = risk_analysis['Risque_adventice'].map(risk_order)
191
+ return risk_analysis.sort_values(['Risk_Score', 'IFT_herbicide_approx'])
192
+
193
+ def get_summary_stats(self):
194
+ """Retourne les statistiques de résumé"""
195
+ if self.df is None:
196
+ return "Aucune donnée disponible"
197
+
198
+ stats_text = f"""
199
+ ## 📊 Statistiques Générales
200
+ - **Nombre total de parcelles**: {self.df['numparcell'].nunique()}
201
+ - **Nombre d'interventions**: {len(self.df):,}
202
+ - **Surface totale**: {self.df['surfparc'].sum():.2f} hectares
203
+ - **Surface moyenne par parcelle**: {self.df['surfparc'].mean():.2f} hectares
204
+ - **Période**: {self.df['millesime'].min()} - {self.df['millesime'].max()}
205
+
206
+ ## 🧪 Analyse Herbicides
207
+ """
208
+
209
+ if 'familleprod' in self.df.columns:
210
+ herbicides_df = self.df[self.df['familleprod'] == 'Herbicides']
211
+ if len(herbicides_df) > 0:
212
+ stats_text += f"""
213
+ - **Interventions herbicides**: {len(herbicides_df)} ({(len(herbicides_df)/len(self.df)*100):.1f}%)
214
+ - **Parcelles traitées**: {herbicides_df['numparcell'].nunique()}
215
+ - **Produits herbicides différents**: {herbicides_df['produit'].nunique()}
216
+ """
217
+
218
+ if self.risk_analysis is not None and len(self.risk_analysis) > 0:
219
+ risk_distribution = self.risk_analysis['Risque_adventice'].value_counts()
220
+ stats_text += f"""
221
+
222
+ ## 🎯 Répartition des Risques Adventices
223
+ """
224
+ for risk_level in RISK_LEVELS:
225
+ if risk_level in risk_distribution:
226
+ count = risk_distribution[risk_level]
227
+ pct = (count / len(self.risk_analysis)) * 100
228
+ stats_text += f"- **{risk_level}**: {count} parcelles ({pct:.1f}%)\n"
229
+
230
+ return stats_text
231
+
232
+ def get_low_risk_recommendations(self):
233
+ """Retourne les recommandations pour les parcelles à faible risque"""
234
+ if self.risk_analysis is None:
235
+ return "Analyse des risques non disponible"
236
+
237
+ low_risk = self.risk_analysis[
238
+ self.risk_analysis['Risque_adventice'].isin(['TRÈS FAIBLE', 'FAIBLE'])
239
+ ].head(10)
240
+
241
+ recommendations = "## 🌾 TOP 10 - Parcelles Recommandées pour Cultures Sensibles (Pois, Haricot)\n\n"
242
+
243
+ for idx, row in low_risk.iterrows():
244
+ if isinstance(idx, tuple) and len(idx) >= 4:
245
+ parcelle, nom, culture, surface = idx[:4]
246
+ else:
247
+ # Fallback si l'index n'est pas un tuple de 4 éléments
248
+ parcelle = str(idx)
249
+ nom = "N/A"
250
+ culture = "N/A"
251
+ surface = row.get('surfparc', 0) if 'surfparc' in row else 0
252
+
253
+ recommendations += f"""
254
+ **Parcelle {parcelle}** ({nom})
255
+ - Culture actuelle: {culture}
256
+ - Surface: {surface:.2f} ha
257
+ - Niveau de risque: {row['Risque_adventice']}
258
+ - IFT herbicide: {row['IFT_herbicide_approx']:.2f}
259
+ - Nombre d'herbicides: {row.get('Nb_herbicides', 0)}
260
+
261
+ ---
262
+ """
263
+
264
+ return recommendations
265
+
266
+ def get_risk_analysis(self):
267
+ """Retourne l'analyse des risques"""
268
+ return self.risk_analysis
config.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Configuration pour l'application d'analyse des adventices agricoles
3
+ """
4
+ import os
5
+
6
+ # Configuration Hugging Face
7
+ HF_TOKEN = os.environ.get("HF_TOKEN")
8
+ DATASET_ID = "HackathonCRA/2024"
9
+
10
+ # Configuration des données
11
+ REQUIRED_COLUMNS = ["numparcell", "surfparc", "millesime"]
12
+ OPTIONAL_GROUP_COLS = ["nomparc", "libelleusag"]
13
+
14
+ # Configuration des risques
15
+ RISK_LEVELS = ['TRÈS FAIBLE', 'FAIBLE', 'MODÉRÉ', 'ÉLEVÉ', 'TRÈS ÉLEVÉ']
16
+ RISK_COLORS = {
17
+ 'TRÈS FAIBLE': 'green',
18
+ 'FAIBLE': 'lightgreen',
19
+ 'MODÉRÉ': 'orange',
20
+ 'ÉLEVÉ': 'red',
21
+ 'TRÈS ÉLEVÉ': 'darkred'
22
+ }
23
+
24
+ # Configuration Gradio
25
+ GRADIO_CONFIG = {
26
+ "server_name": "0.0.0.0",
27
+ "server_port": 7860,
28
+ "share": False
29
+ }
30
+
31
+ # Configuration des graphiques
32
+ PLOT_CONFIG = {
33
+ "width": 800,
34
+ "height": 600,
35
+ "title_font_size": 16
36
+ }
37
+
38
+ # Messages de l'application
39
+ MESSAGES = {
40
+ "loading": "🔄 Chargement des données depuis Hugging Face...",
41
+ "success": "✅ Données chargées avec succès",
42
+ "error_loading": "❌ Erreur lors du chargement du dataset",
43
+ "no_data": "❌ Aucune donnée disponible",
44
+ "analysis_complete": "✅ Analyse terminée avec succès"
45
+ }
data_loader.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module de chargement des données depuis Hugging Face
3
+ """
4
+ import os
5
+ import pandas as pd
6
+ from datasets import load_dataset
7
+ from huggingface_hub import HfApi, hf_hub_download
8
+ from config import HF_TOKEN, DATASET_ID, REQUIRED_COLUMNS, MESSAGES
9
+
10
+
11
+ class DataLoader:
12
+ """Classe responsable du chargement des données depuis différentes sources"""
13
+
14
+ def __init__(self):
15
+ self.df = None
16
+
17
+ def load_data(self):
18
+ """Charge les données du dataset Hugging Face"""
19
+ print(MESSAGES["loading"])
20
+ print(f"📋 Dataset ID: {DATASET_ID}")
21
+ print(f"📋 Token disponible: {'Oui' if HF_TOKEN else 'Non'}")
22
+
23
+ self.df = None
24
+
25
+ # 1) Tentative de chargement direct via datasets.load_dataset
26
+ try:
27
+ dataset = load_dataset(
28
+ DATASET_ID,
29
+ split="train",
30
+ token=HF_TOKEN,
31
+ trust_remote_code=True,
32
+ )
33
+ print(f"📊 Dataset chargé: {len(dataset)} exemples")
34
+
35
+ try:
36
+ self.df = dataset.to_pandas()
37
+ print("✅ Conversion to_pandas() réussie")
38
+ except Exception as pandas_error:
39
+ print(f"❌ Erreur to_pandas(): {pandas_error}")
40
+ print("🔄 Tentative de conversion manuelle...")
41
+ data_list = []
42
+ for i, item in enumerate(dataset):
43
+ data_list.append(item)
44
+ if i < 5:
45
+ print(f"📋 Exemple {i}: {list(item.keys())}")
46
+ self.df = pd.DataFrame(data_list)
47
+ print(f"✅ Conversion manuelle réussie: {len(self.df)} lignes")
48
+ except Exception as e:
49
+ print(f"❌ Erreur lors du chargement depuis Hugging Face: {str(e)}")
50
+ print(f"❌ Type d'erreur: {type(e).__name__}")
51
+ # 2) Fallback: récupérer directement les fichiers du repo
52
+ fallback_msg = self._fallback_load_from_repo_files()
53
+ if self.df is None:
54
+ return f"❌ Erreur lors du chargement du dataset : {str(e)} | Fallback: {fallback_msg}"
55
+
56
+ # Si on n'a toujours pas de dataframe, arrêter
57
+ if self.df is None:
58
+ return MESSAGES["no_data"]
59
+
60
+ print(f"📊 Données chargées: {len(self.df)} lignes")
61
+ print(f"📊 Colonnes disponibles: {list(self.df.columns)}")
62
+
63
+ # Nettoyage et validation
64
+ return self._clean_and_validate_data()
65
+
66
+ def _clean_and_validate_data(self):
67
+ """Nettoie et valide les données chargées"""
68
+ missing_cols = [col for col in REQUIRED_COLUMNS if col not in self.df.columns]
69
+
70
+ if missing_cols:
71
+ print(f"❌ Colonnes manquantes: {missing_cols}")
72
+ self.df = None
73
+ return f"❌ Colonnes manquantes: {missing_cols}"
74
+
75
+ # Nettoyage
76
+ initial_len = len(self.df)
77
+ self.df = self.df.dropna(subset=REQUIRED_COLUMNS)
78
+
79
+ print(f"📊 Avant nettoyage: {initial_len} lignes")
80
+ print(f"📊 Après nettoyage: {len(self.df)} lignes")
81
+
82
+ return MESSAGES["success"]
83
+
84
+ def _fallback_load_from_repo_files(self):
85
+ """Fallback pour charger les données en téléchargeant directement les fichiers du repo HF."""
86
+ try:
87
+ print("🔄 Tentative de chargement alternatif via fichiers du dépôt Hugging Face...")
88
+ api = HfApi()
89
+ files = api.list_repo_files(repo_id=DATASET_ID, repo_type="dataset", token=HF_TOKEN)
90
+ if not files:
91
+ print("❌ Aucun fichier dans le dépôt")
92
+ return "Aucun fichier trouvé dans le dépôt."
93
+
94
+ data_files = [
95
+ f for f in files if f.lower().endswith((".parquet", ".csv", ".tsv", ".json"))
96
+ ]
97
+ if not data_files:
98
+ print("❌ Aucun fichier de données exploitable (csv/tsv/parquet/json)")
99
+ return "Aucun fichier exploitable (csv/tsv/parquet/json)."
100
+
101
+ # Priorité: parquet > csv > tsv > json
102
+ for ext in [".parquet", ".csv", ".tsv", ".json"]:
103
+ selected = [f for f in data_files if f.lower().endswith(ext)]
104
+ if selected:
105
+ chosen_ext = ext
106
+ selected_files = selected
107
+ break
108
+
109
+ print(f"📂 Fichiers détectés ({chosen_ext}): {selected_files[:5]}{' ...' if len(selected_files) > 5 else ''}")
110
+
111
+ local_paths = []
112
+ for f in selected_files:
113
+ local_path = hf_hub_download(
114
+ repo_id=DATASET_ID,
115
+ repo_type="dataset",
116
+ filename=f,
117
+ token=HF_TOKEN,
118
+ )
119
+ local_paths.append(local_path)
120
+
121
+ frames = []
122
+ if chosen_ext == ".parquet":
123
+ for p in local_paths:
124
+ frames.append(pd.read_parquet(p))
125
+ elif chosen_ext == ".csv":
126
+ for p in local_paths:
127
+ frames.append(pd.read_csv(p))
128
+ elif chosen_ext == ".tsv":
129
+ for p in local_paths:
130
+ frames.append(pd.read_csv(p, sep="\t"))
131
+ elif chosen_ext == ".json":
132
+ for p in local_paths:
133
+ try:
134
+ frames.append(pd.read_json(p, lines=True))
135
+ except Exception:
136
+ frames.append(pd.read_json(p))
137
+
138
+ self.df = pd.concat(frames, ignore_index=True) if len(frames) > 1 else frames[0]
139
+ print(f"✅ Fallback réussi: {len(self.df)} lignes chargées depuis les fichiers du dépôt")
140
+ return None
141
+ except Exception as e:
142
+ print(f"❌ Fallback échoué: {e}")
143
+ # Dernier recours: fichier local d'exemple
144
+ return self._load_local_sample()
145
+
146
+ def _load_local_sample(self):
147
+ """Charge un fichier local de secours"""
148
+ sample_path = os.path.join(os.path.dirname(__file__), "sample_data.csv")
149
+ if os.path.exists(sample_path):
150
+ try:
151
+ self.df = pd.read_csv(sample_path)
152
+ print(f"✅ Chargement du fichier local 'sample_data.csv' ({len(self.df)} lignes)")
153
+ return "Chargement via fichier local de secours."
154
+ except Exception as e2:
155
+ print(f"❌ Échec du chargement du fichier local: {e2}")
156
+ return "Aucune source de données disponible."
157
+
158
+ def get_data(self):
159
+ """Retourne les données chargées"""
160
+ return self.df
161
+
162
+ def has_data(self):
163
+ """Vérifie si des données sont disponibles"""
164
+ return self.df is not None and len(self.df) > 0
interface.py ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module d'interface utilisateur avec Gradio
3
+ """
4
+ import os
5
+ # Désactiver les analytics Gradio dès le début
6
+ os.environ["GRADIO_ANALYTICS_ENABLED"] = "False"
7
+
8
+ import gradio as gr
9
+ from data_loader import DataLoader
10
+ from analyzer import AgricultureAnalyzer
11
+ from visualizations import AgricultureVisualizer
12
+ from config import GRADIO_CONFIG
13
+
14
+
15
+ class AgricultureInterface:
16
+ """Classe responsable de l'interface utilisateur Gradio"""
17
+
18
+ def __init__(self):
19
+ self.data_loader = DataLoader()
20
+ self.analyzer = AgricultureAnalyzer()
21
+ self.visualizer = AgricultureVisualizer()
22
+ self._initialize_data()
23
+
24
+ def _initialize_data(self):
25
+ """Initialise les données au démarrage"""
26
+ self.data_loader.load_data()
27
+ if self.data_loader.has_data():
28
+ self.analyzer.set_data(self.data_loader.get_data())
29
+ self.analyzer.analyze_data()
30
+ self.visualizer.set_data(
31
+ self.data_loader.get_data(),
32
+ self.analyzer.get_risk_analysis()
33
+ )
34
+
35
+ def refresh_data(self):
36
+ """Rafraîchit toutes les données"""
37
+ self.data_loader.load_data()
38
+ if self.data_loader.has_data():
39
+ self.analyzer.set_data(self.data_loader.get_data())
40
+ self.analyzer.analyze_data()
41
+ self.visualizer.set_data(
42
+ self.data_loader.get_data(),
43
+ self.analyzer.get_risk_analysis()
44
+ )
45
+ return (
46
+ self.analyzer.get_summary_stats(),
47
+ self.visualizer.create_culture_analysis(),
48
+ self.visualizer.create_risk_distribution(),
49
+ self.visualizer.create_risk_visualization(),
50
+ self.analyzer.get_low_risk_recommendations()
51
+ )
52
+ else:
53
+ # Retourner des valeurs par défaut si pas de données
54
+ empty_fig = self.visualizer.create_culture_analysis() # Créera un graphique vide
55
+ return (
56
+ "❌ Aucune donnée disponible",
57
+ empty_fig,
58
+ empty_fig,
59
+ empty_fig,
60
+ "❌ Aucune recommandation disponible"
61
+ )
62
+
63
+ def create_interface(self):
64
+ """Crée l'interface Gradio"""
65
+ with gr.Blocks(title="🌾 Analyse Adventices Agricoles CRA", theme=gr.themes.Soft()) as demo:
66
+ gr.Markdown("""
67
+ # 🌾 Analyse des Adventices Agricoles - CRA Bretagne
68
+
69
+ **Objectif**: Anticiper et réduire la pression des adventices dans les parcelles agricoles bretonnes
70
+
71
+ Cette application analyse les données historiques pour identifier les parcelles les plus adaptées
72
+ à la culture de plantes sensibles comme le pois ou le haricot.
73
+ """)
74
+
75
+ with gr.Tabs():
76
+ with gr.TabItem("📊 Vue d'ensemble"):
77
+ self._create_overview_tab()
78
+
79
+ with gr.TabItem("🎯 Analyse des Risques"):
80
+ self._create_risk_analysis_tab()
81
+
82
+ with gr.TabItem("🌾 Recommandations"):
83
+ self._create_recommendations_tab()
84
+
85
+ with gr.TabItem("ℹ️ À propos"):
86
+ self._create_about_tab()
87
+
88
+ # Bouton de rafraîchissement
89
+ refresh_btn = gr.Button("🔄 Actualiser les données", variant="secondary")
90
+
91
+ # Connecter le bouton de rafraîchissement
92
+ refresh_btn.click(
93
+ self.refresh_data,
94
+ outputs=[
95
+ self.stats_output,
96
+ self.culture_plot,
97
+ self.risk_dist_plot,
98
+ self.risk_plot,
99
+ self.reco_output
100
+ ]
101
+ )
102
+
103
+ return demo
104
+
105
+ def _create_overview_tab(self):
106
+ """Crée l'onglet de vue d'ensemble"""
107
+ gr.Markdown("## Statistiques générales des données agricoles")
108
+
109
+ self.stats_output = gr.Markdown(self.analyzer.get_summary_stats())
110
+
111
+ with gr.Row():
112
+ self.culture_plot = gr.Plot(self.visualizer.create_culture_analysis())
113
+ self.risk_dist_plot = gr.Plot(self.visualizer.create_risk_distribution())
114
+
115
+ def _create_risk_analysis_tab(self):
116
+ """Crée l'onglet d'analyse des risques"""
117
+ gr.Markdown("## Cartographie des risques adventices par parcelle")
118
+
119
+ self.risk_plot = gr.Plot(self.visualizer.create_risk_visualization())
120
+
121
+ gr.Markdown("""
122
+ **Interprétation du graphique**:
123
+ - **Axe X**: Surface de la parcelle (hectares)
124
+ - **Axe Y**: IFT Herbicide approximatif
125
+ - **Couleur**: Niveau de risque adventice
126
+ - **Taille**: Nombre d'herbicides utilisés
127
+
128
+ Les parcelles vertes (risque faible) sont idéales pour les cultures sensibles.
129
+ """)
130
+
131
+ def _create_recommendations_tab(self):
132
+ """Crée l'onglet des recommandations"""
133
+ self.reco_output = gr.Markdown(self.analyzer.get_low_risk_recommendations())
134
+
135
+ gr.Markdown("""
136
+ ## 💡 Conseils pour la gestion des adventices
137
+
138
+ ### Parcelles à Très Faible Risque (Vertes)
139
+ - ✅ **Idéales pour pois et haricot**
140
+ - ✅ Historique d'usage herbicide minimal
141
+ - ✅ Pression adventice faible attendue
142
+
143
+ ### Parcelles à Faible Risque (Vert clair)
144
+ - ⚠️ Surveillance légère recommandée
145
+ - ✅ Conviennent aux cultures sensibles avec précautions
146
+
147
+ ### Parcelles à Risque Modéré/Élevé (Orange/Rouge)
148
+ - ❌ Éviter pour cultures sensibles
149
+ - 🔍 Rotation nécessaire avant implantation
150
+ - 📈 Surveillance renforcée des adventices
151
+
152
+ ### Stratégies alternatives
153
+ - **Rotation longue**: 3-4 ans avant cultures sensibles
154
+ - **Cultures intermédiaires**: CIPAN pour réduire la pression
155
+ - **Techniques mécaniques**: Hersage, binage
156
+ - **Biostimulants**: Renforcement naturel des cultures
157
+ """)
158
+
159
+ def _create_about_tab(self):
160
+ """Crée l'onglet à propos"""
161
+ gr.Markdown("""
162
+ ## 🎯 Méthodologie
163
+
164
+ Cette analyse se base sur :
165
+
166
+ ### Calcul de l'IFT (Indice de Fréquence de Traitement)
167
+ - **IFT ≈ Quantité appliquée / Surface de parcelle**
168
+ - Indicateur de l'intensité des traitements herbicides
169
+
170
+ ### Classification des risques
171
+ - **TRÈS FAIBLE**: IFT = 0, aucun herbicide
172
+ - **FAIBLE**: IFT < 1, usage minimal
173
+ - **MODÉRÉ**: IFT < 3, usage modéré
174
+ - **ÉLEVÉ**: IFT < 5, usage important
175
+ - **TRÈS ÉLEVÉ**: IFT ≥ 5, usage intensif
176
+
177
+ ### Données analysées
178
+ - **Source**: Station Expérimentale de Kerguéhennec
179
+ - **Période**: Campagne 2025
180
+ - **Variables**: Interventions, produits, quantités, surfaces
181
+
182
+ ---
183
+
184
+ **Développé pour le Hackathon CRA Bretagne** 🏆
185
+
186
+ *Application d'aide à la décision pour une agriculture durable*
187
+ """)
188
+
189
+ def launch(self):
190
+ """Lance l'interface"""
191
+ demo = self.create_interface()
192
+ demo.launch(**GRADIO_CONFIG)
main.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Point d'entrée principal de l'application d'analyse des adventices agricoles
3
+ """
4
+ import warnings
5
+ import matplotlib.pyplot as plt
6
+ import seaborn as sns
7
+ from interface import AgricultureInterface
8
+
9
+ # Suppression des warnings
10
+ warnings.filterwarnings('ignore')
11
+
12
+ # Configuration des graphiques
13
+ plt.style.use('default')
14
+ sns.set_palette("husl")
15
+
16
+
17
+ def main():
18
+ """Fonction principale qui lance l'application"""
19
+ print("🌾 Démarrage de l'application d'analyse des adventices agricoles...")
20
+
21
+ # Création et lancement de l'interface
22
+ app = AgricultureInterface()
23
+ app.launch()
24
+
25
+
26
+ if __name__ == "__main__":
27
+ main()
sample_data.csv CHANGED
@@ -1,22 +1,5 @@
1
- Station Expérimentale de Kerguéhennec - Données d'intervention 2025
2
  millesime,raisonsoci,siret,pacage,refca,numilot,numparcell,nomparc,surfparc,rang,estpac,libelleusag,datedebut,datefin,libperiode,libregroupe,libevenem,dureeeffect,familleprod,produit,quantitetot,unite,neffqte,peffqte,kqte,teneurn,teneurp,teneurk,keq,volumebo,codeamm,codegnis,materiel,mainoeuvre
3
- 2025,Station Expérimentale de Kerguéhennec,12345678901234,1001,CA001,1,1102,Bourg bas,6.73,1,True,blé tendre hiver,15/03/25,15/03/25,,Herbicides,Traitement et protection des cultures,1,Herbicides,LUMEO,0.20,L,,,,,,,,,,,Pulvérisateur,2.5
4
- 2025,Station Expérimentale de Kerguéhennec,12345678901234,1001,CA001,1,1301,Bois Guillemin,5.97,1,True,blé tendre hiver,20/03/25,20/03/25,,Herbicides,Traitement et protection des cultures,1,Herbicides,PEAK,0.01,L,,,,,,,,,,,Pulvérisateur,2.0
5
- 2025,Station Expérimentale de Kerguéhennec,12345678901234,1001,CA001,1,1101,Bourg Haut,5.55,1,True,maïs grain,25/04/25,25/04/25,,Herbicides,Traitement et protection des cultures,1,Herbicides,GLISTER ULTRA 360,3.50,L,,,,,,,,,,,Pulvérisateur,3.0
6
- 2025,Station Expérimentale de Kerguéhennec,12345678901234,1001,CA001,1,1001,Carancier Ht,5.46,1,True,colza hiver,10/04/25,10/04/25,,Herbicides,Traitement et protection des cultures,1,Herbicides,BISCOTO,1.20,L,,,,,,,,,,,Pulvérisateur,2.5
7
- 2025,Station Expérimentale de Kerguéhennec,12345678901234,1001,CA001,1,48,Etang Bois,3.36,1,True,haricot vert industrie,05/05/25,05/05/25,,Herbicides,Traitement et protection des cultures,1,Herbicides,ISARD,2.40,L,,,,,,,,,,,Pulvérisateur,4.0
8
- 2025,Station Expérimentale de Kerguéhennec,12345678901234,1001,CA001,1,44,La Défriche,3.25,1,True,CIPAN autre,,,,,Traitement et protection des cultures,0,,,0.00,L,,,,,,,,,,,Pulvérisateur,0.0
9
- 2025,Station Expérimentale de Kerguéhennec,12345678901234,1001,CA001,1,2,Kersuzan Bas,3.05,1,True,CIPAN autre,,,,,Traitement et protection des cultures,0,,,0.00,L,,,,,,,,,,,Pulvérisateur,0.0
10
- 2025,Station Expérimentale de Kerguéhennec,12345678901234,1001,CA001,1,81,Charbonnerie Entrée,3.01,1,True,CIPAN autre,,,,,Traitement et protection des cultures,0,,,0.00,L,,,,,,,,,,,Pulvérisateur,0.0
11
- 2025,Station Expérimentale de Kerguéhennec,12345678901234,1001,CA001,1,11,Cléhury,2.97,1,True,orge hiver,12/04/25,12/04/25,,Herbicides,Traitement et protection des cultures,1,Herbicides,FREEWAY 480,0.80,L,,,,,,,,,,,Pulvérisateur,2.0
12
- 2025,Station Expérimentale de Kerguéhennec,12345678901234,1001,CA001,1,5,Etang Moulin,2.85,1,True,CIPAN autre,,,,,Traitement et protection des cultures,0,,,0.00,L,,,,,,,,,,,Pulvérisateur,0.0
13
- 2025,Station Expérimentale de Kerguéhennec,12345678901234,1001,CA001,1,50,Lann Chebot Le Roch,2.20,1,True,blé tendre hiver,18/03/25,18/03/25,,Herbicides,Traitement et protection des cultures,1,Herbicides,NISSHIN PREMIUM 6 OD,1.50,L,,,,,,,,,,,Pulvérisateur,3.5
14
- 2025,Station Expérimentale de Kerguéhennec,12345678901234,1001,CA001,1,16,Champ ferme W du sol parking,1.95,1,True,maïs grain,28/04/25,28/04/25,,Herbicides,Traitement et protection des cultures,1,Herbicides,ALABAMA,1.20,L,,,,,,,,,,,Pulvérisateur,2.5
15
- 2025,Station Expérimentale de Kerguéhennec,12345678901234,1001,CA001,1,39,Champ ferme transfert,1.85,1,True,blé tendre hiver,22/03/25,22/03/25,,Herbicides,Traitement et protection des cultures,1,Herbicides,CENT-7,0.15,L,,,,,,,,,,,Pulvérisateur,2.0
16
- 2025,Station Expérimentale de Kerguéhennec,12345678901234,1001,CA001,1,1201,Champ Robert,1.75,1,True,blé tendre hiver,25/03/25,25/03/25,,Herbicides,Traitement et protection des cultures,1,Herbicides,CORUM,0.95,L,,,,,,,,,,,Pulvérisateur,2.5
17
- 2025,Station Expérimentale de Kerguéhennec,12345678901234,1001,CA001,1,38,Champ ferme W du sol,1.65,1,True,colza hiver,15/04/25,15/04/25,,Herbicides,Traitement et protection des cultures,1,Herbicides,LUMEO,0.18,L,,,,,,,,,,,Pulvérisateur,2.0
18
- 2025,Station Expérimentale de Kerguéhennec,12345678901234,1001,CA001,1,14,Grand-Champ 1 essai soja 25,0.53,1,True,soja,,,,,Traitement et protection des cultures,0,,,0.00,L,,,,,,,,,,,Pulvérisateur,0.0
19
- 2025,Station Expérimentale de Kerguéhennec,12345678901234,1001,CA001,1,10,Penderff 7 analytique,1.56,1,True,avoine printemps,,,,,Traitement et protection des cultures,0,,,0.00,L,,,,,,,,,,,Pulvérisateur,0.0
20
- 2025,Station Expérimentale de Kerguéhennec,12345678901234,1001,CA001,1,33,Penderff Luzerne,2.10,1,True,luzerne,,,,,Traitement et protection des cultures,0,,,0.00,L,,,,,,,,,,,Pulvérisateur,0.0
21
- 2025,Station Expérimentale de Kerguéhennec,12345678901234,1001,CA001,1,4,Penderff 1,0.37,1,True,feverole printemps,,,,,Traitement et protection des cultures,0,,,0.00,L,,,,,,,,,,,Pulvérisateur,0.0
22
- 2025,Station Expérimentale de Kerguéhennec,12345678901234,1001,CA001,1,6,Lann Chebot chemin,0.11,1,True,CIPAN autre,,,,,Traitement et protection des cultures,0,,,0.00,L,,,,,,,,,,,Pulvérisateur,0.0
 
 
1
  millesime,raisonsoci,siret,pacage,refca,numilot,numparcell,nomparc,surfparc,rang,estpac,libelleusag,datedebut,datefin,libperiode,libregroupe,libevenem,dureeeffect,familleprod,produit,quantitetot,unite,neffqte,peffqte,kqte,teneurn,teneurp,teneurk,keq,volumebo,codeamm,codegnis,materiel,mainoeuvre
2
+ 2014,Station Expérimentale de Kerguéhennec,18560001000016,056021200,70000308,2,21,Champ ferme Bas,1.97,1,true,blé tendre hiver,15/11/13,15/11/13,,Plantation et Semis,Semis classique,120,BLE TENDRE.,RUBISKO dose ,11.82,Dose,,,,,,,,,,512C771,"TASSE-AVANT, Vibro/tasse-Avant - TRACTEURS CLASSIQUES, Tracteur JOHN DEERE 6530 Premium - SEMOIRS & ACCESSOIRES, Semoir 3 m Combiné HR LEMKEN Solitair 9 - HERSES, Herse Rotative LEMKEN Zirkon 3m - ",
3
+ 2014,Station Expérimentale de Kerguéhennec,18560001000016,056021200,70000308,2,21,Champ ferme Bas,1.97,1,true,blé tendre hiver,25/2/14,25/2/14,,Fertilisation,Ferti minérale amendement et foliaire,44,Engrais et amendements mineraux,Solution Liquide N 39,197.0,L,76.83000183105469,0.0,0.0,39.0,0.0,0.0,,,,,"PULVERISATEURS, Porté 1200 l 21 m DPA - TRACTEURS CLASSIQUES, Arion 410 CIS - ",
4
+ 2014,Station Expérimentale de Kerguéhennec,18560001000016,056021200,70000308,2,21,Champ ferme Bas,1.97,1,true,blé tendre hiver,12/3/14,12/3/14,,Protection des cultures,Traitement et protection des cultures,46,Herbicides,ALIGATOR,0.04104167,Kg,,,,,,,,,8400255,,"PULVERISATEURS, Porté 1200 l 21 m DPA - TRACTEURS CLASSIQUES, ARION 530 CIS - ",
5
+ 2014,Station Expérimentale de Kerguéhennec,18560001000016,056021200,70000308,2,21,Champ ferme Bas,1.97,1,true,blé tendre hiver,12/3/14,12/3/14,,Protection des cultures,Traitement et protection des cultures,46,Herbicides,CHARADE,2.5651042,l,,,,,,,,,9600293,,"PULVERISATEURS, Porté 1200 l 21 m DPA - TRACTEURS CLASSIQUES, ARION 530 CIS - ",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
visualizations.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module de visualisation des données agricoles
3
+ """
4
+ import plotly.express as px
5
+ import plotly.graph_objects as go
6
+ from config import RISK_COLORS, PLOT_CONFIG
7
+
8
+
9
+ class AgricultureVisualizer:
10
+ """Classe responsable de la création des visualisations"""
11
+
12
+ def __init__(self, data=None, risk_analysis=None):
13
+ self.df = data
14
+ self.risk_analysis = risk_analysis
15
+
16
+ def set_data(self, data, risk_analysis=None):
17
+ """Définit les données à visualiser"""
18
+ self.df = data
19
+ if risk_analysis is not None:
20
+ self.risk_analysis = risk_analysis
21
+
22
+ def create_risk_visualization(self):
23
+ """Crée la visualisation des risques"""
24
+ if self.risk_analysis is None or len(self.risk_analysis) == 0:
25
+ # Créer un graphique vide avec message d'erreur
26
+ fig = px.scatter(title="❌ Aucune donnée d'analyse des risques disponible")
27
+ fig.add_annotation(
28
+ text="Veuillez charger les données d'abord",
29
+ xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False
30
+ )
31
+ return fig
32
+
33
+ risk_df = self.risk_analysis.reset_index()
34
+
35
+ # Vérifier quelles colonnes sont disponibles pour hover_data
36
+ available_hover_cols = []
37
+ for col in ['nomparc', 'libelleusag']:
38
+ if col in risk_df.columns:
39
+ available_hover_cols.append(col)
40
+
41
+ fig = px.scatter(
42
+ risk_df,
43
+ x='surfparc',
44
+ y='IFT_herbicide_approx',
45
+ color='Risque_adventice',
46
+ size='Nb_herbicides',
47
+ hover_data=available_hover_cols if available_hover_cols else None,
48
+ color_discrete_map=RISK_COLORS,
49
+ title="🎯 Analyse du Risque Adventice par Parcelle",
50
+ labels={
51
+ 'surfparc': 'Surface de la parcelle (ha)',
52
+ 'IFT_herbicide_approx': 'IFT Herbicide (approximatif)',
53
+ 'Risque_adventice': 'Niveau de risque'
54
+ }
55
+ )
56
+
57
+ fig.update_layout(
58
+ width=PLOT_CONFIG["width"],
59
+ height=PLOT_CONFIG["height"],
60
+ title_font_size=PLOT_CONFIG["title_font_size"]
61
+ )
62
+ return fig
63
+
64
+ def create_culture_analysis(self):
65
+ """Analyse par type de culture"""
66
+ if self.df is None or len(self.df) == 0:
67
+ # Créer un graphique vide avec message d'erreur
68
+ fig = px.pie(title="❌ Aucune donnée disponible")
69
+ fig.add_annotation(
70
+ text="Veuillez charger les données d'abord",
71
+ xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False
72
+ )
73
+ return fig
74
+
75
+ if 'libelleusag' not in self.df.columns:
76
+ fig = px.pie(title="❌ Colonne 'libelleusag' non disponible")
77
+ fig.add_annotation(
78
+ text="Les données de culture ne sont pas disponibles",
79
+ xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False
80
+ )
81
+ return fig
82
+
83
+ culture_counts = self.df['libelleusag'].value_counts()
84
+
85
+ fig = px.pie(
86
+ values=culture_counts.values,
87
+ names=culture_counts.index,
88
+ title="🌱 Répartition des Cultures"
89
+ )
90
+
91
+ fig.update_layout(width=700, height=500)
92
+ return fig
93
+
94
+ def create_risk_distribution(self):
95
+ """Distribution des niveaux de risque"""
96
+ if self.risk_analysis is None or len(self.risk_analysis) == 0:
97
+ # Créer un graphique vide avec message d'erreur
98
+ fig = px.bar(title="❌ Aucune analyse des risques disponible")
99
+ fig.add_annotation(
100
+ text="Veuillez charger les données d'abord",
101
+ xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False
102
+ )
103
+ return fig
104
+
105
+ risk_counts = self.risk_analysis['Risque_adventice'].value_counts()
106
+
107
+ fig = px.bar(
108
+ x=risk_counts.index,
109
+ y=risk_counts.values,
110
+ color=risk_counts.index,
111
+ color_discrete_map=RISK_COLORS,
112
+ title="📊 Distribution des Niveaux de Risque Adventice",
113
+ labels={'x': 'Niveau de risque', 'y': 'Nombre de parcelles'}
114
+ )
115
+
116
+ fig.update_layout(width=700, height=500, showlegend=False)
117
+ return fig
118
+
119
+ def create_herbicide_timeline(self):
120
+ """Crée un graphique de l'évolution temporelle des herbicides"""
121
+ if self.df is None or len(self.df) == 0:
122
+ fig = px.line(title="❌ Aucune donnée disponible")
123
+ fig.add_annotation(
124
+ text="Veuillez charger les données d'abord",
125
+ xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False
126
+ )
127
+ return fig
128
+
129
+ if 'millesime' not in self.df.columns or 'familleprod' not in self.df.columns:
130
+ fig = px.line(title="❌ Colonnes nécessaires non disponibles")
131
+ fig.add_annotation(
132
+ text="Les données temporelles ne sont pas disponibles",
133
+ xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False
134
+ )
135
+ return fig
136
+
137
+ # Filtrer les herbicides et grouper par année
138
+ herbicides_df = self.df[self.df['familleprod'] == 'Herbicides']
139
+ if len(herbicides_df) == 0:
140
+ fig = px.line(title="❌ Aucune donnée d'herbicide disponible")
141
+ return fig
142
+
143
+ yearly_herbicides = herbicides_df.groupby('millesime').agg({
144
+ 'numparcell': 'nunique',
145
+ 'quantitetot': 'sum'
146
+ }).reset_index()
147
+
148
+ fig = px.line(
149
+ yearly_herbicides,
150
+ x='millesime',
151
+ y='quantitetot',
152
+ title="📈 Évolution de l'Usage des Herbicides par Année",
153
+ labels={
154
+ 'millesime': 'Année',
155
+ 'quantitetot': 'Quantité totale d\'herbicides'
156
+ }
157
+ )
158
+
159
+ fig.update_layout(width=700, height=400)
160
+ return fig
161
+
162
+ def create_surface_analysis(self):
163
+ """Analyse de la distribution des surfaces"""
164
+ if self.df is None or len(self.df) == 0:
165
+ fig = px.histogram(title="❌ Aucune donnée disponible")
166
+ return fig
167
+
168
+ fig = px.histogram(
169
+ self.df,
170
+ x='surfparc',
171
+ nbins=20,
172
+ title="📏 Distribution des Surfaces de Parcelles",
173
+ labels={
174
+ 'surfparc': 'Surface (ha)',
175
+ 'count': 'Nombre de parcelles'
176
+ }
177
+ )
178
+
179
+ fig.update_layout(width=700, height=400)
180
+ return fig