gdleds commited on
Commit
d2c2cdf
·
1 Parent(s): c4f4d77

first push

Browse files
Files changed (4) hide show
  1. Dockerfile +12 -12
  2. Visus.ipynb +0 -0
  3. app.py +898 -0
  4. requirements.txt +21 -2
Dockerfile CHANGED
@@ -1,20 +1,20 @@
1
- FROM python:3.13.5-slim
 
2
 
 
3
  WORKDIR /app
4
 
5
- RUN apt-get update && apt-get install -y \
6
- build-essential \
7
- curl \
8
- git \
9
- && rm -rf /var/lib/apt/lists/*
10
 
11
- COPY requirements.txt ./
12
- COPY src/ ./src/
13
 
14
- RUN pip3 install -r requirements.txt
 
15
 
 
16
  EXPOSE 8501
17
 
18
- HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
19
-
20
- ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
 
1
+ # Utiliser une image Python
2
+ FROM python:3.9
3
 
4
+ # Définir le dossier de travail dans le conteneur
5
  WORKDIR /app
6
 
7
+ # Copier requirements.txt en premier (pour tirer parti du cache)
8
+ COPY requirements.txt .
 
 
 
9
 
10
+ # Installer les dépendances Python
11
+ RUN pip install --no-cache-dir -r requirements.txt
12
 
13
+ # Copier le reste des fichiers du projet dans le conteneur
14
+ COPY . .
15
 
16
+ # Exposer le port par défaut de Streamlit
17
  EXPOSE 8501
18
 
19
+ # Lancer l'application Streamlit
20
+ CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
 
Visus.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
app.py ADDED
@@ -0,0 +1,898 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #-------------------------------------------------------- Imports nécessaires ---------------------------------------------------
2
+ import pandas as pd
3
+ import plotly.express as px
4
+ import matplotlib.pyplot as plt
5
+ import plotly.io as pio
6
+ import sklearn
7
+ import warnings
8
+ import sksurv.datasets
9
+ import numpy as np
10
+ import joblib
11
+ import pickle
12
+ import streamlit as st
13
+ import os
14
+ import io
15
+ import urllib.request
16
+ import json
17
+ import matplotlib
18
+ import plotly.graph_objects as go
19
+ import xgboost as xgb
20
+ import warnings
21
+ import folium
22
+ import boto3
23
+ import psycopg2
24
+ import datetime
25
+ # import mlflow
26
+ from lifelines import CoxPHFitter
27
+ from itertools import product
28
+ from tqdm import tqdm
29
+ from xgboost import XGBRegressor, XGBClassifier, DMatrix, train as xgb_train
30
+ from xgbse import XGBSEKaplanNeighbors
31
+ from xgbse.converters import convert_to_structured
32
+ from sklearn.preprocessing import StandardScaler
33
+ from sklearn.impute import SimpleImputer
34
+ from sklearn.pipeline import Pipeline, make_pipeline
35
+ from sklearn.model_selection import train_test_split,GridSearchCV, KFold, ParameterGrid,
36
+ from sklearn.ensemble import GradientBoostingSurvivalAnalysis
37
+ from sklearn.metrics import classification_report, confusion_matrix, accuracy_score, roc_auc_score, cumulative_dynamic_auc, concordance_index_censored
38
+ from sklearn.exceptions import UndefinedMetricWarning
39
+ from sklearn.cluster import DBSCAN
40
+ from sklearn import set_config
41
+ from sksurv.datasets import load_breast_cancer
42
+ from sksurv.linear_model import CoxnetSurvivalAnalysis, CoxPHSurvivalAnalysis
43
+ from sksurv.preprocessing import OneHotEncoder
44
+ from sksurv.util import Surv
45
+ from branca.element import Template, MacroElement
46
+ from folium import DivIcon
47
+ from streamlit_folium import st_folium
48
+ from sqlalchemy import create_engine, text
49
+
50
+ from dotenv import load_dotenv
51
+
52
+
53
+ warnings.filterwarnings("ignore", category=UndefinedMetricWarning)
54
+ set_config(display="text")
55
+
56
+
57
+ #_________________________________________________# Configuration de la page_______________________________________________________
58
+ st.set_page_config(page_title="Projet Incendies", layout="wide")
59
+
60
+ #_________________________________________________# Sidebar de navigation_________________________________________________
61
+ st.sidebar.title("Navigation")
62
+ page = st.sidebar.radio("Aller à", [
63
+ "Accueil",
64
+ "Notre Projet",
65
+ "Exploration des données",
66
+ # "Résultats des modèles",
67
+
68
+ ])
69
+ #________________________________________________________# Footer#_____________________________________________________________
70
+ def show_footer():
71
+ st.markdown("---")
72
+ st.markdown("Projet réalisé dans le cadre de la LEAD. © 2025")
73
+ #_________________________________________________# Chargement DATASET (modèle)#_______________________________________________
74
+
75
+ # mlflow.set_tracking_uri(os.getenv('https://gdleds-mlflow-fire2.hf.space'))
76
+
77
+ os.environ['AWS_ACCESS_KEY_ID'] = os.getenv('AWS_ACCESS_KEY_ID')
78
+ os.environ['AWS_SECRET_ACCESS_KEY'] = os.getenv('AWS_SECRET_ACCESS_KEY')
79
+ os.environ['MLFLOW_DEFAULT_ARTIFACT_ROOT'] = os.getenv('MLFLOW_DEFAULT_ARTIFACT_ROOT')
80
+ os.environ['S3_BUCKET'] = os.getenv('S3_BUCKET')
81
+ s3 = boto3.client('s3')
82
+
83
+ db_user = os.getenv("DB_USER")
84
+ db_password = os.getenv("DB_PASSWORD")
85
+ db_host = os.getenv("DB_HOST")
86
+ db_name = os.getenv("DB_NAME")
87
+
88
+ engine = create_engine(f"postgresql+psycopg2://{db_user}:{db_password}@{db_host}/{db_name}")
89
+
90
+ @st.cache_data
91
+ def load_model_data():
92
+ yesterday = (datetime.date.today() - datetime.timedelta(days=1)).strftime("%Y-%m-%d")
93
+ query = f"""SELECT * FROM data_prediction WHERE Date = {yesterday}"""
94
+ # s3.download_file(os.getenv('S3_Bucket'),'compile/predictions_feu_2025.csv', 'prediction_feu_2025.csv')
95
+ # url = "https://projet-incendie.s3.eu-west-3.amazonaws.com/dataset_modele_decompte.csv"
96
+ try:
97
+ df = pd.read_sql(query, engine)
98
+ df=pd.DataFrame(df)
99
+ for col in df.columns:
100
+ if "date" in col.lower():
101
+ df[col] = pd.to_datetime(df[col], errors="coerce", dayfirst=True)
102
+ return df
103
+ except Exception as e:
104
+ st.error(f"❌ Erreur lors du chargement des données : {e}")
105
+ return pd.DataFrame()
106
+ #_________________________________________________# Chargement des données d'incendies et de coordonnées#_______________________________________
107
+ @st.cache_data
108
+ def load_data():
109
+ s3.download_file=(os.getenv('S3_BUCKET'),'dataset/Incendies_2006_2024 (1).csv', 'incendies_2006_2024.csv')
110
+ # url_incendies = 'https://projet-incendie.s3.eu-west-3.amazonaws.com/Incendies_2006_2024.csv'
111
+ return pd.read_csv('incendies_2006_2024.csv', sep=';', encoding='utf-8', skiprows=3)
112
+
113
+ @st.cache_data
114
+ def load_coords():
115
+ s3.download_file(os.getenv('S3_BUCKET'), 'meteo-corse/coordonnees_villes (2).csv', 'coordonnees_villes.csv')
116
+ # url_coords = 'https://projet-incendie.s3.eu-west-3.amazonaws.com/coordonnees_villes.csv'
117
+ return pd.read_csv('coordonnees_villes.csv', sep=',', encoding='utf-8')
118
+
119
+ @st.cache_data
120
+ def load_df_merge():
121
+ s3.download_file(os.getenv('S3_BUCKET'), 'dataset/historique_incendies_avec_coordonnees.csv', 'historique_incendies_avec_coordonnees.csv')
122
+ # url = 'https://projet-incendie.s3.eu-west-3.amazonaws.com/historique_incendies_avec_coordonnees.csv'
123
+ return pd.read_csv('historique_incendies_avec_coordonnees.csv', sep=';', encoding='utf-8')
124
+ #------------------------------------------------------- ----------------Notre produit#_________________________________________________
125
+
126
+
127
+ warnings.filterwarnings("ignore", category=UndefinedMetricWarning)
128
+ set_config(display="text")
129
+
130
+ # ────────────────────────────────────────────────
131
+ # 1) FONCTION DE CHARGEMENT DU CSV BRUT
132
+ # ────────────────────────────────────────────────
133
+ @st.cache_data(show_spinner="🔄 Téléchargement du CSV…", ttl=None)
134
+ def load_raw_data() -> pd.DataFrame:
135
+ # url = (
136
+ # "https://projet-incendie.s3.eu-west-3.amazonaws.com/"
137
+ # "dataset_modele_decompte.csv"
138
+ # )
139
+ s3.download_file(os.getenv('S3_BUCKET'), 'compile/predictions_feu_2025.csv', 'predictions_feu_2025.csv')
140
+ return pd.read_csv('predictions_feu_2025.csv', sep=";")
141
+ def get_latest_model_key():
142
+ try:
143
+ response = s3.list_objects_v2(
144
+ Bucket=os.getenv('S3_BUCKET'),
145
+ Prefix='mlflow/models/'
146
+ )
147
+
148
+ if "Contents" not in response:
149
+ raise ValueError("Aucun modèle trouvé dans le bucket S3.")
150
+
151
+ # Filtrer uniquement les .joblib
152
+ models = [
153
+ obj for obj in response["Contents"]
154
+ if obj["Key"].endswith(".joblib")
155
+ ]
156
+
157
+ if not models:
158
+ raise ValueError("Aucun fichier .joblib trouvé.")
159
+
160
+ # Trier par LastModified (date d'upload dans S3)
161
+ models.sort(key=lambda x: x["LastModified"], reverse=True)
162
+
163
+ latest_key = models[0]["Key"]
164
+ print(f"Dernier modèle détecté : {latest_key}")
165
+ return latest_key
166
+
167
+ except Exception as e:
168
+ raise RuntimeError(f"Erreur récupération modèle S3 : {e}")
169
+
170
+ def load_model_from_s3(key: str | None = None):
171
+ bucket = os.getenv('S3_BUCKET')
172
+ if key is None:
173
+ key = get_latest_model_key()
174
+ response = s3.get_object(Bucket=bucket, Key=key)
175
+ buffer = io.BytesIO(response["Body"].read())
176
+ model = joblib.load(buffer)
177
+ return model
178
+
179
+
180
+ def load_baseline_S3(bucket: str, key: str):
181
+ bucket = os.getenv('S3_BUCKET')
182
+ BASELINE_KEY = "mlflow/models/baseline_c87008d290ec435eb93cc302e41ce934.pkl"
183
+ response = s3.get_object(Bucket=bucket, Key=BASELINE_KEY)
184
+ buffer = io.BytesIO(response["Body"].read())
185
+ baseline = pickle.load(buffer)
186
+ return baseline["baseline_survival"], baseline["baseline_hazard"]
187
+
188
+ # ────────────────────────────────────────────────
189
+ # 2) FONCTION D’ENTRAÎNEMENT + PRÉDICTIONS
190
+ # ────────────────────────────────────────────────
191
+ @st.cache_data(show_spinner="⚙️ Entraînement du modèle…", ttl=None)
192
+ def predict(df_raw: pd.DataFrame, model, baseline_survival) -> pd.DataFrame:
193
+ """Retourne df_map prêt pour la carte avec les colonnes
194
+ proba_7j, proba_30j, …, proba_180j."""
195
+ # a) Nettoyage
196
+ df = df_raw.copy()
197
+ df = df.rename(columns={"feu_prévu": "event", "décompte": "duration"})
198
+ df["event"] = df["event"].astype(bool)
199
+ df["duration"] = df["duration"].fillna(0)
200
+
201
+ # b) Features
202
+ features = [
203
+ "rr","um","tn","tx","jours_sans_pluie","jours_tx_sup_30",
204
+ "etpgrille_7j","compteur_jours_vers_prochain_feu", "moyenne_temperature_mois","moyenne_precipitations_mois","moyenne_vitesse_vent_mois","compteur_feu_log"
205
+ ]
206
+ features = [f.lower() for f in features]
207
+ features = [f for f in features if f in df.columns]
208
+
209
+ log_hr = model.predict(df[features])
210
+ HR = np.exp(log_hr)
211
+
212
+ horizons = {7: "proba_7j", 30: "proba_30j", 60: "proba_60j",
213
+ 90: "proba_90j", 180: "proba_180j"}
214
+
215
+ S0 = baseline_survival.squeeze()
216
+ def S0_at(t):
217
+ return np.interp(t, S0.index.values, S0.values)
218
+
219
+ for t, col in horizons.items():
220
+ S_t = (S0_at(t)) ** HR # survival
221
+ df[col] = 1 - S_t # prob event
222
+
223
+ df_map = df[["latitude_feu", "longitude_feu", "ville"] + list(horizons.values())].copy()
224
+ return df_map
225
+
226
+ # ────────────────────────────────────────────────
227
+ # 3) AFFICHAGE SUR LA PAGE « Accueil »
228
+ # ────────────────────────────────────────────────
229
+ if page == "Accueil":
230
+ st.title("Carte du risque d’incendie en Corse")
231
+
232
+ df_raw = load_raw_data()
233
+
234
+ latest_model_key = get_latest_model_key()
235
+ model = load_model_from_s3(latest_model_key)
236
+ baseline_survival, baseline_hazard = load_baseline_S3()
237
+
238
+ df_map = predict(df_raw, model, baseline_survival)
239
+
240
+ horizons_lbl = {
241
+ "7 jours": "proba_7j",
242
+ "30 jours": "proba_30j",
243
+ "60 jours": "proba_60j",
244
+ "90 jours": "proba_90j",
245
+ "180 jours": "proba_180j",
246
+ }
247
+ choix = st.radio(
248
+ "Choisis l’horizon temporel :",
249
+ list(horizons_lbl.keys()),
250
+ horizontal=True,
251
+ index=0,
252
+ )
253
+ col_proba = horizons_lbl[choix]
254
+
255
+ # Palette dynamique
256
+ vmax = float(df_map[col_proba].max())
257
+ fig = px.scatter_mapbox(
258
+ df_map,
259
+ lat="latitude_feu",
260
+ lon="longitude_feu",
261
+ hover_name="ville",
262
+ hover_data={col_proba: ":.2%"},
263
+ color=col_proba,
264
+ color_continuous_scale="YlOrRd", # jaune → orange → rouge
265
+ range_color=(0.0, vmax),
266
+ zoom=7,
267
+ height=650,
268
+ )
269
+ fig.update_layout(
270
+ mapbox_style="open-street-map",
271
+ margin=dict(l=0, r=0, t=0, b=0),
272
+ coloraxis_colorbar=dict(title="Probabilité", tickformat=".0%"),
273
+ )
274
+
275
+ st.subheader(f"Risque d’incendie – horizon **{choix}**")
276
+ st.plotly_chart(fig, use_container_width=True)
277
+
278
+ # ---------------------------------------------------------------Carte des casernes de pompiers#______________________________________________
279
+
280
+
281
+ if page == "Accueil":
282
+
283
+ # Chargement des données des casernes
284
+ # df_casernes = pd.read_csv(
285
+ # 'https://projet-incendie.s3.eu-west-3.amazonaws.com/casernes_corses.csv',
286
+ # sep=',',
287
+ # encoding='utf8'
288
+ # )
289
+ s3.download_file(os.getenv('S3_BUCKET'), 'dataset/casernes_corses.csv', 'casernes_corses.csv')
290
+ df_casernes = pd.read_csv('casernes_corses.csv', sep=',', encoding='utf8')
291
+ # Nettoyage des coordonnées
292
+ df_casernes['latitude'] = df_casernes['latitude'].astype(str).str.replace(',', '.').astype(float)
293
+ df_casernes['longitude'] = df_casernes['longitude'].astype(str).str.replace(',', '.').astype(float)
294
+ df_casernes = df_casernes.dropna(subset=['latitude', 'longitude'])
295
+
296
+ # Catégorisation des casernes
297
+ df_casernes['categorie'] = np.select(
298
+ [
299
+ df_casernes['nom'].str.contains('centre', case=False, na=False),
300
+ df_casernes['nom'].str.contains('base', case=False, na=False),
301
+ df_casernes['nom'].str.contains('SSLIA', case=False, na=False),
302
+ df_casernes['nom'].str.contains('citerne', case=False, na=False),
303
+ df_casernes['nom'].str.contains('borne', case=False, na=False),
304
+ ],
305
+ [
306
+ "Centre d'incendie et de secours",
307
+ 'Base forestière',
308
+ 'SSLIA (aérodromes)',
309
+ 'Citerne',
310
+ 'Borne incendie'
311
+ ],
312
+ default='Autre'
313
+ )
314
+
315
+ # Dictionnaire d'emojis
316
+ emoji_legende = {
317
+ "Centre d'incendie et de secours": "🚒",
318
+ "Base forestière": "🌲",
319
+ "SSLIA (aérodromes)": "✈️",
320
+ "Citerne": "💦"
321
+ }
322
+
323
+ # Carte centrée sur la Corse
324
+ m = folium.Map(location=[42.0396, 9.0129], zoom_start=8)
325
+
326
+ for _, row in df_casernes.iterrows():
327
+ emoji = emoji_legende.get(row['categorie'], '❓')
328
+ folium.Marker(
329
+ location=[row['latitude'], row['longitude']],
330
+ popup=f"{emoji} {row['nom']}",
331
+ icon=DivIcon(html=f"""<div style="font-size:24px">{emoji}</div>""")
332
+ ).add_to(m)
333
+
334
+ # Légende HTML
335
+ legend_html = """
336
+ {% macro html(this, kwargs) %}
337
+ <div style="
338
+ position: fixed;
339
+ bottom: 50px; left: 50px; width: 280px;
340
+ background-color: white;
341
+ border: 2px solid grey;
342
+ z-index: 9999;
343
+ font-size: 14px;
344
+ color: black;
345
+ padding: 10px;
346
+ border-radius: 10px;
347
+ box-shadow: 2px 2px 6px rgba(0,0,0,0.3);
348
+ ">
349
+ <b>📘 Légende</b><br>
350
+ 🚒 Centre d'incendie et de secours<br>
351
+ 🌲 Base forestière<br>
352
+ ✈️ SSLIA (aérodromes)<br>
353
+ 💦 Citerne<br>
354
+ </div>
355
+ {% endmacro %}
356
+ """
357
+
358
+ legend = MacroElement()
359
+ legend._template = Template(legend_html)
360
+
361
+ m.get_root().add_child(legend)
362
+
363
+ st.subheader("🗺️ Carte des casernes et équipements de lutte contre les incendies")
364
+ st_folium(m, width=1000, height=800)
365
+ #----------------------------------------------------------------------Page Notre Projet---------------------------------------------------
366
+ if page == "Notre Projet":
367
+ st.title("🔥 Projet Analyse des Incendies 🔥")
368
+
369
+ st.subheader(" 📊 Contexte")
370
+ st.subheader("🌲La forêt française en chiffres")
371
+
372
+ col1, col2 = st.columns([2, 1])
373
+ with col1:
374
+ st.markdown("""
375
+ La France est le 4ᵉ pays européen en superficie forestière, avec **17,5 millions d’hectares** en métropole (32 % du territoire) et **8 millions** en Guyane.
376
+ Au total, les forêts couvrent environ **41 %** du territoire national.
377
+
378
+ - **75 %** des forêts sont privées (3,5 millions de propriétaires).
379
+ - **16 %** publiques (collectivités).
380
+ - **9 %** domaniales (État).
381
+
382
+ La forêt française est un réservoir de biodiversité :
383
+ - **190 espèces d’arbres** (67 % feuillus, 33 % conifères).
384
+ - **73 espèces de mammifères**, **120 d’oiseaux**.
385
+ - Environ **30 000 espèces** de champignons et autant d’insectes.
386
+ - **72 %** de la flore française se trouve en forêt.
387
+
388
+ Les forêts françaises absorbent environ **9 %** des émissions nationales de gaz à effet de serre, jouant un rôle crucial dans la lutte contre le changement climatique.
389
+
390
+ Le Code forestier encadre leur gestion durable pour protéger la biodiversité, l’air, l’eau et prévenir les risques naturels.
391
+ """)
392
+
393
+ if page == "Notre Projet":
394
+ st.header("🔥 Corse : Bilan Campagne Feux de Forêts 2024")
395
+
396
+ # Tabs par grande section
397
+ tab1, tab2, tab3, tab4, tab5, tab6 = st.tabs([
398
+ "📌 Contexte", "🛠️ Prévention", "🚒 Moyens", "📊 Statistiques",
399
+ "🔍 Causes", "🔎 Enquêtes"
400
+ ])
401
+
402
+ with tab1:
403
+ with st.expander("📌 Contexte général"):
404
+ st.markdown("""
405
+ - **80 %** de la Corse est couverte de forêts/maquis → **fort risque incendie**
406
+ - **2023-2024** : la plus chaude et la plus sèche jamais enregistrée
407
+ - **714 mm** de pluie sur l’année (**78 %** de la normale)
408
+ - **Façade orientale** : seulement **30 %** des précipitations normales
409
+ """)
410
+
411
+ with tab2:
412
+ with st.expander("🛠️ Prévention & Investissements"):
413
+ st.markdown("""
414
+ - **1,9 million €** investis en 2023-2024 par l’État (jusqu’à 80 % de financement)
415
+ - Travaux financés :
416
+ - Pistes DFCI/DECI (Sorio di Tenda, Oletta, Île-Rousse…)
417
+ - Citernes souples & points d’eau
418
+ - Drones, caméras thermiques, logiciels SIG
419
+ - Véhicules pour réserves communales
420
+ """)
421
+
422
+ with tab3:
423
+ with st.expander("🚒 Moyens déployés"):
424
+ st.markdown("""
425
+ - Jusqu’à **500 personnels mobilisables**
426
+ - **168 sapeurs-pompiers SIS2B**, **261 UIISC5**, forestiers-sapeurs, gendarmerie, ONF…
427
+ - Moyens aériens :
428
+ - **1 hélico**, **2 canadairs** à Ajaccio
429
+ - **12 canadairs** + **8 Dashs** nationaux en renfort
430
+ """)
431
+
432
+ with tab4:
433
+ with st.expander("📊 Statistiques Feux Été 2024"):
434
+ st.markdown("""
435
+ - **107 feux** recensés (~9/semaine)
436
+ - **130 ha** brûlés dont :
437
+ - 83 % des feux <1 ha : **5,42 ha**
438
+ - 4 gros feux >10 ha : **72,84 ha**
439
+ - Linguizetta (**22,19 ha**), Oletta (**18,9 ha**), Pioggiola (**18,75 ha**), Tallone (**13 ha**)
440
+ - Depuis janvier 2024 : **285 feux** pour **587 ha**
441
+ - Feu majeur à Barbaggio : **195 ha** (33 % du total annuel)
442
+ """)
443
+
444
+ with tab5:
445
+ with st.expander("🔍 Causes des feux (38 cas identifiés)"):
446
+ st.markdown("""
447
+ - **11** : foudre
448
+ - **8** : écobuages
449
+ - **6** : malveillance
450
+ - **5** : accidents
451
+ - **4** : mégots de cigarette
452
+ """)
453
+
454
+ with st.expander("⚠️ Prévention = priorité absolue"):
455
+ st.markdown("""
456
+ - **90 %** des feux ont une origine humaine
457
+ - Causes principales : **imprudences** (mégots, BBQ, travaux, écobuages…)
458
+ """)
459
+
460
+ with tab6:
461
+ with st.expander("🔎 Enquêtes & Surveillance"):
462
+ st.markdown("""
463
+ - **20 incendies** étudiés par la Cellule Technique d’Investigation (CTIFF)
464
+ - Équipes mobilisées : **7 forestiers**, **15 pompiers**, **21 forces de l’ordre**
465
+ - **Fermeture de massif** enclenchée 1 seule fois : forêt de Pinia
466
+ """)
467
+ #---------------------------------------------------Equipe du projet---------------------------------------------------
468
+ st.subheader("👨‍💻 Équipe du projet")
469
+ col3= st.columns(1)
470
+ # col1, col2, col3 = st.columns(3)
471
+ # with col1:
472
+ # st.image("images/Faycal_Belambri.jpg", width=150)
473
+ # st.markdown("**Fayçal Belambri**\n\nData Scientist\n\nSpécialiste App Streamlit et visualisation")
474
+ # with col2:
475
+ # st.image("images/Joel_Termondjian.jpg", width=150)
476
+ # st.markdown("**Joël Termondjian**\n\nData Scientist\n\nResponsable des données\n\nPreprocessing\n\nData Enagineering")
477
+ with col3:
478
+ st.image("images/Marc_Barthes.jpg", width=150)
479
+ st.markdown("**Marc Barthes**\n\nData Scientist\n\nML Engineer\n\nExpert en modèles de prédiction")
480
+ #---------------------------------------------------Notre Objectif --------------------------------------------------------
481
+
482
+ st.subheader("🎯 Notre Objectif")
483
+ st.markdown("""
484
+ Dans un contexte de **changement climatique** et de **risques accrus d’incendies de forêt**, notre équipe a développé un projet innovant visant à **analyser et prédire les zones à risque d’incendie** en France, avec un focus particulier sur la **Corse**.
485
+ """)
486
+ #---------------------------------------------------Obectifs du projet---------------------------------------------------
487
+ col1, col2 = st.columns([1, 1])
488
+ with col1:
489
+ st.subheader("🔍 Exploration des données")
490
+ st.markdown("""
491
+ - ✅ **Évolution du nombre d’incendies**, répartition par mois et par causes.
492
+ - ✅ **Cartographie interactive** des incendies sur tout le territoire.
493
+ - ✅ **Analyse des clusters** grâce à DBSCAN pour identifier les zones les plus à risque.
494
+ """)
495
+
496
+ with col2:
497
+ st.subheader("📈 Modèles prédictifs")
498
+ st.markdown("""
499
+ - ✅ **Comparaison des modèles** : Random Forest, XGBoost, analyse de survie.
500
+ - ✅ **Prédiction des zones à risque** avec visualisation sur carte.
501
+ - ✅ Fourniture d'un **outil décisionnel** pour les autorités et les services de gestion des risques.
502
+ """)
503
+
504
+ st.subheader("📘 Définition de l'analyse de survie (Survival Analysis")
505
+ col1, col2, col3, col4 = st.columns(4)
506
+
507
+ with col1:
508
+ st.markdown("### 🧠 Qu’est-ce que l’analyse de survie ?")
509
+ st.markdown("""
510
+ L’**analyse de survie** (ou **Survival Analysis**) est une méthode statistique utilisée pour **modéliser le temps avant qu’un événement se produise**, comme :
511
+ - 🔥 un incendie,
512
+ - 🏥 un décès,
513
+ - 📉 une résiliation d’abonnement,
514
+ - 🧯 une panne.
515
+ """)
516
+
517
+ with col2:
518
+ st.markdown("### 📌 Objectif :")
519
+ st.markdown("""
520
+ > Estimer la **probabilité qu’un événement ne se soit pas encore produit** à un instant donné.
521
+ """)
522
+
523
+ with col3:
524
+ st.markdown("### 🔑 Concepts fondamentaux : ")
525
+ st.markdown("""
526
+ - ⏳ **Temps de survie (`T`)** : temps écoulé jusqu’à l’événement.
527
+ - 🎯 **Événement** : le phénomène qu’on cherche à prédire (feu, panne, décès...).
528
+ - ❓ **Censure** : l’événement **n’a pas encore eu lieu** durant la période d’observation.
529
+ - 📉 **Fonction de survie `S(t)`** : probabilité de "survivre" après le temps `t`.
530
+ - ⚠️ **Fonction de risque `h(t)`** : probabilité que l’événement se produise **immédiatement après `t`**, sachant qu’il ne s’est pas encore produit.
531
+ """)
532
+
533
+ with col4:
534
+ st.markdown ("### 🧪 Exemples d’applications :")
535
+ st.markdown("""
536
+ | Domaine | Exemple |
537
+ |--------|---------|
538
+ | 🔥 Incendies | Quand un feu va-t-il se déclarer ? |
539
+ | 🏥 Santé | Combien de temps un patient survivra après traitement ? |
540
+ | 📉 Marketing | Quand un client risque-t-il de partir ? |
541
+ | 🧑‍💼 RH | Quand un salarié quittera-t-il l’entreprise ? |
542
+
543
+ """)
544
+
545
+ show_footer()
546
+
547
+ #---------------------------------------------------# Page EDA -----------------------------------------------------------------
548
+
549
+ if page == "Exploration des données":
550
+ st.title("🗺️ Visualisation des incendies entre 2006 et 2024")
551
+
552
+ df = load_data()
553
+ coords = load_coords()
554
+ df_merge = load_df_merge()
555
+
556
+ st.subheader("Aperçu des coordonnées des villes")
557
+
558
+ fig = px.scatter_map(
559
+ coords,
560
+ lat="latitude",
561
+ lon="longitude",
562
+ hover_name="ville",
563
+ height=800,
564
+ zoom=5,
565
+ map_style="carto-positron",
566
+ title="Carte interactive des communes (coordonnées)"
567
+ )
568
+ st.plotly_chart(fig, use_container_width=True)
569
+
570
+ #---------------------------------------------------# DBSCAN Clustering---------------------------------------------------
571
+ st.subheader("🔥 Détection des clusters d'incendies avec DBSCAN")
572
+
573
+ commune_counts = df_merge.groupby(['Nom de la commune', 'latitude', 'longitude']).size().reset_index(name='frequence')
574
+ df_expanded = commune_counts.loc[commune_counts.index.repeat(commune_counts['frequence'])].reset_index(drop=True)
575
+
576
+ X = df_expanded[['latitude', 'longitude']]
577
+ coords_rad = np.radians(X)
578
+ kms_per_radian = 6371.0088
579
+ eps_km = 5
580
+ eps = eps_km / kms_per_radian
581
+
582
+ db = DBSCAN(eps=eps, min_samples=20, metric='haversine').fit(coords_rad)
583
+ df_expanded['cluster'] = db.labels_
584
+
585
+ clustered_data = df_expanded[df_expanded['cluster'] != -1]
586
+
587
+ fig = px.scatter_map(
588
+ clustered_data,
589
+ lat="latitude",
590
+ lon="longitude",
591
+ color="cluster",
592
+ hover_name="Nom de la commune",
593
+ zoom=5,
594
+ height=900,
595
+ title="🔥 Clusters d'incendies en France (2006-2024) détectés par DBSCAN",
596
+ map_style="carto-positron"
597
+ )
598
+ st.plotly_chart(fig, use_container_width=True)
599
+
600
+
601
+ #---------------------------------------------------Histogramme mensuel#---------------------------------------------------
602
+ st.title("Comparaison mensuelle des incendies par année")
603
+
604
+ df_temp = df_merge.copy()
605
+ df_temp['Date'] = pd.to_datetime(df_temp['Date'], errors='coerce')
606
+ df_temp = df_temp.dropna(subset=['Date'])
607
+
608
+ df_temp['mois'] = df_temp['Date'].dt.month
609
+ df_temp['année'] = df_temp['Date'].dt.year
610
+
611
+ mois_abbr = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
612
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
613
+ df_temp['mois_nom'] = df_temp['mois'].apply(lambda x: mois_abbr[x - 1])
614
+ df_temp['mois_nom'] = pd.Categorical(df_temp['mois_nom'], categories=mois_abbr, ordered=True)
615
+
616
+ df_grouped = df_temp.groupby(['mois_nom', 'année']).size().reset_index(name='nombre_feux')
617
+
618
+ fig = px.bar(
619
+ df_grouped,
620
+ x='mois_nom',
621
+ y='nombre_feux',
622
+ color='année',
623
+ barmode='group',
624
+ title='Comparaison mensuelle des incendies par année',
625
+ height=600,
626
+ width=1000
627
+ )
628
+ st.plotly_chart(fig, use_container_width=True)
629
+
630
+ show_footer()
631
+
632
+ #--------------------------------------------------- Analyse des causes---------------------------------------------------
633
+ causes = df_merge['Nature'].value_counts()
634
+ st.subheader("Répartition des causes d'incendies")
635
+ fig, ax = plt.subplots(figsize=(8, 6))
636
+ ax.pie(
637
+ causes.values,
638
+ labels=causes.index,
639
+ autopct='%1.1f%%',
640
+ startangle=140,
641
+ shadow=True,
642
+ explode=[0.05]*len(causes)
643
+ )
644
+ ax.set_title("Répartition des causes d'incendies")
645
+ ax.axis('equal')
646
+ st.pyplot(fig)
647
+
648
+ #---------------------------------------------------- Nombre total d’incendies par année-----------------------------------------
649
+
650
+ if page == "Exploration des données":
651
+ st.title("Analyse des incendies par année 🔥")
652
+
653
+ # Copie du DataFrame
654
+ df_temp = df_merge.copy()
655
+
656
+ # Conversion de la colonne Date
657
+ df_temp['Date'] = pd.to_datetime(df_temp['Date'])
658
+ df_temp['année'] = df_temp['Date'].dt.year
659
+
660
+ # Regroupement par année uniquement
661
+ df_grouped = df_temp.groupby('année').size().reset_index(name='nombre_feux')
662
+
663
+ # Création du graphique en barres
664
+ fig = px.bar(
665
+ df_grouped,
666
+ x='année',
667
+ y='nombre_feux',
668
+ title='Nombre total d’incendies par année',
669
+ height=600,
670
+ width=1200,
671
+ text='nombre_feux'
672
+ )
673
+
674
+ fig.update_xaxes(
675
+ tickmode='linear',
676
+ dtick=1 # une année à chaque tick
677
+ )
678
+
679
+ fig.update_layout(
680
+ xaxis_title='Année',
681
+ yaxis_title='Nombre de feux',
682
+ xaxis_tickangle=0
683
+ )
684
+
685
+ st.plotly_chart(fig)
686
+ #---------------------------------------------------- Page Exploration des données -----------------------------------------
687
+
688
+ if page == "Exploration des données":
689
+
690
+ #---------------------------------------------------- Les 10 départements avec le plus d’incendies -----------------------------------------
691
+
692
+ # Copie du DataFrame
693
+ df_temp = df_merge.copy()
694
+
695
+ # Regroupement par département
696
+ df_grouped = df_temp.groupby('Département').size().reset_index(name='nombre_feux')
697
+
698
+ # Classement décroissant et sélection du top 10
699
+ df_top10 = df_grouped.sort_values(by='nombre_feux', ascending=False).head(10)
700
+
701
+ # Graphique en barres
702
+ fig = px.bar(
703
+ df_top10,
704
+ x='Département',
705
+ y='nombre_feux',
706
+ title='Les 10 départements avec le plus d’incendies',
707
+ height=600,
708
+ width=1000,
709
+ text='nombre_feux'
710
+ )
711
+
712
+ # Fond clair
713
+ fig.update_layout(
714
+ template='plotly_white',
715
+ xaxis_title='Département',
716
+ yaxis_title='Nombre de feux',
717
+ xaxis_tickangle=-45,
718
+ )
719
+
720
+ # Texte au-dessus des barres
721
+ fig.update_traces(textposition='outside')
722
+
723
+ # Affichage dans l'app
724
+ st.plotly_chart(fig)
725
+
726
+
727
+ #---------------------------------------------------- Les 10 départements les plus touchés -----------------------------------------
728
+
729
+ if page == "Exploration des données":
730
+
731
+ # 🔎 Vérification rapide du DataFrame
732
+ if "Département" not in df_merge.columns:
733
+ st.error("❌ La colonne 'Département' est absente du DataFrame.")
734
+ elif df_merge.empty:
735
+ st.warning("⚠️ Le DataFrame est vide.")
736
+ else:
737
+ # ✅ Copie et nettoyage du DataFrame
738
+ df_temp = df_merge.copy()
739
+ df_temp = df_temp[df_temp['Département'].notna()] # Supprime les lignes sans département
740
+
741
+ # 📊 Regroupement par département
742
+ df_grouped = df_temp.groupby('Département').size().reset_index(name='nombre_feux')
743
+
744
+ # 🔢 Total général
745
+ total_feux = df_grouped['nombre_feux'].sum()
746
+
747
+ # 🔝 Top 10 des départements
748
+ df_top10 = df_grouped.sort_values(by='nombre_feux', ascending=False).head(10)
749
+
750
+ # 📈 Calcul des proportions
751
+ df_top10['proportion_totale'] = df_top10['nombre_feux'] / total_feux
752
+
753
+ # 🥧 Création du graphique circulaire
754
+ fig_pie = px.pie(
755
+ df_top10,
756
+ names='Département',
757
+ values='nombre_feux',
758
+ title='Les 10 départements les plus touchés (proportion sur le total global)',
759
+ )
760
+ fig_pie.update_traces(textinfo='label+percent')
761
+
762
+ # 📌 Affichage dans l'app
763
+ st.plotly_chart(fig_pie)
764
+
765
+ #---------------------------------------------------- Carte des feux par département (2006-2024) ---------------------------------------------------------
766
+
767
+ if page == "Exploration des données":
768
+ st.subheader("Carte des feux par département (2006-2024)")
769
+
770
+ # Copie du dataset
771
+ df_temp = df_merge.copy()
772
+
773
+ # Codes départements formatés
774
+ df_temp['Département'] = df_temp['Département'].astype(str).str.zfill(2)
775
+ df_grouped = df_temp.groupby('Département').size().reset_index(name='nombre_feux')
776
+
777
+ # Chargement GeoJSON
778
+ url_geojson = 'https://raw.githubusercontent.com/gregoiredavid/france-geojson/master/departements-version-simplifiee.geojson'
779
+ with urllib.request.urlopen(url_geojson) as response:
780
+ departements_geojson = json.load(response)
781
+
782
+ # Carte choroplèthe
783
+ fig = px.choropleth(
784
+ df_grouped,
785
+ geojson=departements_geojson,
786
+ locations='Département',
787
+ featureidkey='properties.code',
788
+ color='nombre_feux',
789
+ color_continuous_scale='OrRd',
790
+ title='Total des feux par département (2006-2024)',
791
+ labels={'nombre_feux': 'Feux'},
792
+ )
793
+
794
+ # Style géographique
795
+ fig.update_geos(
796
+ visible=False,
797
+ lataxis_range=[41, 52],
798
+ lonaxis_range=[-5.5, 10],
799
+ showcountries=False,
800
+ showcoastlines=False,
801
+ showland=True,
802
+ landcolor='white',
803
+ fitbounds="locations"
804
+ )
805
+
806
+ # Mise en page
807
+ fig.update_layout(
808
+ template='plotly_white',
809
+ width=1000,
810
+ height=700,
811
+ margin=dict(l=0, r=20, t=40, b=0),
812
+ coloraxis_colorbar=dict(
813
+ title="Feux",
814
+ thickness=15,
815
+ len=0.4,
816
+ y=0.5
817
+ )
818
+ )
819
+
820
+ # Affichage Streamlit
821
+ st.plotly_chart(fig)
822
+ #---------------------------------------------------- Les 10 départements avec le plus d’incendies -----------------------------------------
823
+ # import plotly.graph_objects as go
824
+
825
+ # -----------------------------------------------------------
826
+ # 🔥 Top-10 des départements par nombre de feux – version GO
827
+ # -----------------------------------------------------------
828
+ if page == "Exploration des données":
829
+ df_temp = df_merge.copy()
830
+ df_temp["Département"] = df_temp["Département"].replace(
831
+ {"2A": "2A/2B", "2B": "2A/2B"}
832
+ )
833
+
834
+ df_count = (
835
+ df_temp.groupby("Département")
836
+ .size()
837
+ .reset_index(name="Nombre de feux")
838
+ .sort_values("Nombre de feux", ascending=False)
839
+ .head(10)
840
+ )
841
+
842
+ # ── Barres avec labels (Graph Objects)
843
+ fig_top10_feux = go.Figure(
844
+ data=go.Bar(
845
+ x=df_count["Département"],
846
+ y=df_count["Nombre de feux"],
847
+ text=df_count["Nombre de feux"].apply(lambda x: f"{x:,}"),
848
+ textposition="outside",
849
+ textfont=dict(size=16, color="#2e2e2e"), # police foncée
850
+ marker=dict(
851
+ color="#627CFF",
852
+ line=dict(color="black", width=1.5),
853
+ ),
854
+ )
855
+ )
856
+
857
+ # ── Mise en page inspirée de ta Fig 1
858
+ fig_top10_feux.update_layout(
859
+ title="🔥 Top 10 des départements avec le plus d’incendies",
860
+ title_font_size=28,
861
+ template="plotly_white", # fond clair + grille
862
+ plot_bgcolor="rgba(245,248,255,1)",
863
+ paper_bgcolor="rgba(245,248,255,1)",
864
+ margin=dict(l=80, r=80, t=110, b=120),
865
+ font=dict(size=18, color="#2e2e2e"), # police par défaut foncée
866
+ xaxis=dict(
867
+ title="Département",
868
+ tickangle=-35,
869
+ tickfont=dict(size=16),
870
+ ),
871
+ yaxis=dict(
872
+ title="Nombre de feux",
873
+ tickformat=",d",
874
+ tickfont=dict(size=16),
875
+ ),
876
+ bargap=0.05,
877
+ )
878
+
879
+ st.plotly_chart(fig_top10_feux, use_container_width=True)
880
+
881
+
882
+ # #---------------------------------------------------- Page Résultats des modèles -----------------------------------------
883
+
884
+ # elif page == "Résultats des modèles":
885
+ # st.title("📈 Résultats des modèles prédictifs")
886
+ # st.markdown("### Comparaison des modèles de Survival Analysis")
887
+
888
+ # #--------------------------------------------------- Tableau codé en dur en Markdown -----------------------------------
889
+ # st.markdown("""
890
+ # | Modèle | Concordance Index |
891
+ # |-----------------------------------|-------------------|
892
+ # | Predict survival fonction (MVP) | 0.69 |
893
+ # | XGBOOST survival cox | 0.809 |
894
+ # """)
895
+
896
+ # st.markdown("👉 Le modèle **XGBOOST survival cox** obtient la meilleure performance globale.")
897
+
898
+ # show_footer()
requirements.txt CHANGED
@@ -1,3 +1,22 @@
1
- altair
2
  pandas
3
- streamlit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  pandas
2
+ numpy
3
+ scikit-learn
4
+ matplotlib
5
+ seaborn
6
+ streamlit
7
+ scipy
8
+ plotly
9
+ streamlit
10
+ tqdm
11
+ statsmodels
12
+ requests
13
+ tqdm
14
+ folium
15
+ streamlit_folium
16
+ scikit-survival==0.22.0
17
+ geopandas
18
+ streamlit-folium
19
+ joblib
20
+ xgboost
21
+ lifelines
22
+ xgbse