File size: 14,375 Bytes
0b59a26 353750d 0b59a26 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 | import streamlit as st
import pandas as pd
import pickle
import matplotlib.pyplot as plt
from dotenv import load_dotenv
import os
import numpy as np
import shap
from sklearn.linear_model import LinearRegression
from langchain_mistralai import ChatMistralAI
from langchain_core.output_parsers import StrOutputParser
# partie initialisation
model_paths = {
"Deblais et Gravats": "model_paths/model_Deblais_gravats.pkl",
"Dechets verts": "model_paths/model_Dechets_verts.pkl",
"Encombrants": "model_paths/model_Encombrants.pkl",
"Materiaux recyclables": "model_paths/model_Materiaux_recyclables.pkl"
}
col_mapping = {
"Deblais et Gravats": "Deblais_gravats",
"Dechets verts": "Dechets_verts",
"Encombrants": "Encombrants",
"Materiaux recyclables": "Materiaux_recyclables"
}
default_dept = "Aisne"
valeurs_observees = []
valeurs_predites = []
labels = []
categories = {
"📊 Population": [
"pop_globale",
"tranche_age_0-24", "tranche_age_25-59", "tranche_age_60+",
"csp1_agriculteurs", "csp2_artisans_commerçant_chef_entreprises",
"csp3_cadres_professions_intellectuelles", "csp4_professions_intermediaires",
"csp5_employes", "csp6_ouvriers", "csp7_retraites", "csp8_sans_activite",
"densite"
],
"🏭 Activite economique": [
"nbre_entreprises", "nbre_entreprises_agricole", "nb_salaries_secteur_agricole",
"nbre_entreprises_industrie", "nb_salaries_secteur_industrie",
"nb_salaries_secteur_service", "nbre_entreprises_service"
]
}
# Pour la gestion automatique du run eval uniquement au demarrage et en cas de changement de departement
# sinon necessaire de cliquer sur Lancer l'evaluation
if "previous_dept" not in st.session_state:
st.session_state["previous_dept"] = None
if "auto_run_done" not in st.session_state:
st.session_state["auto_run_done"] = False
def run_eval(selected_dept, form_input):
# Transformation en DataFrame
input_df = pd.DataFrame([form_input])
input_df_complete = row_default.to_frame().T.copy()
for col in input_df.columns:
if col in input_df_complete.columns:
input_df_complete.at[input_df_complete.index[0], col] = input_df.at[0, col]
# Verification des incoherences
Liste_age = ["pop_globale", "tranche_age_0-24", "tranche_age_25-59", "tranche_age_60+"]
Somme = 0
if all(v in form_input for v in Liste_age):
for elt in Liste_age:
if elt != "pop_globale":
Somme += form_input[elt]
if abs(form_input["pop_globale"] - Somme) > 1:
st.error(f"❌ Incoherence : Population globale = {form_input['pop_globale']} ne correspond pas à la somme des tranches d'âge : {Somme}")
Liste_CSP = ["pop_globale","csp1_agriculteurs", "csp2_artisans_commercant_chef_entreprises",
"csp3_cadres_professions_intellectuelles", "csp4_professions_intermediaires",
"csp5_employes", "csp6_ouvriers", "csp7_retraites", "csp8_sans_activite"]
Somme = 0
if all(v in form_input for v in Liste_CSP):
for elt in Liste_CSP:
if elt != "pop_globale":
Somme += form_input[elt]
if abs(form_input["pop_globale"] - Somme) >1:
st.error(f"❌ Incoherence : Population globale = {form_input['pop_globale']} ne correspond pas à la somme des CSP : {Somme}")
Liste_Entreprise = ["nbre_entreprises", "nbre_entreprises_agricole",
"nbre_entreprises_industrie", "nbre_entreprises_service"]
Somme = 0
if all(v in form_input for v in Liste_Entreprise):
for elt in Liste_Entreprise:
if elt != "nbre_entreprises":
Somme += form_input[elt]
if abs(form_input["nbre_entreprises"] - Somme) > 1:
st.error(f"❌ Incoherence : Le nombre d'entreprises = {form_input['nbre_entreprises']} ne correspond pas à la somme des types d'entreprise : {Somme}")
# on reinitialise pour que ça ne se lance pas automatiquement (lourd)
evaluation = False
for typologie, path in model_paths.items():
try:
if os.path.exists(path):
with open(path, "rb") as f:
model = pickle.load(f)
else:
st.error(f"❌ Modèle manquant : {path}")
expected_cols = model.model.exog_names
if "const" in expected_cols and "const" not in input_df_complete.columns:
input_df_complete["const"] = 1.0
prediction = max(0, model.predict(input_df_complete[expected_cols]).iloc[0])
valeurs_predites.append(prediction)
labels.append(typologie)
filtered = observed_df[
(observed_df["Departement"] == selected_dept) & (observed_df["annee"] == 2021)
]
excel_col = col_mapping.get(typologie)
if not filtered.empty and excel_col in filtered.columns:
valeurs_observees.append(filtered[excel_col].values[0])
else:
valeurs_observees.append(0.0)
except Exception as e:
st.error(f"Erreur avec le modèle {typologie}")
st.exception(e)
# === Chargement des donnees
df = pd.read_csv(
"Data/df_dummies.csv").drop(columns=["Unnamed: 0"], errors="ignore")
observed_df = pd.read_csv("Data/data_wip.csv")
# === Liste des departements
departements = [col.replace("Departement_", "") for col in df.columns if col.startswith("Departement_")]
# === Mise en page
st.set_page_config(layout="wide")
st.markdown("<h1 style='text-align: left;'>♻️ Simulateur de production de dechets par departement</h1>", unsafe_allow_html=True)
st.markdown("<h3 style='text-align: left;'>📍 Choix du departement</h3>", unsafe_allow_html=True)
st.markdown("<h3 style='text-align: left;'>📈 Comparaison entre valeurs observees et predites</h3>", unsafe_allow_html=True)
st.markdown("<div style='text-align:left: 60px;'></div>", unsafe_allow_html=True)
selected_dept = st.selectbox("Selectionner un departement", sorted(departements), index=sorted(departements).index(default_dept))
row_default = df[df[f"Departement_{selected_dept}"] == 1].iloc[0]
default_dict = row_default.to_dict()
st.subheader("⚙️ Paramètres modifiables")
form_input = {}
for category_name, variables in categories.items():
with st.expander(category_name, expanded=False):
for i, var in enumerate(variables):
if var in default_dict:
if category_name == "📊 Population":
default_value = int(round(float(default_dict[var])/100)*100)
else:
default_value = int(round(float(default_dict[var])/10)*10)
val = st.number_input(
f"✏️ {var}",
min_value=0,
value=default_value,
step=1,
format="%d",
key=f"number_input_{selected_dept}_{var}"
)
form_input[var] = val
st.markdown("<div style='margin-bottom: 10px;'></div>", unsafe_allow_html=True)
if st.session_state["previous_dept"] != selected_dept or not st.session_state["auto_run_done"]:
st.session_state["previous_dept"] = selected_dept
st.session_state["auto_run_done"] = True
run_eval(selected_dept, form_input)
# with chart_col:
st.markdown("<div style='text-align:center: 30px;'></div>", unsafe_allow_html=True)
# btn_col = st.columns([3, 2, 3])[1]
# with btn_col:
evaluation = st.button("🔍 Lancer l'evaluation")
st.markdown("<div style='text-align:center: 40px;'></div>", unsafe_allow_html=True)
if evaluation:
run_eval(selected_dept, form_input)
st.session_state["auto_run_done"] = True
if valeurs_observees and valeurs_predites:
x = np.arange(len(labels))
width = 0.4
fig, ax = plt.subplots(figsize=(10, 6))
bars1 = ax.bar(x - width / 2, valeurs_observees, width, label='Observé (2021)', color='steelblue')
bar_colors = [(1, 0, 0, 0.6) if pred > obs else (0, 0.6, 0, 0.6)
for pred, obs in zip(valeurs_predites, valeurs_observees)]
bars2 = ax.bar(x + width / 2, valeurs_predites, width, label='Prevision', color=bar_colors)
for i in range(len(labels)):
ax.text(x[i] - width / 2, valeurs_observees[i] + max(valeurs_observees) * 0.01, f"{valeurs_observees[i]:,.0f}",
ha='center', va='bottom', fontsize=9)
ax.text(x[i] + width / 2, valeurs_predites[i] + max(valeurs_predites) * 0.01, f"{valeurs_predites[i]:,.0f}",
ha='center', va='bottom', fontsize=9)
ax.set_ylabel("Tonnes")
ax.set_title("Comparaison Observe vs Predit")
ax.set_xticks(x)
ax.set_xticklabels(labels, rotation=45, ha='right')
ax.legend()
st.pyplot(fig)
# === Graphiques SHAP ===
st.markdown("---")
st.subheader(f"📉 SHAP - Analyse des contributions pour le departement : {selected_dept}")
# Menu deroulant
selected_typologie = st.selectbox("Choisissez une typologie de dechets à analyser avec SHAP :", list(model_paths.keys()))
# SHAP pour la typologie selectionnee
typologie = selected_typologie
path = model_paths[typologie]
st.markdown(f"### 🔍 {typologie}")
try:
with open(path, "rb") as f:
model_sm = pickle.load(f)
used_features = model_sm.model.exog_names
used_features_no_const = [f for f in used_features if f != "const"]
X_used = df[used_features_no_const].copy()
if "const" in used_features:
X_used["const"] = 1.0
intercept = model_sm.params['const'] if 'const' in model_sm.params else 0
coefs = model_sm.params[used_features_no_const].values
lr = LinearRegression()
lr.intercept_ = intercept
lr.coef_ = coefs
lr.feature_names_in_ = np.array(used_features_no_const)
X_used_corrected = X_used.reindex(columns=lr.feature_names_in_, fill_value=0)
explainer = shap.Explainer(lr, X_used_corrected)
shap_values = explainer(X_used_corrected)
selected_index = df[df[f"Departement_{selected_dept}"] == 1].index[0]
# === Première ligne : Waterfall + Beeswarm + Moyenne des contributions
exclude_vars = [
"Deblais_gravats", "Dechets_verts", "Encombrants", "Materiaux_recyclables"
]
exclude_vars += [
name for name in shap_values.feature_names
if name.startswith(("Departement_", "Region_"))
]
# Creation d’un masque pour filtrer les SHAP plots sans toucher à la prediction
mask = np.array([name not in exclude_vars for name in shap_values.feature_names])
filtered_shap = shap.Explanation(
values=shap_values.values[:, mask],
base_values=shap_values.base_values,
data=shap_values.data[:, mask],
feature_names=[name for name in shap_values.feature_names if name not in exclude_vars]
)
col1 = st.columns(1)[0]
with col1:
st.markdown("<h6 style='text-align: center;'> Waterfall</h6>", unsafe_allow_html=True)
fig = plt.figure(figsize=(3, 2))
shap.plots.waterfall(filtered_shap[selected_index], max_display=10, show=False)
st.pyplot(fig, bbox_inches='tight', dpi=200, clear_figure=True)
except Exception as e:
st.error(f"Erreur dans le SHAP pour {typologie}")
st.exception(e)
# === 🧠 Explication automatique avec Mistral ===
# 1. Recuperation des moyennes absolues des SHAP values
shap_local = filtered_shap[selected_index]
# 2. Creation d’un resume lisible des coefficients (tries par impact)
sorted_indices = np.argsort(np.abs(shap_local.values))[::-1]
top_n = 10
list_coef = "\n".join([
f"{shap_local.feature_names[i]}: {shap_local.values[i]:.2f}"
for i in sorted_indices[:top_n]
])
# 3. Prompt + contexte
prompt_template = f"""
Tu es un expert en data science et en statistique, specialise dans l'interpretation des resultats de modèles explicatifs à l'aide des coefficients de Shapley.
Je vais te fournir les valeurs des coefficients de Shapley pour un modèle lineaire de regression, associes à chaque variable explicative.
Ta mission :
Redige un paragraphe clair et synthetique, de 1500 caractères maximum, interpretant le rôle des variables dans le modèle pour repondre au besoin de notre client qui sont les presidents des conseils departementaux. Tu dois identifier :
- Les variables qui ont le plus d’impact positif ou negatif sur la variable cible.
- Les grandes tendances demographiques ou economiques qui expliquent la production de dechets.
- Une interpretation comprehensible par un public non-expert, mais avec une rigueur statistique.
Voici les coefficients :
{list_coef}
Contexte :
- Objectif : Comprendre comment les caracteristiques demographiques et economiques influencent la production des differents types de dechets en France.
- Variable cible : {typologie}
- Modèle utilisé : OLS de Statsmodel avec coefficients de Shapley.
- Variables explicatives :
- Secteurs d’activites : Nombre de salaries par secteur : Agricole, Service, Industrie.
- Profils socioprofessionnels (CSP) :
csp1_agriculteurs, csp2_artisans_commerçant_chef_entreprises, csp3_cadres_professions_intellectuelles, csp4_professions_intermediaires, csp5_employes, csp6_ouvriers, csp7_retraites, csp8_sans_activite.
- Tranches d’âge : tranche_age_0-24, tranche_age_25-59, tranche_age_60+.
- Autres variables :
Densite de population, Population globale, Typologie d'entreprises
- tu ne dois pas prendre en compte les {exclude_vars} dans ton analyse
"""
# 4. Appel à l’API Mistral
# Charge les variables d'environnement à partir du fichier .env
load_dotenv()
api_key = os.getenv("MISTRAL_API_KEY")
#st.write(f"Clé API : `****-****-****-{api_key[-4:]}`")
try:
with st.spinner("🧠 Generation de l'interpretation avec Mistral..."):
# model_llm = ChatMistralAI(model="mistral-large-latest", mistral_api_key=api_key)
model_llm = ChatMistralAI(model="mistral-small-latest", mistral_api_key=api_key)
parser = StrOutputParser()
response = model_llm.invoke(prompt_template)
explanation_text = parser.invoke(response)
# 5. Affichage dans l’interface Streamlit
st.markdown("#### 🤖 Interpretation automatique (LLM)")
st.success(explanation_text)
except Exception as e:
st.error("Erreur lors de l'appel au LLM Mistral.")
st.exception(e)
|