ram-maroc-churn / app.py
mhdbbbbb's picture
Initial deploymen
7fc4c75 verified
import streamlit as st
import pandas as pd
import numpy as np
import joblib
import os
import plotly.express as px
import plotly.graph_objects as go
from PIL import Image
# --- Configuration de la page ---
st.set_page_config(
page_title="Maroc Vol ✈ | Intelligence Client",
page_icon="✈️",
layout="wide",
initial_sidebar_state="expanded",
)
# --- Chargement du Style Custom (Moroccan Luxury Style) ---
def local_css():
st.markdown("""
<style>
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700&family=Outfit:wght@300;400;600;700&display=swap');
:root {
--primary-color: #004D2C; /* Deep Emerald */
--secondary-color: #D4AF37; /* Metallic Gold */
--accent-color: #8B0000; /* Deep Moroccan Red */
--bg-dark: #05140F;
--glass-bg: rgba(255, 255, 255, 0.05);
--glass-border: rgba(255, 255, 255, 0.1);
}
/* Base Reset & Typography */
html, body, [class*="css"] {
font-family: 'Outfit', sans-serif;
}
.main {
background: radial-gradient(circle at top right, #0A261B, #05140F);
color: #F0F0F0;
}
h1, h2, h3 {
font-family: 'Playfair Display', serif !important;
color: var(--secondary-color) !important;
letter-spacing: 0.5px;
}
/* Sidebar Styling */
[data-testid="stSidebar"] {
background-color: rgba(5, 20, 15, 0.95);
backdrop-filter: blur(20px);
border-right: 1px solid var(--glass-border);
}
/* Professional Glass Cards */
.st-emotion-cache-12w0qpk {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 20px;
padding: 20px;
backdrop-filter: blur(10px);
}
/* Premium Buttons */
.stButton>button {
width: 100%;
background: linear-gradient(135deg, var(--secondary-color) 0%, #B8860B 100%);
color: black !important;
border-radius: 12px;
border: none;
padding: 15px 0;
font-weight: 700;
font-size: 1.1rem;
transition: all 0.3s ease;
box-shadow: 0 10px 20px rgba(212, 175, 55, 0.2);
}
.stButton>button:hover {
transform: translateY(-2px);
box-shadow: 0 15px 30px rgba(212, 175, 55, 0.4);
background: linear-gradient(135deg, #E5C38B 0%, var(--secondary-color) 100%);
}
/* Mobile First Adjustments */
@media (max-width: 768px) {
.metric-val { font-size: 2.5rem !important; }
.stTitle { font-size: 2.2rem !important; }
}
/* Detailed Prediction Card */
.prediction-card {
background: linear-gradient(145deg, rgba(212, 175, 55, 0.1), rgba(0, 77, 44, 0.1));
border-radius: 24px;
padding: 40px;
border: 1px solid var(--secondary-color);
text-align: center;
margin: 20px 0;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
.metric-val {
font-size: 5rem;
font-weight: 800;
background: linear-gradient(to bottom, #FFF, #D4AF37);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
line-height: 1;
}
.status-badge {
display: inline-block;
padding: 6px 16px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
text-transform: uppercase;
margin-top: 15px;
}
/* Custom Input Styling */
.stNumberInput, .stSelectbox, .stSlider {
margin-bottom: 20px;
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.main-container { animation: fadeIn 0.8s ease-out; }
</style>
""", unsafe_allow_html=True)
local_css()
# --- Chargement des Ressources ---
@st.cache_resource
def load_assets():
try:
model_path = 'models/churn_rf_model.joblib'
enc_path = 'models/encoders.joblib'
feat_path = 'models/feature_names.joblib'
if not os.path.exists(model_path):
st.warning("Mode Démo : Modèle non trouvé.")
return None, None, None
model = joblib.load(model_path)
encoders = joblib.load(enc_path)
features = joblib.load(feat_path)
return model, encoders, features
except Exception as e:
st.error(f"Erreur technique : {e}")
return None, None, None
model, encoders, feature_names = load_assets()
# --- Sidebar ---
with st.sidebar:
st.markdown("<h1 style='color: #D4AF37; margin-bottom: 0;'>MAROC VOL</h1>", unsafe_allow_html=True)
st.write("---")
st.markdown("### 🏹 Intelligence Client")
st.write("Système prédictif Haute-Fidélité pour l'analyse du désengagement passager.")
with st.expander("⚙️ Préférences"):
st.selectbox("Langue", ["Français", "العربية", "English"])
st.toggle("Mode Expert", value=True)
st.markdown("---")
st.caption("© 2026 Maroc Vol - Excellence & Innovation")
# --- Main Layout ---
st.markdown('<div class="main-container">', unsafe_allow_html=True)
st.title("Tableau de Bord Prédictif")
st.write("Analysez le comportement de vos passagers en temps réel avec notre intelligence artificielle de pointe.")
# Mobile First: Layout stack on mobile, side-by-side on desktop
tabs = st.tabs(["📋 Saisie des Données", "📊 Résultats & Analyse"])
with tabs[0]:
st.subheader("Profil du Voyageur")
c1, c2 = st.columns([1, 1])
with c1:
age = st.number_input("Âge du passager", 18, 100, 35)
genre = st.selectbox("Genre", ["Homme", "Femme"])
region = st.selectbox("Origine", ["Casablanca-Settat", "Rabat-Salé-Kénitra", "Marrakech-Safi", "Europe", "Afrique", "Amérique/Asie"])
statut = st.select_slider("Statut Safar Flyer", options=["Blue", "Silver", "Gold", "Platinum"], value="Silver")
with c2:
vols_an = st.select_slider("Fréquence (vols/an)", options=list(range(0, 51)), value=5)
depense = st.number_input("Dépense Cumulée (MAD)", 0, 1000000, 25000, step=1000)
jours_dernier = st.slider("Dernier vol (jours)", 0, 730, 45)
retards = st.slider("Retards subis (6 mois)", 0, 10, 0)
st.markdown("---")
with st.container():
c3, c4 = st.columns(2)
with c3:
type_voyage = st.radio("Motif de voyage", ["Loisirs", "Affaires"], horizontal=True)
with c4:
classe = st.radio("Confort Préféré", ["Economique", "Business"], horizontal=True)
appels = st.slider("Requêtes Service Client", 0, 15, 1)
predict_btn = st.button("DIAGNOSTIQUER LA FIDÉLITÉ")
with tabs[1]:
if predict_btn:
if model is not None:
# Data Preparation
input_dict = {
'Age': age, 'Genre': genre, 'Region': region,
'Statut_Safar_Flyer': statut, 'Nombre_Vols_An': vols_an,
'Depense_Annuelle_MAD': depense, 'Jours_Depuis_Dernier_Vol': jours_dernier,
'Nombre_Retards_Subis_6m': retards, 'Type_Voyage_Frequent': type_voyage,
'Classe_Preferee': classe, 'Appels_Service_Client': appels
}
df_input = pd.DataFrame([input_dict])
for col, le in encoders.items():
df_input[col] = le.transform(df_input[col])
df_input = df_input[feature_names]
prob = model.predict_proba(df_input)[0][1]
# --- Visual Results ---
st.markdown(f"""
<div class="prediction-card">
<div style="color: #A0A0A0; text-transform: uppercase; letter-spacing: 2px; font-size: 0.9rem;">Indice de Churn Prédictif</div>
<div class="metric-val">{prob:.0%}</div>
</div>
""", unsafe_allow_html=True)
col_res1, col_res2 = st.columns([1, 1])
with col_res1:
st.markdown("#### 🎯 Diagnostic")
if prob > 0.7:
st.error("### RISQUE CRITIQUE")
st.info("Ce passager présente un haut potentiel de départ vers la concurrence.")
elif prob > 0.4:
st.warning("### VIGILANCE")
st.info("Compte à surveiller réactivement.")
else:
st.success("### FIDÉLITÉ SOLIDE")
st.info("Ambassadeur de la marque.")
with col_res2:
st.markdown("#### 💡 Recommandation")
if prob > 0.7:
st.write("1. **Surclassement immédiat** sur le prochain vol.")
st.write("2. **Appel VIP** du gestionnaire de compte.")
elif prob > 0.4:
st.write("1. Envoi d'un **bon de réduction** personnalisé.")
st.write("2. Newsletter exclusive status +1.")
else:
st.write("1. Invitation au **Salon VIP**.")
st.write("2. Offre early-access nouvelles routes.")
# Insights Chart
st.markdown("---")
st.markdown("#### 📊 Facteurs Déterminants")
# Visual pseudo-shap
importance = {
"Recency": jours_dernier / 365,
"Incidents": retards / 5,
"Engagement": appels / 10,
"Loyalty": (1 - ["Blue", "Silver", "Gold", "Platinum"].index(statut)/3)
}
fig = px.bar(
x=list(importance.values()),
y=list(importance.keys()),
orientation='h',
color_discrete_sequence=['#D4AF37'],
template="plotly_dark"
)
fig.update_layout(
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(0,0,0,0)',
xaxis_title="Impact sur la décision",
yaxis_title="",
margin=dict(l=0, r=0, t=20, b=0)
)
st.plotly_chart(fig, use_container_width=True)
else:
st.error("Modèle non chargé. Veuillez vérifier le dossier 'models/'.")
else:
st.write("### 🏺 En attente d'analyse")
st.info("Configurez le profil passager dans l'onglet 'Saisie' pour générer les insights.")
st.markdown('</div>', unsafe_allow_html=True)