import streamlit as st
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import requests
import plotly.express as px
from streamlit_option_menu import option_menu
from PIL import Image
# ========== CONFIGURATION ==========
st.set_page_config(page_title="Getaround Dashboard", layout="wide")
st.markdown("
🚗 Getaround Dashboard
", unsafe_allow_html=True)
# ========== LOGO ==========
try:
logo = Image.open("logo.png")
st.sidebar.image(logo, width=200)
except Exception:
st.sidebar.warning("Logo non chargé")
# ========== MENU ==========
with st.sidebar:
selected = option_menu(
"Menu principal",
["🏁 EDA delay", "📈 EDA pricing", "⏳ Seuil entre locations", "🔮 API prediction"],
icons=["hourglass-split", "bar-chart-line", "clock", "cpu"],
menu_icon="cast",
default_index=0,
)
st.markdown("---")
st.markdown("📄 [Espace API HuggingFace](https://huggingface.co/spaces/licorne2lc/get_around_api)")
st.markdown("🛠️ [Docs API /predict](https://licorne2lc-get-around-api.hf.space/docs)")
# ========== 1. EDA DELAY ==========
if selected == "🏁 EDA delay":
st.title("📊 Analyse des retards de Getaround")
try:
dataset = pd.read_excel("get_around_delay_analysis.xlsx")
dataset["delay_status"] = dataset["delay_at_checkout_in_minutes"].apply(
lambda x: "En avance" if x < 0 else "À l’heure" if x == 0 else "En retard"
)
def catégoriser_retard(x):
if x <= 0:
return "Pas de retard"
elif x <= 30:
return "< 30 min"
elif x <= 60:
return "30-60 min"
elif x <= 120:
return "1h-2h"
elif x <= 240:
return "2h-4h"
else:
return "> 4h"
retard_order = ["Pas de retard", "< 30 min", "30-60 min", "1h-2h", "2h-4h", "> 4h"]
dataset["retard_cat"] = dataset["delay_at_checkout_in_minutes"].apply(catégoriser_retard)
dataset["retard_cat"] = pd.Categorical(dataset["retard_cat"], categories=retard_order, ordered=True)
fig_checkin = px.pie(dataset, names="checkin_type", title="Répartition des types de check-in",
hole=0.4, color_discrete_sequence=px.colors.qualitative.Pastel, width=1000, height=400)
st.plotly_chart(fig_checkin)
fig_state = px.pie(dataset, names="state", title="Répartition des états de location",
hole=0.4, color_discrete_sequence=px.colors.qualitative.Set3, width=1000, height=400)
st.plotly_chart(fig_state)
fig_delay_status = px.pie(dataset, names="delay_status", title="Retards : avance / à l’heure / en retard",
hole=0.4, color_discrete_sequence=px.colors.qualitative.Vivid, width=1000, height=400)
st.plotly_chart(fig_delay_status)
filtered = dataset[(dataset["delay_at_checkout_in_minutes"] > 0) & (dataset["delay_at_checkout_in_minutes"] <= 1440)]
fig_delay_hist = px.histogram(filtered, x="delay_at_checkout_in_minutes", nbins=50,
title="Distribution des retards (0 à 1440 minutes)", width=1000, height=400)
st.plotly_chart(fig_delay_hist)
fig_retard_cat = px.bar(dataset["retard_cat"].value_counts().reindex(retard_order),
title="Gravité des retards", labels={"value": "Nombre", "index": "Catégorie"},
width=1000, height=400)
st.plotly_chart(fig_retard_cat)
dataset_for_join_B = dataset[["rental_id", "delay_at_checkout_in_minutes"]].rename(
columns={"rental_id": "rental_id_join", "delay_at_checkout_in_minutes": "delay_at_checkout_in_minutes_join"}
)
dataset_join = pd.merge(dataset, dataset_for_join_B,
left_on="previous_ended_rental_id", right_on="rental_id_join")
dataset_join["real_delay"] = dataset_join["time_delta_with_previous_rental_in_minutes"] - dataset_join["delay_at_checkout_in_minutes_join"]
dataset_join_clean = dataset_join.dropna(subset=["delay_at_checkout_in_minutes_join"])
dataset_join_clean["annulation_due_au_retard"] = dataset_join_clean.apply(
lambda row: "Oui" if row["state"] == "canceled" and row["real_delay"] < 0 else
"Annulée (autre raison)" if row["state"] == "canceled" else "Non", axis=1
)
fig_annul = px.pie(dataset_join_clean, names="annulation_due_au_retard",
title="Répartition des annulations dues au retard du locataire précédent",
hole=0.4, color_discrete_sequence=px.colors.qualitative.Bold, width=1000, height=400)
st.plotly_chart(fig_annul)
except FileNotFoundError:
st.error("❌ Fichier Excel non trouvé : get_around_delay_analysis.xlsx")
# ========== 2. EDA PRICING ==========
elif selected == "📈 EDA pricing":
st.title("💰 Analyse des prix de location Getaround")
try:
df_price = pd.read_csv("get_around_pricing_project.csv")
st.subheader("Distribution du prix journalier")
fig, ax = plt.subplots(figsize=(10, 4))
sns.histplot(df_price["rental_price_per_day"], bins=50, kde=True, ax=ax)
st.pyplot(fig)
st.subheader("Prix par type de carburant")
fig, ax = plt.subplots(figsize=(10, 4))
sns.boxplot(x="fuel", y="rental_price_per_day", data=df_price, ax=ax)
st.pyplot(fig)
st.subheader("Prix par type de voiture")
fig, ax = plt.subplots(figsize=(10, 4))
sns.boxplot(x="car_type", y="rental_price_per_day", data=df_price, ax=ax)
st.pyplot(fig)
st.subheader("Prix selon la présence de Getaround Connect")
fig, ax = plt.subplots(figsize=(10, 4))
sns.boxplot(x="has_getaround_connect", y="rental_price_per_day", data=df_price, ax=ax)
st.pyplot(fig)
st.subheader("Corrélation entre puissance moteur et prix journalier")
fig, ax = plt.subplots(figsize=(10, 4))
sns.scatterplot(x="engine_power", y="rental_price_per_day", data=df_price, hue="car_type", ax=ax)
st.pyplot(fig)
except FileNotFoundError:
st.error("❌ Fichier CSV non trouvé : get_around_pricing_project.csv")
# ========== 3. SEUIL ENTRE LOCATIONS ==========
elif selected == "⏳ Seuil entre locations":
st.title("⏳ Analyse du seuil entre deux locations")
# Chargement des données
try:
dataset = pd.read_excel("get_around_delay_analysis.xlsx")
def resolved_rentals(threshold, scope):
if scope == "connect":
connect_late = dataset[
(dataset["delay_at_checkout_in_minutes"] > 0) &
(dataset["checkin_type"] == "connect")
]
resolved = connect_late[connect_late["delay_at_checkout_in_minutes"] <= threshold]
return round((len(resolved) / len(connect_late)) * 100, 2)
else:
late = dataset[dataset["delay_at_checkout_in_minutes"] > 0]
resolved = late[late["delay_at_checkout_in_minutes"] <= threshold]
return round((len(resolved) / len(late)) * 100, 2)
# Interface utilisateur
st.markdown("### Sélection du seuil")
col1, col2 = st.columns(2)
with col1:
seuil = st.slider("⏱️ Seuil entre deux locations (minutes)", 0, 240, 150, step=30)
with col2:
scope = st.radio("🚗 Type de véhicules concernés", ["all", "connect"],
format_func=lambda x: "Tous les véhicules" if x == "all" else "Connect uniquement")
resultat = resolved_rentals(seuil, scope)
# Calcul des locations incompatibles (intervalle entre locations trop court)
if scope == "connect":
total_short = dataset[
(dataset["checkin_type"] == "connect") &
(dataset["time_delta_with_previous_rental_in_minutes"] < seuil)
]
else:
total_short = dataset[
dataset["time_delta_with_previous_rental_in_minutes"] < seuil
]
nb_louees_non_resolues = len(total_short)
pourcentage_incompatibles = round((nb_louees_non_resolues / len(dataset)) * 100, 2)
# Affichage
st.success(f"✅ Avec un seuil de **{seuil} minutes**, environ **{resultat}%** des retards sont absorbés.")
st.info(f"📉 Environ **{nb_louees_non_resolues} locations** ({pourcentage_incompatibles}%) seraient incompatibles avec ce seuil.")
# Préparer les courbes
import plotly.graph_objects as go
seuils = list(range(0, 241, 30))
all_vals = [resolved_rentals(s, "all") for s in seuils]
connect_vals = [resolved_rentals(s, "connect") for s in seuils]
# Courbe des % incompatibles
if scope == "connect":
incompat_vals = [
round(len(dataset[
(dataset["checkin_type"] == "connect") &
(dataset["time_delta_with_previous_rental_in_minutes"] < s)
]) / len(dataset) * 100, 2)
for s in seuils
]
else:
incompat_vals = [
round(len(dataset[
dataset["time_delta_with_previous_rental_in_minutes"] < s
]) / len(dataset) * 100, 2)
for s in seuils
]
# Création du graphique
fig = go.Figure()
fig.add_trace(go.Scatter(
x=seuils,
y=all_vals,
name="Tous les véhicules - % retards résolus",
mode="lines+markers",
line=dict(color="blue")
))
fig.add_trace(go.Scatter(
x=seuils,
y=connect_vals,
name="Connect uniquement - % retards résolus",
mode="lines+markers",
line=dict(color="orange")
))
fig.add_trace(go.Scatter(
x=seuils,
y=incompat_vals,
name="% locations incompatibles",
mode="lines+markers",
line=dict(color="purple", dash="dot")
))
# Ligne verticale dynamique pour le seuil sélectionné
fig.add_trace(go.Scatter(
x=[seuil],
y=[100],
mode="lines",
name="Seuil choisi",
line=dict(color="red", dash="dash")
))
fig.update_layout(
title="Impact du seuil : retards résolus vs locations incompatibles",
xaxis_title="Seuil (minutes)",
yaxis_title="Pourcentage (%)",
yaxis_range=[0, 100],
template="simple_white"
)
st.plotly_chart(fig, use_container_width=True)
st.markdown("""
ℹ️ **Conseil produit** : un seuil entre **120 et 150 minutes** permet de résoudre une part significative des retards
tout en limitant la perte de réservations successives.
""")
except FileNotFoundError:
st.error("❌ Fichier Excel non trouvé : get_around_delay_analysis.xlsx")
# ========== 4. API PREDICTION ==========
elif selected == "🔮 API prediction":
st.title("🔮 Prédiction du prix de location")
st.markdown("Remplis les informations suivantes pour obtenir une estimation du prix")
with st.form("prediction_form"):
model_key = st.selectbox("Modèle de voiture", ["Peugeot", "Audi", "BMW"])
mileage = st.number_input("Kilométrage", value=50000)
engine_power = st.number_input("Puissance moteur", value=100)
fuel = st.selectbox("Type de carburant", ["diesel", "petrol", "electric", "hybrid"])
paint_color = st.selectbox("Couleur", ["black", "white", "grey", "blue", "red"])
car_type = st.selectbox("Type de voiture", ["sedan", "convertible", "suv", "coupe"])
private_parking_available = st.checkbox("Parking privé disponible", value=True)
has_gps = st.checkbox("GPS", value=True)
has_air_conditioning = st.checkbox("Climatisation", value=True)
automatic_car = st.checkbox("Boîte automatique", value=True)
has_getaround_connect = st.checkbox("Getaround Connect", value=True)
has_speed_regulator = st.checkbox("Régulateur de vitesse", value=True)
winter_tires = st.checkbox("Pneus hiver", value=True)
submitted = st.form_submit_button("📤 Envoyer")
if submitted:
data = {
"model_key": model_key,
"mileage": mileage,
"engine_power": engine_power,
"fuel": fuel,
"paint_color": paint_color,
"car_type": car_type,
"private_parking_available": private_parking_available,
"has_gps": has_gps,
"has_air_conditioning": has_air_conditioning,
"automatic_car": automatic_car,
"has_getaround_connect": has_getaround_connect,
"has_speed_regulator": has_speed_regulator,
"winter_tires": winter_tires,
}
api_url = "https://licorne2lc-get-around-api.hf.space/predict"
try:
response = requests.post(api_url, json=data)
if response.status_code == 200:
price = round(response.json()["prediction"], 2)
st.success(f"💰 Prix de location estimé : {price} € / jour")
else:
st.error(f"❌ Erreur {response.status_code} : {response.text}")
except Exception as e:
st.error(f"❌ Erreur lors de la requête : {e}")