GAIA26CCPA / src /app.py
JosephMcDonnell's picture
update structure layout for deployment
e9acbc1
Raw
History Blame
16.2 kB
"""
app.py - Application Streamlit pour la détermination de l'impact carbone
des matières premières pour aliments composés.
Lancement : streamlit run app.py
"""
import streamlit as st
import pandas as pd
from flowchart_engine import evaluate_carbon_impact, CarbonResult
import data_loader
import config
# ============================================================================
# Configuration de la page
# ============================================================================
st.set_page_config(
page_title="POC GAIA - Impact Carbone MP",
page_icon="🌿",
layout="wide",
)
st.title("🌿 POC GAIA — Impact Carbone des Matières Premières")
st.markdown("""
**Logigramme d'aide pour faciliter l'application du Guide de calcul de l'impact carbone
des aliments composés** *(GT Carbone — hors produits dérivés du soja)*
Renseignez les informations sur votre matière première ci-dessous, puis lancez l'évaluation.
""")
st.divider()
# ============================================================================
# Listes de suggestions (chargées une fois, mises en cache par Streamlit)
# ============================================================================
@st.cache_data
def get_country_list():
"""Retourne la liste des noms de pays disponibles (clés du mapping FR→ISO)."""
noms = sorted(set(k.title() for k in config.PAYS_FR_TO_ISO.keys()))
return noms
@st.cache_data
def get_matiere_list():
"""Retourne la liste des matières premières disponibles dans EcoALIM."""
return data_loader.get_ecoalim_matieres()
# ============================================================================
# Composant autocomplete maison
# ============================================================================
def autocomplete_input(label: str, options: list[str], key: str,
placeholder: str = "", help_text: str = "",
max_suggestions: int = 8) -> str:
"""
Champ texte libre avec suggestions filtrées cliquables en dessous.
L'utilisateur peut taper librement ou cliquer une suggestion.
"""
# Si une suggestion a été cliquée au tour précédent, injecter la valeur
sel_key = f"{key}__sel"
if sel_key in st.session_state:
st.session_state[key] = st.session_state.pop(sel_key)
typed = st.text_input(label, key=key, placeholder=placeholder, help=help_text)
if typed and len(typed) >= 2:
typed_lower = typed.lower().strip()
# Filtrer + trier : commence par > contient, puis longueur croissante
matches = [
o for o in options
if typed_lower in o.lower()
]
matches.sort(key=lambda x: (not x.lower().startswith(typed_lower), len(x)))
matches = matches[:max_suggestions]
# Ne pas afficher si l'input est déjà un match exact
if matches and typed not in matches:
cols = st.columns(min(len(matches), 4))
for i, sugg in enumerate(matches):
with cols[i % 4]:
if st.button(f"➜ {sugg}", key=f"{key}__sug_{i}", use_container_width=True):
st.session_state[sel_key] = sugg
st.rerun()
return typed or ""
# ============================================================================
# Formulaire d'entrée
# ============================================================================
col_form, col_info = st.columns([2, 1])
with col_form:
st.subheader("📝 Formulaire de saisie")
matiere = autocomplete_input(
"Nom de la matière première",
get_matiere_list(),
key="input_matiere",
placeholder="Ex : BLE, T.TNSL DEC., ORGE, T. COLZA, LUZERNE…",
help_text="Entrez le nom usuel de la matière première. Des suggestions apparaîtront.",
)
provenance_connue = st.radio(
"Connaissez-vous la provenance de la matière première ?",
options=["Oui", "Non"],
horizontal=True,
)
pays_production = None
pays_transformation = None
if provenance_connue == "Oui":
pays_production = autocomplete_input(
"Pays de production primaire de la MP brute",
get_country_list(),
key="input_pays_prod",
placeholder="Ex : France, Brésil, Ukraine…",
help_text="Tapez les premières lettres et cliquez sur une suggestion, ou saisissez librement.",
)
pays_transformation = autocomplete_input(
"Pays de transformation (laisser vide si MP brute)",
get_country_list(),
key="input_pays_transfo",
placeholder="Ex : France, Allemagne… (vide si non transformée)",
help_text="Pays où la transformation a eu lieu. Laissez vide si la matière est brute.",
)
st.markdown("---")
run_button = st.button("🚀 Évaluer l'impact carbone", type="primary", use_container_width=True)
with col_info:
st.subheader("ℹ️ Règles générales")
st.info("""
- Le choix des valeurs par défaut relève de la responsabilité des entreprises.
- Il faut attribuer un facteur d'émission à **tous** les intrants.
- Si provenance inconnue → valeur la **plus défavorable**.
- Si provenance connue mais donnée inexistante → donnée générique la **plus pertinente**.
- Bases : **GFLI** et **ECOALIM** en priorité.
""")
st.warning("⚠️ **Hors produits dérivés du soja** — Ce logigramme ne s'applique pas aux dérivés de soja.")
# ============================================================================
# Exécution et affichage des résultats
# ============================================================================
if run_button:
if not matiere:
st.error("❌ Veuillez saisir un nom de matière première.")
st.stop()
# Nettoyage des entrées
pays_prod = pays_production.strip() if pays_production and pays_production.strip() else None
pays_transfo = pays_transformation.strip() if pays_transformation and pays_transformation.strip() else None
with st.spinner("🔄 Évaluation en cours… (classification LLM + recherche dans les bases de données)"):
result: CarbonResult = evaluate_carbon_impact(
matiere_premiere=matiere.strip(),
pays_production=pays_prod,
pays_transformation=pays_transfo,
)
st.divider()
# ------------------------------------------------------------------
# Section 1 : Résultat principal
# ------------------------------------------------------------------
st.subheader("📊 Résultat de l'impact carbone")
if result.erreur:
st.error(f"❌ {result.erreur}")
else:
col1, col2, col3 = st.columns(3)
with col1:
if result.impact_kg_co2_eq is not None:
if "tonne" in result.unite_source:
# GFLI : valeur en kg CO2 / tonne
st.metric(
label="Impact carbone",
value=f"{result.impact_kg_co2_eq:.2f}",
delta=f"kg CO2 eq / tonne",
)
st.metric(
label="Soit en tonnes CO2 eq / tonne produit",
value=f"{result.impact_tonne_co2_eq:.4f}",
delta="t CO2 eq / t produit",
)
else:
# EcoALIM : valeur en kg CO2 / kg
st.metric(
label="Impact carbone",
value=f"{result.impact_kg_co2_eq:.4f}",
delta=f"kg CO2 eq / kg",
)
st.metric(
label="Soit en tonnes CO2 eq / tonne produit",
value=f"{result.impact_kg_co2_eq:.4f}",
delta="t CO2 eq / t produit (même valeur numérique)",
)
with col2:
st.markdown(f"**Source :** {result.source_db}")
st.markdown(f"**Intrant utilisé :** {result.intrant_utilise}")
st.markdown(f"**Match exact :** {'✅ Oui' if result.match_exact else '⚠️ Non — valeur approchée'}")
with col3:
st.markdown(f"**Classification :** {'🏭 Transformé' if result.classification == 'transforme' else '🌾 Brut'}")
st.markdown(f"**Node résultat :** `{result.node_resultat}`")
if result.pays_production:
st.markdown(f"**Pays production :** {result.pays_production}")
if result.pays_transformation:
st.markdown(f"**Pays transformation :** {result.pays_transformation}")
# ------------------------------------------------------------------
# Section 2 : Parcours de logique (logigramme)
# ------------------------------------------------------------------
st.subheader("🔀 Parcours du logigramme")
for i, step in enumerate(result.parcours):
with st.expander(f"Étape {i+1}{step.node_id}", expanded=(i == 0)):
if step.question:
st.markdown(f"**Question :** {step.question}")
if step.answer:
st.markdown(f"**Réponse :** {step.answer}")
if step.action:
st.markdown(f"**Action :** {step.action}")
# ------------------------------------------------------------------
# Section 3 : Actions appliquées (recherche dans les BDD)
# ------------------------------------------------------------------
st.subheader("🔍 Détail des recherches effectuées")
for action in result.actions_appliquees:
if action.startswith(" →"):
st.success(action)
else:
st.markdown(f"- {action}")
# ------------------------------------------------------------------
# Section 4 : Justification si valeur alternative
# ------------------------------------------------------------------
if result.justification_alternative:
st.subheader("💡 Justification du choix de valeur")
st.info(result.justification_alternative)
# ------------------------------------------------------------------
# Section 5 : Candidats alternatifs
# ------------------------------------------------------------------
if result.candidats_alternatifs:
st.subheader("📋 Produits candidats (triés par pertinence)")
if not result.match_exact:
st.warning("⚠️ Pas de correspondance exacte — voici les produits les plus proches avec leur impact carbone.")
else:
st.info("ℹ️ Autres produits correspondant à la recherche dans les bases de données.")
# Construire un DataFrame pour un affichage clair
df_candidates = pd.DataFrame(result.candidats_alternatifs)
df_candidates = df_candidates.rename(columns={
"nom": "Intrant",
"impact": "Impact carbone",
"unite": "Unité",
"source": "Base",
})
# Formater les colonnes
st.dataframe(
df_candidates,
use_container_width=True,
hide_index=True,
column_config={
"Impact carbone": st.column_config.NumberColumn(format="%.4f"),
},
)
# ------------------------------------------------------------------
# Section 6 : Classification détaillée
# ------------------------------------------------------------------
with st.expander("📋 Détail de la classification brut/transformé"):
st.markdown(f"**Classification :** {result.classification}")
st.markdown(f"**Justification :** {result.classification_justification}")
# ============================================================================
# Mode batch (facultatif)
# ============================================================================
st.divider()
st.subheader("📁 Évaluation par lot (import fichier)")
st.markdown("Importez un fichier Excel avec les colonnes : `Matière première`, `Pays de la production primaire de la MP brute`, `Pays de la transformation`")
uploaded_file = st.file_uploader("Choisir un fichier Excel", type=["xlsx", "xls"])
if uploaded_file is not None:
try:
df_input = pd.read_excel(uploaded_file)
st.dataframe(df_input, use_container_width=True)
# Chercher les colonnes par nom approximatif
col_mapping = {}
for col in df_input.columns:
col_lower = str(col).lower()
if "matière" in col_lower or "matiere" in col_lower or "mp" in col_lower:
col_mapping["matiere"] = col
elif "production" in col_lower or "pays" in col_lower and "transf" not in col_lower:
if "matiere" not in col_lower:
col_mapping["pays_prod"] = col
elif "transf" in col_lower:
col_mapping["pays_transfo"] = col
if "matiere" not in col_mapping:
st.warning("⚠️ Colonne 'Matière première' non détectée. Vérifiez les noms de colonnes.")
else:
if st.button("🚀 Lancer l'évaluation par lot", type="primary"):
results_list = []
progress_bar = st.progress(0)
for idx, row in df_input.iterrows():
mp = str(row.get(col_mapping["matiere"], "")).strip()
if not mp or mp == "nan":
continue
pays_p = str(row.get(col_mapping.get("pays_prod", ""), "")).strip()
pays_t = str(row.get(col_mapping.get("pays_transfo", ""), "")).strip()
pays_p = pays_p if pays_p and pays_p != "nan" else None
pays_t = pays_t if pays_t and pays_t != "nan" else None
with st.spinner(f"Évaluation de {mp}..."):
res = evaluate_carbon_impact(mp, pays_p, pays_t)
results_list.append({
"Matière première": mp,
"Pays production": pays_p or "",
"Pays transformation": pays_t or "",
"Classification": res.classification,
"Impact (kg CO2 eq)": res.impact_kg_co2_eq,
"Unité": res.unite_source,
"Source": res.source_db,
"Intrant utilisé": res.intrant_utilise,
"Match exact": "Oui" if res.match_exact else "Non",
"Node résultat": res.node_resultat,
"Justification": res.justification_alternative or "",
"Erreur": res.erreur or "",
})
progress_bar.progress((idx + 1) / len(df_input))
if results_list:
df_results = pd.DataFrame(results_list)
st.subheader("📊 Résultats du lot")
st.dataframe(df_results, use_container_width=True)
# Téléchargement
csv = df_results.to_csv(index=False).encode("utf-8")
st.download_button(
label="📥 Télécharger les résultats (CSV)",
data=csv,
file_name="resultats_impact_carbone.csv",
mime="text/csv",
)
except Exception as e:
st.error(f"Erreur lors de la lecture du fichier : {e}")
# ============================================================================
# Footer
# ============================================================================
st.divider()
st.caption(
"POC GAIA — Outil d'aide à la détermination de l'impact carbone des matières premières "
"pour aliments composés. Basé sur le Guide GT Carbone, les bases ECOALIM v9 et GFLI 2.0. "
"Classification brut/transformé assistée par Mistral AI et le Catalogue UE des Matières Premières."
)