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