Spaces:
Sleeping
Sleeping
| 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 --- | |
| 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) | |