gdleds commited on
Commit
fc775ed
·
1 Parent(s): 5706836

grosse modif

Browse files
Files changed (1) hide show
  1. app.py +335 -285
app.py CHANGED
@@ -1,7 +1,7 @@
1
  import streamlit as st
2
  import pandas as pd
3
  import plotly.express as px
4
-
5
 
6
  st.set_page_config(page_title="Getaround – Dashboard Analyse", layout="wide")
7
 
@@ -9,343 +9,393 @@ st.title("🚗 Dashboard Analyse – Getaround")
9
  st.markdown("""
10
  Bienvenue sur le dashboard interactif de l’étude Getaround.
11
  Nous allons explorer :
12
- 1. Les retards et annulations
13
- 2. L’impact d’un buffer entre deux locations
14
- 3. L’analyse des prix des véhicules
15
- 4. La conclusion business
16
- """)
17
-
18
-
19
- df = pd.read_csv("data/get_around_delay_analysis.csv")
20
- df_pricing = pd.read_csv("data/pricing_clean.csv")
21
-
22
- loc_moyenne = df_pricing["rental_price_per_day"].mean()
23
-
24
- # EDA SUR LES RETARDS
25
- st.header("📊 Analyse des retards et annulations")
26
-
27
-
28
- fig_state = px.pie(df, names="state", title="Répartition par état des locations")
29
- st.plotly_chart(fig_state, use_container_width=True)
30
-
31
- annulation = (df["state"] == "canceled").sum()
32
- st.metric("Nombre total d'annulations", annulation)
33
-
34
- st.markdown("""
35
- 👉 Environ **15 % des transactions sont annulées**.
36
- La proportion est légèrement plus élevée via **Connect** que via Mobile.
37
- """)
38
-
39
-
40
- fig_delay = px.histogram(
41
- df.query("-150 <= delay_at_checkout_in_minutes <= 150"),
42
- x="delay_at_checkout_in_minutes",
43
- nbins=60,
44
- title="Distribution des retards (fenêtre -150 à 150 min)"
45
- )
46
- st.plotly_chart(fig_delay, use_container_width=True)
47
-
48
- st.markdown("""
49
- 👉 La majorité des retards est **courte** :
50
- - 50 % ≤ 9 min
51
- - 75 % ≤ 67 min
52
- Mais certains cas extrêmes dépassent plusieurs jours.
53
- """)
54
-
55
- # ANALYSE DES ANNULATIONS
56
- st.header("❌ Analyse des annulations liées aux retards")
57
-
58
- mask = df['previous_ended_rental_id'] > 0
59
- df_nan = df[mask]
60
- st.metric("Le nombre d'annulations du au retard du véhicule liées à un location précendente est de", (df_nan["state"] == "canceled").sum())
61
- annulation = (df_nan["state"] == "canceled").sum()
62
- st.metric("La location moyenne d'un véhicule par jour est de ", df_pricing['rental_price_per_day'].mean())
63
- st.metric("La perte du aux annulations s'élève a :", (annulation * loc_moyenne).round(2),"$")
64
-
65
- canceled = df[df["state"] == "canceled"][["rental_id", "car_id", "previous_ended_rental_id", "checkin_type","time_delta_with_previous_rental_in_minutes"]]
66
-
67
- merged = canceled.merge(
68
- df[["rental_id", "car_id", "delay_at_checkout_in_minutes", "checkin_type"]],
69
- left_on="previous_ended_rental_id",
70
- right_on="rental_id",
71
- suffixes=("_canceled", "_previous")
72
- )
73
-
74
- conflict_strict = merged[
75
- merged["delay_at_checkout_in_minutes"] > merged["time_delta_with_previous_rental_in_minutes"]
76
- ]
77
- st.metric("Conflits stricts :", len(conflict_strict))
78
- st.metric("Moyenne retard précédent (conflits seulement) :", conflict_strict["delay_at_checkout_in_minutes"].mean().round(2))
79
-
80
- st.markdown("""
81
- 👉 Parmi les **229 annulations liées à une location précédente**,
82
- **37** sont dues à un retard allant au delà de l'heure de début de la prochaine location.
83
- Ces retards sont très longs (~4h15 en moyenne).
84
  """)
85
 
86
- conflict_minus30 = merged[
87
- merged["delay_at_checkout_in_minutes"] > merged["time_delta_with_previous_rental_in_minutes"] - 30
88
- ]
89
-
90
- conflict_minus60 = merged[
91
- merged["delay_at_checkout_in_minutes"] > merged["time_delta_with_previous_rental_in_minutes"] - 60
92
- ]
93
-
94
- conflict_minus90 = merged[
95
- merged["delay_at_checkout_in_minutes"] > merged["time_delta_with_previous_rental_in_minutes"] - 90
96
- ]
97
-
98
- conflict_minus180 = merged[
99
- merged["delay_at_checkout_in_minutes"] > merged["time_delta_with_previous_rental_in_minutes"] - 180
100
- ]
101
-
102
-
103
- results = [
104
- {"Buffer": "-30 min", "Conflits": (len(conflict_minus30)-len(conflict_strict)), "Moyenne retard (min)": conflict_minus30["delay_at_checkout_in_minutes"].mean()},
105
- {"Buffer": "-60 min", "Conflits": (len(conflict_minus60)-len(conflict_strict)), "Moyenne retard (min)": conflict_minus60["delay_at_checkout_in_minutes"].mean()},
106
- {"Buffer": "-90 min", "Conflits": (len(conflict_minus90)-len(conflict_strict)), "Moyenne retard (min)": conflict_minus90["delay_at_checkout_in_minutes"].mean()},
107
- {"Buffer": "-180 min", "Conflits": (len(conflict_minus180)-len(conflict_strict)), "Moyenne retard (min)": conflict_minus180["delay_at_checkout_in_minutes"].mean()},
108
- ]
109
- df_conflicts = pd.DataFrame(results)
110
- df_conflicts['lost_value'] = df_conflicts["Conflits"]*loc_moyenne
111
- st.dataframe(df_conflicts)
112
-
113
- st.markdown("""Si on regarde 30 miniutes avant le début de la location du véhicule combien ont été annulé (buffer de 30 minutes), on détecte 10 annulations supplémentaires : ces cas correspondent à des locations rendues trop proches de la suivante (<30 min de marge).
114
- Avec un buffer de 60 minutes, seulement 3 cas en plus → ce sont donc des situations rares mais critiques.
115
- En élargissant à 90 minutes, on ajoute encore 7 cas.""")
116
-
117
-
118
- fig_type = px.histogram(
119
- df.query("-150 <= delay_at_checkout_in_minutes <= 150"),
120
- x="delay_at_checkout_in_minutes",
121
- color="checkin_type",
122
- nbins=int((150 - (-150)) / 10),
123
- opacity=0.2,
124
- title="Distribution des retards par type de check-in",
125
- labels={"delay_at_checkout_in_minutes": "Retard (minutes)"},
126
- range_x=[-150, 150],
127
- width=1200, height=600,
128
- )
129
- fig_type.update_layout(barmode="overlay")
130
- fig_type.update_yaxes(title="Nombre de locations")
131
- #
132
- st.plotly_chart(fig_type, use_container_width=True)
133
-
134
 
135
- st.markdown(""" ANALYSE SUR MOBILE """)
136
-
137
- mask = merged["checkin_type_canceled"] == "mobile"
138
- merged_mob = merged[mask]
139
- conflict_strict1= merged_mob[
140
- merged_mob["delay_at_checkout_in_minutes"] > merged_mob["time_delta_with_previous_rental_in_minutes"]
141
- ]
142
- st.metric("Conflits stricts :", len(conflict_strict1))
143
- st.metric("Moyenne retard précédent (conflits seulement) :", conflict_strict1["delay_at_checkout_in_minutes"].mean().round(2))
144
 
145
- conflict_1minus30 = merged_mob[
146
- merged_mob["delay_at_checkout_in_minutes"] > merged_mob["time_delta_with_previous_rental_in_minutes"] - 30
147
- ]
148
 
149
- conflict_1minus60 = merged_mob[
150
- merged_mob["delay_at_checkout_in_minutes"] > merged_mob["time_delta_with_previous_rental_in_minutes"] - 60
151
- ]
152
 
153
- conflict_1minus90 = merged_mob[
154
- merged_mob["delay_at_checkout_in_minutes"] > merged_mob["time_delta_with_previous_rental_in_minutes"] - 90
155
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
 
157
- conflict_1minus180 = merged_mob[
158
- merged_mob["delay_at_checkout_in_minutes"] > merged_mob["time_delta_with_previous_rental_in_minutes"] - 180
159
- ]
160
 
 
 
 
 
 
 
 
161
 
162
- results = [
163
- {"Buffer": "-30 min", "Conflits": (len(conflict_1minus30)-len(conflict_strict1)), "Moyenne retard (min)": conflict_1minus30["delay_at_checkout_in_minutes"].mean()},
164
- {"Buffer": "-60 min", "Conflits": (len(conflict_1minus60)-len(conflict_strict1)), "Moyenne retard (min)": conflict_1minus60["delay_at_checkout_in_minutes"].mean()},
165
- {"Buffer": "-90 min", "Conflits": (len(conflict_1minus90)-len(conflict_strict1)), "Moyenne retard (min)": conflict_1minus90["delay_at_checkout_in_minutes"].mean()},
166
- {"Buffer": "-180 min", "Conflits": (len(conflict_1minus180)-len(conflict_strict1)), "Moyenne retard (min)": conflict_1minus180["delay_at_checkout_in_minutes"].mean()},
167
- ]
168
- df_conflicts_mob = pd.DataFrame(results)
169
- df_conflicts_mob['lost_value'] = df_conflicts_mob["Conflits"]*loc_moyenne
170
- st.dataframe(df_conflicts_mob)
171
 
172
- st.markdown(""" ANALYSE SUR CONNECT """)
 
 
173
 
174
- mask = merged["checkin_type_canceled"] == "connect"
175
- merged_connect = merged[mask]
176
- conflict_strict2 = merged_connect[
177
- merged_connect["delay_at_checkout_in_minutes"] > merged_connect["time_delta_with_previous_rental_in_minutes"]
178
- ]
179
- st.metric("Conflits stricts :", len(conflict_strict2))
180
- st.metric("Moyenne retard précédent (conflits seulement) :", conflict_strict2["delay_at_checkout_in_minutes"].mean().round(2))
181
 
 
 
 
182
 
183
- conflict_2minus30 = merged_connect[
184
- merged_connect["delay_at_checkout_in_minutes"] > merged_connect["time_delta_with_previous_rental_in_minutes"] - 30
185
- ]
186
 
 
 
 
187
 
188
- conflict_2minus60 = merged_connect[
189
- merged_connect["delay_at_checkout_in_minutes"] > merged_connect["time_delta_with_previous_rental_in_minutes"] - 60
190
- ]
191
 
192
- conflict_2minus90 = merged_connect[
193
- merged_connect["delay_at_checkout_in_minutes"] > merged_connect["time_delta_with_previous_rental_in_minutes"] - 90
194
- ]
 
 
 
 
195
 
196
- conflict_2minus180 = merged_connect[
197
- merged_connect["delay_at_checkout_in_minutes"] > merged_connect["time_delta_with_previous_rental_in_minutes"] - 180
198
- ]
199
 
 
200
 
201
- results = [
202
- {"Buffer": "-30 min", "Conflits": (len(conflict_2minus30)-len(conflict_strict2)), "Moyenne retard (min)": conflict_2minus30["delay_at_checkout_in_minutes"].mean()},
203
- {"Buffer": "-60 min", "Conflits": (len(conflict_2minus60)-len(conflict_strict2)), "Moyenne retard (min)": conflict_2minus60["delay_at_checkout_in_minutes"].mean()},
204
- {"Buffer": "-90 min", "Conflits": (len(conflict_2minus90)-len(conflict_strict2)), "Moyenne retard (min)": conflict_2minus90["delay_at_checkout_in_minutes"].mean()},
205
- {"Buffer": "-180 min", "Conflits": (len(conflict_2minus180)-len(conflict_strict2)), "Moyenne retard (min)": conflict_2minus180["delay_at_checkout_in_minutes"].mean()},
206
- ]
207
- df_conflicts_con = pd.DataFrame(results)
208
 
209
- df_conflicts_con['lost_value'] = df_conflicts_con["Conflits"]*loc_moyenne
 
 
 
210
 
211
- st.dataframe(df_conflicts_con)
 
212
 
213
- st.markdown("""Les deux systèmes subissent des annulations liées aux délais courts.
214
- Connect est plus fréquent en volume absolu, mais ses retards sont légèrement moins sévères.
215
- Mobile présente moins de cas mais avec des retards plus longs → donc plus difficiles à absorber sans impact client.
216
- Les deux méthodes sont exposées aux retards, mais Connect encaisse plus de cas tandis que Mobile concentre les retards les plus importants.
217
- Un buffer opérationnel de 30 minutes permettrai un gain rapide sans trop affecté le CA global.
218
- """ )
219
 
220
- # IMPACT DES BUFFERS
221
- st.header(" Simulation de buffer entre locations")
222
- st.markdown(""" Afin d'analyser l'impact d'un buffer nous prenons cette fois ci que les locations qui ont eu lieu et filtrons toutes les locations ayant débutées dans les 30 minutes qui suit la précéddentes.
223
- Ces locations sont celle qui risquent d'etre impactées par ce delai """)
224
 
225
- loc_moyenne = df_pricing["rental_price_per_day"].mean()
226
- CA_total = (df['state'] == 'ended').sum() * loc_moyenne
227
 
 
 
 
 
 
228
 
229
- df_valid = df[df["state"] == "ended"].copy()
230
- df_valid["ecart"] = df_valid["time_delta_with_previous_rental_in_minutes"] - df_valid["delay_at_checkout_in_minutes"]
231
 
232
- def count_blocked(df, buffer):
233
- return (df["ecart"] < buffer).sum()
234
 
235
- results = []
236
- for b in [30, 60, 90, 180]:
237
- blocked = count_blocked(df_valid, b)
238
- perte_estimee = blocked * loc_moyenne
239
- results.append({"Buffer (min)": b, "Locations bloquées": blocked, "Perte estimée ($)": round(perte_estimee, 2)})
240
 
241
- df_results = pd.DataFrame(results)
242
- st.dataframe(df_results)
243
 
 
 
244
 
245
- st.markdown(""" MOBILE UNIQUEMENT""")
 
 
 
 
246
 
247
- df_valid_mob = df[(df["state"] == "ended") & (df["checkin_type"] == "mobile")].copy()
248
- df_valid_mob["ecart"] = df_valid_mob["time_delta_with_previous_rental_in_minutes"] - df_valid_mob["delay_at_checkout_in_minutes"]
249
 
250
- def count_blocked(df, buffer):
251
- return (df["ecart"] < buffer).sum()
252
 
253
- results2 = []
254
- for b in [30, 60, 90, 180]:
255
- blocked = count_blocked(df_valid_mob, b)
256
- perte_estimee = blocked * loc_moyenne
257
- results.append({"Buffer (min)": b, "Locations bloquées": blocked, "Perte estimée ($)": round(perte_estimee, 2)})
258
 
259
- df_results_mob = pd.DataFrame(results2)
260
- st.dataframe(df_results_mob)
261
 
 
 
262
 
263
- st.markdown(""" CONNECT UNIQUEMENT""")
 
 
 
 
264
 
265
- df_valid_con = df[(df["state"] == "ended") & (df["checkin_type"] == "connect")].copy()
266
- df_valid_con["ecart"] = df_valid_con["time_delta_with_previous_rental_in_minutes"] - df_valid_con["delay_at_checkout_in_minutes"]
267
 
268
- def count_blocked(df, buffer):
269
- return (df["ecart"] < buffer).sum()
 
 
 
270
 
271
- results3 = []
272
- for b in [30, 60, 90, 180]:
273
- blocked = count_blocked(df_valid_con, b)
274
- perte_estimee = blocked * loc_moyenne
275
- results.append({"Buffer (min)": b, "Locations bloquées": blocked, "Perte estimée ($)": round(perte_estimee, 2)})
276
 
277
- df_results_con = pd.DataFrame(results3)
278
- st.dataframe(df_results_con)
 
 
 
 
 
 
 
 
 
279
 
280
- st.markdown("""
281
- 👉 Un buffer **de 30 min bloque ~340 locations** pertes supérieures aux gains.
282
- 👉 Un buffer de **180 min réduit beaucoup les annulations** mais fait perdre trop de CA.
283
- **Conclusion : un buffer global n’est pas viable.**
284
- """)
285
 
286
- # ANALYSE DU PRICING
287
- st.header("💰 Analyse des prix de location")
 
 
 
288
 
289
- fig_price = px.histogram(df_pricing, x="rental_price_per_day", nbins=50,
290
- title="Distribution des prix de location par jour")
291
- st.plotly_chart(fig_price, use_container_width=True)
292
 
293
 
294
- fig_car_typ = px.box(df_pricing, x="car_type", y="rental_price_per_day",
295
- title="Prix par type de véhicule")
296
- st.plotly_chart(fig_car_typ, use_container_width=True)
297
 
298
 
299
- st.markdown("""
300
- 👉 Le marché est concentré autour de **100–140 $/jour**.
301
- Les **SUV, coupés, cabriolets** sont plus chers.
302
- Les **citadines** et **hatchbacks** moins chers.
303
- """)
304
-
305
- st.markdown(""" **Indice de correlation** """)
306
- df_corr = df_pricing.copy()
307
- bool_cols = df_corr.select_dtypes(include="bool").columns
308
- df_corr[bool_cols] = df_corr[bool_cols].astype(int)
309
-
310
- num_df = df_corr.select_dtypes(include=["int64", "float64"])
311
 
312
- corr = num_df.corr()
313
 
314
- fig_cor = px.imshow(
315
- corr,
316
- text_auto=True,
317
- color_continuous_scale="RdBu_r",
318
- title="Matrice de corrélation",
319
- height=800,
320
- width=1000
321
- )
322
- st.plotly_chart(fig_cor, use_container_width=True)
323
 
324
- st.markdown(""" + corrélation prix ↔ puissance moteur
325
 
326
- corrélation prix ↔ kilométrage
327
 
328
- Transmission automatique = + valeur
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
 
330
- Présence de GPS = + valeur ajoutée """)
331
 
332
 
333
-
334
- # CONCLUSION BUSINESS
335
- st.header("📌 Conclusion business")
336
-
337
- st.markdown("""
338
- Nos analyses montrent :
339
- - Les retards courts sont fréquents, mais les **annulations sont rares (229 cas)**.
340
- - Les retards extrêmes sont à l’origine de la majorité des conflits.
341
- - Un buffer global **réduit les annulations mais détruit plus de revenu** qu’il n’en sauve.
342
-
343
- ✅ **Recommandations** :
344
- - Pas de buffer global.
345
- - Buffer ciblé (<1h) sur cas à risque, zone chaude.
346
- - Notifications pour inciter à rendre le véhicule en avance.
347
- - Eventuellement pénalités pour retards répétés.(=> mais c'est anticommercial )
348
-
349
- 👉 L’impact économique du buffer est donc limité,
350
- mais des mesures ciblées peuvent réduire la friction sans perte majeure de CA.
351
- """)
 
1
  import streamlit as st
2
  import pandas as pd
3
  import plotly.express as px
4
+ import requests
5
 
6
  st.set_page_config(page_title="Getaround – Dashboard Analyse", layout="wide")
7
 
 
9
  st.markdown("""
10
  Bienvenue sur le dashboard interactif de l’étude Getaround.
11
  Nous allons explorer :
12
+ 1. Analyse Retards
13
+ 2. Analyse Pricing
14
+ 3. Prédiction API
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  """)
16
 
17
+ tab1, tab2, tab3 = st.tabs(["📊 Analyse Retards", "💰 Analyse Pricing", "🤖 Prédiction API"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
+ with tab1:
20
+ st.header("Analyse des retards")
 
 
 
 
 
 
 
21
 
22
+ df = pd.read_csv("data/get_around_delay_analysis.csv")
23
+ df_pricing = pd.read_csv("data/pricing_clean.csv")
 
24
 
25
+ loc_moyenne = df_pricing["rental_price_per_day"].mean()
 
 
26
 
27
+ # EDA SUR LES RETARDS
28
+ st.header("📊 Analyse des retards et annulations")
29
+
30
+
31
+ fig_state = px.pie(df, names="state", title="Répartition par état des locations")
32
+ st.plotly_chart(fig_state, use_container_width=True)
33
+
34
+ annulation = (df["state"] == "canceled").sum()
35
+ st.metric("Nombre total d'annulations", annulation)
36
+
37
+ st.markdown("""
38
+ 👉 Environ **15 % des transactions sont annulées**.
39
+ La proportion est légèrement plus élevée via **Connect** que via Mobile.
40
+ """)
41
+
42
+
43
+ fig_delay = px.histogram(
44
+ df.query("-150 <= delay_at_checkout_in_minutes <= 150"),
45
+ x="delay_at_checkout_in_minutes",
46
+ nbins=60,
47
+ title="Distribution des retards (fenêtre -150 à 150 min)"
48
+ )
49
+ st.plotly_chart(fig_delay, use_container_width=True)
50
+
51
+ st.markdown("""
52
+ 👉 La majorité des retards est **courte** :
53
+ - 50 % ≤ 9 min
54
+ - 75 % ≤ 67 min
55
+ Mais certains cas extrêmes dépassent plusieurs jours.
56
+ """)
57
+
58
+ # ANALYSE DES ANNULATIONS
59
+ st.header("❌ Analyse des annulations liées aux retards")
60
+
61
+ mask = df['previous_ended_rental_id'] > 0
62
+ df_nan = df[mask]
63
+ st.metric("Le nombre d'annulations du au retard du véhicule liées à un location précendente est de", (df_nan["state"] == "canceled").sum())
64
+ annulation = (df_nan["state"] == "canceled").sum()
65
+ st.metric("La location moyenne d'un véhicule par jour est de ", df_pricing['rental_price_per_day'].mean().round(2))
66
+ st.metric("La perte du aux annulations s'élève a :", (annulation * loc_moyenne).round(2))
67
+
68
+ canceled = df[df["state"] == "canceled"][["rental_id", "car_id", "previous_ended_rental_id", "checkin_type","time_delta_with_previous_rental_in_minutes"]]
69
+
70
+ merged = canceled.merge(
71
+ df[["rental_id", "car_id", "delay_at_checkout_in_minutes", "checkin_type"]],
72
+ left_on="previous_ended_rental_id",
73
+ right_on="rental_id",
74
+ suffixes=("_canceled", "_previous")
75
+ )
76
+
77
+ conflict_strict = merged[
78
+ merged["delay_at_checkout_in_minutes"] > merged["time_delta_with_previous_rental_in_minutes"]
79
+ ]
80
+ st.metric("Conflits stricts :", len(conflict_strict))
81
+ st.metric("Moyenne retard précédent (conflits seulement) :", conflict_strict["delay_at_checkout_in_minutes"].mean().round(2))
82
+
83
+ st.markdown("""
84
+ 👉 Parmi les **229 annulations liées à une location précédente**,
85
+ **37** sont dues à un retard allant au delà de l'heure de début de la prochaine location.
86
+ Ces retards sont très longs (~4h15 en moyenne).
87
+ """)
88
+
89
+ conflict_minus30 = merged[
90
+ merged["delay_at_checkout_in_minutes"] > merged["time_delta_with_previous_rental_in_minutes"] - 30
91
+ ]
92
+
93
+ conflict_minus60 = merged[
94
+ merged["delay_at_checkout_in_minutes"] > merged["time_delta_with_previous_rental_in_minutes"] - 60
95
+ ]
96
+
97
+ conflict_minus90 = merged[
98
+ merged["delay_at_checkout_in_minutes"] > merged["time_delta_with_previous_rental_in_minutes"] - 90
99
+ ]
100
+
101
+ conflict_minus180 = merged[
102
+ merged["delay_at_checkout_in_minutes"] > merged["time_delta_with_previous_rental_in_minutes"] - 180
103
+ ]
104
+
105
+
106
+ results = [
107
+ {"Buffer": "-30 min", "Conflits": (len(conflict_minus30)-len(conflict_strict)), "Moyenne retard (min)": conflict_minus30["delay_at_checkout_in_minutes"].mean()},
108
+ {"Buffer": "-60 min", "Conflits": (len(conflict_minus60)-len(conflict_strict)), "Moyenne retard (min)": conflict_minus60["delay_at_checkout_in_minutes"].mean()},
109
+ {"Buffer": "-90 min", "Conflits": (len(conflict_minus90)-len(conflict_strict)), "Moyenne retard (min)": conflict_minus90["delay_at_checkout_in_minutes"].mean()},
110
+ {"Buffer": "-180 min", "Conflits": (len(conflict_minus180)-len(conflict_strict)), "Moyenne retard (min)": conflict_minus180["delay_at_checkout_in_minutes"].mean()},
111
+ ]
112
+ df_conflicts = pd.DataFrame(results)
113
+ df_conflicts['lost_value'] = df_conflicts["Conflits"]*loc_moyenne
114
+ st.dataframe(df_conflicts)
115
+
116
+ st.markdown("""Si on regarde 30 miniutes avant le début de la location du véhicule combien ont été annulé (buffer de 30 minutes), on détecte 10 annulations supplémentaires : ces cas correspondent à des locations rendues trop proches de la suivante (<30 min de marge).
117
+ Avec un buffer de 60 minutes, seulement 3 cas en plus → ce sont donc des situations rares mais critiques.
118
+ En élargissant à 90 minutes, on ajoute encore 7 cas.""")
119
+
120
+
121
+ fig_type = px.histogram(
122
+ df.query("-150 <= delay_at_checkout_in_minutes <= 150"),
123
+ x="delay_at_checkout_in_minutes",
124
+ color="checkin_type",
125
+ nbins=int((150 - (-150)) / 10),
126
+ opacity=0.2,
127
+ title="Distribution des retards par type de check-in",
128
+ labels={"delay_at_checkout_in_minutes": "Retard (minutes)"},
129
+ range_x=[-150, 150],
130
+ width=1200, height=600,
131
+ )
132
+ fig_type.update_layout(barmode="overlay")
133
+ fig_type.update_yaxes(title="Nombre de locations")
134
+ #
135
+ st.plotly_chart(fig_type, use_container_width=True)
136
+
137
+
138
+ st.markdown(""" ANALYSE SUR MOBILE """)
139
+
140
+ mask = merged["checkin_type_canceled"] == "mobile"
141
+ merged_mob = merged[mask]
142
+ conflict_strict1= merged_mob[
143
+ merged_mob["delay_at_checkout_in_minutes"] > merged_mob["time_delta_with_previous_rental_in_minutes"]
144
+ ]
145
+ st.metric("Conflits stricts :", len(conflict_strict1))
146
+ st.metric("Moyenne retard précédent (conflits seulement) :", conflict_strict1["delay_at_checkout_in_minutes"].mean().round(2))
147
+
148
+ conflict_1minus30 = merged_mob[
149
+ merged_mob["delay_at_checkout_in_minutes"] > merged_mob["time_delta_with_previous_rental_in_minutes"] - 30
150
+ ]
151
+
152
+ conflict_1minus60 = merged_mob[
153
+ merged_mob["delay_at_checkout_in_minutes"] > merged_mob["time_delta_with_previous_rental_in_minutes"] - 60
154
+ ]
155
+
156
+ conflict_1minus90 = merged_mob[
157
+ merged_mob["delay_at_checkout_in_minutes"] > merged_mob["time_delta_with_previous_rental_in_minutes"] - 90
158
+ ]
159
+
160
+ conflict_1minus180 = merged_mob[
161
+ merged_mob["delay_at_checkout_in_minutes"] > merged_mob["time_delta_with_previous_rental_in_minutes"] - 180
162
+ ]
163
+
164
+
165
+ results = [
166
+ {"Buffer": "-30 min", "Conflits": (len(conflict_1minus30)-len(conflict_strict1)), "Moyenne retard (min)": conflict_1minus30["delay_at_checkout_in_minutes"].mean()},
167
+ {"Buffer": "-60 min", "Conflits": (len(conflict_1minus60)-len(conflict_strict1)), "Moyenne retard (min)": conflict_1minus60["delay_at_checkout_in_minutes"].mean()},
168
+ {"Buffer": "-90 min", "Conflits": (len(conflict_1minus90)-len(conflict_strict1)), "Moyenne retard (min)": conflict_1minus90["delay_at_checkout_in_minutes"].mean()},
169
+ {"Buffer": "-180 min", "Conflits": (len(conflict_1minus180)-len(conflict_strict1)), "Moyenne retard (min)": conflict_1minus180["delay_at_checkout_in_minutes"].mean()},
170
+ ]
171
+ df_conflicts_mob = pd.DataFrame(results)
172
+ df_conflicts_mob['lost_value'] = df_conflicts_mob["Conflits"]*loc_moyenne
173
+ st.dataframe(df_conflicts_mob)
174
 
175
+ st.markdown(""" ANALYSE SUR CONNECT """)
 
 
176
 
177
+ mask = merged["checkin_type_canceled"] == "connect"
178
+ merged_connect = merged[mask]
179
+ conflict_strict2 = merged_connect[
180
+ merged_connect["delay_at_checkout_in_minutes"] > merged_connect["time_delta_with_previous_rental_in_minutes"]
181
+ ]
182
+ st.metric("Conflits stricts :", len(conflict_strict2))
183
+ st.metric("Moyenne retard précédent (conflits seulement) :", conflict_strict2["delay_at_checkout_in_minutes"].mean().round(2))
184
 
 
 
 
 
 
 
 
 
 
185
 
186
+ conflict_2minus30 = merged_connect[
187
+ merged_connect["delay_at_checkout_in_minutes"] > merged_connect["time_delta_with_previous_rental_in_minutes"] - 30
188
+ ]
189
 
 
 
 
 
 
 
 
190
 
191
+ conflict_2minus60 = merged_connect[
192
+ merged_connect["delay_at_checkout_in_minutes"] > merged_connect["time_delta_with_previous_rental_in_minutes"] - 60
193
+ ]
194
 
195
+ conflict_2minus90 = merged_connect[
196
+ merged_connect["delay_at_checkout_in_minutes"] > merged_connect["time_delta_with_previous_rental_in_minutes"] - 90
197
+ ]
198
 
199
+ conflict_2minus180 = merged_connect[
200
+ merged_connect["delay_at_checkout_in_minutes"] > merged_connect["time_delta_with_previous_rental_in_minutes"] - 180
201
+ ]
202
 
 
 
 
203
 
204
+ results = [
205
+ {"Buffer": "-30 min", "Conflits": (len(conflict_2minus30)-len(conflict_strict2)), "Moyenne retard (min)": conflict_2minus30["delay_at_checkout_in_minutes"].mean()},
206
+ {"Buffer": "-60 min", "Conflits": (len(conflict_2minus60)-len(conflict_strict2)), "Moyenne retard (min)": conflict_2minus60["delay_at_checkout_in_minutes"].mean()},
207
+ {"Buffer": "-90 min", "Conflits": (len(conflict_2minus90)-len(conflict_strict2)), "Moyenne retard (min)": conflict_2minus90["delay_at_checkout_in_minutes"].mean()},
208
+ {"Buffer": "-180 min", "Conflits": (len(conflict_2minus180)-len(conflict_strict2)), "Moyenne retard (min)": conflict_2minus180["delay_at_checkout_in_minutes"].mean()},
209
+ ]
210
+ df_conflicts_con = pd.DataFrame(results)
211
 
212
+ df_conflicts_con['lost_value'] = df_conflicts_con["Conflits"]*loc_moyenne
 
 
213
 
214
+ st.dataframe(df_conflicts_con)
215
 
216
+ st.markdown("""Les deux systèmes subissent des annulations liées aux délais courts.
217
+ Connect est plus fréquent en volume absolu, mais ses retards sont légèrement moins sévères.
218
+ Mobile présente moins de cas mais avec des retards plus longs → donc plus difficiles à absorber sans impact client.
219
+ Les deux méthodes sont exposées aux retards, mais Connect encaisse plus de cas tandis que Mobile concentre les retards les plus importants.
220
+ Un buffer opérationnel de 30 minutes permettrai un gain rapide sans trop affecté le CA global.
221
+ """ )
 
222
 
223
+ # IMPACT DES BUFFERS
224
+ st.header("⏳ Simulation de buffer entre locations")
225
+ st.markdown(""" Afin d'analyser l'impact d'un buffer nous prenons cette fois ci que les locations qui ont eu lieu et filtrons toutes les locations ayant débutées dans les 30 minutes qui suit la précéddentes.
226
+ Ces locations sont celle qui risquent d'etre impactées par ce delai """)
227
 
228
+ loc_moyenne = df_pricing["rental_price_per_day"].mean()
229
+ CA_total = (df['state'] == 'ended').sum() * loc_moyenne
230
 
 
 
 
 
 
 
231
 
232
+ df_valid = df[df["state"] == "ended"].copy()
233
+ df_valid["ecart"] = df_valid["time_delta_with_previous_rental_in_minutes"] - df_valid["delay_at_checkout_in_minutes"]
 
 
234
 
235
+ def count_blocked(df, buffer):
236
+ return (df["ecart"] < buffer).sum()
237
 
238
+ results = []
239
+ for b in [30, 60, 90, 180]:
240
+ blocked = count_blocked(df_valid, b)
241
+ perte_estimee = blocked * loc_moyenne
242
+ results.append({"Buffer (min)": b, "Locations bloquées": blocked, "Perte estimée ($)": round(perte_estimee, 2)})
243
 
244
+ df_results = pd.DataFrame(results)
245
+ st.dataframe(df_results)
246
 
 
 
247
 
248
+ st.markdown(""" MOBILE UNIQUEMENT""")
 
 
 
 
249
 
250
+ df_valid_mob = df[(df["state"] == "ended") & (df["checkin_type"] == "mobile")].copy()
251
+ df_valid_mob["ecart"] = df_valid_mob["time_delta_with_previous_rental_in_minutes"] - df_valid_mob["delay_at_checkout_in_minutes"]
252
 
253
+ def count_blocked(df, buffer):
254
+ return (df["ecart"] < buffer).sum()
255
 
256
+ results2 = []
257
+ for b in [30, 60, 90, 180]:
258
+ blocked = count_blocked(df_valid_mob, b)
259
+ perte_estimee = blocked * loc_moyenne
260
+ results2.append({"Buffer (min)": b, "Locations bloquées": blocked, "Perte estimée ($)": round(perte_estimee, 2)})
261
 
262
+ df_results_mob = pd.DataFrame(results2)
263
+ st.dataframe(df_results_mob)
264
 
 
 
265
 
266
+ st.markdown(""" CONNECT UNIQUEMENT""")
 
 
 
 
267
 
268
+ df_valid_con = df[(df["state"] == "ended") & (df["checkin_type"] == "connect")].copy()
269
+ df_valid_con["ecart"] = df_valid_con["time_delta_with_previous_rental_in_minutes"] - df_valid_con["delay_at_checkout_in_minutes"]
270
 
271
+ def count_blocked(df, buffer):
272
+ return (df["ecart"] < buffer).sum()
273
 
274
+ results3 = []
275
+ for b in [30, 60, 90, 180]:
276
+ blocked = count_blocked(df_valid_con, b)
277
+ perte_estimee = blocked * loc_moyenne
278
+ results3.append({"Buffer (min)": b, "Locations bloquées": blocked, "Perte estimée ($)": round(perte_estimee, 2)})
279
 
280
+ df_results_con = pd.DataFrame(results3)
281
+ st.dataframe(df_results_con)
282
 
283
+ st.markdown("""
284
+ 👉 Un buffer **de 30 min bloque ~340 locations** → pertes supérieures aux gains.
285
+ 👉 Un buffer de **180 min réduit beaucoup les annulations** mais fait perdre trop de CA.
286
+ **Conclusion : un buffer global n’est pas viable.**
287
+ """)
288
 
289
+ # CONCLUSION BUSINESS
290
+ st.header("📌 Conclusion business")
 
 
 
291
 
292
+ st.markdown("""
293
+ Nos analyses montrent :
294
+ - Les retards courts sont fréquents, mais les **annulations sont rares (229 cas)**.
295
+ - Les retards extrêmes sont à l’origine de la majorité des conflits.
296
+ - Un buffer global **réduit les annulations mais détruit plus de revenu** qu’il n’en sauve.
297
+
298
+ ✅ **Recommandations** :
299
+ - Pas de buffer global.
300
+ - Buffer ciblé (<1h) sur cas à risque, zone chaude.
301
+ - Notifications pour inciter à rendre le véhicule en avance.
302
+ - Eventuellement pénalités pour retards répétés.(=> mais c'est anticommercial )
303
 
304
+ 👉 L’impact économique du buffer est donc limité,
305
+ mais des mesures ciblées peuvent réduire la friction sans perte majeure de CA.
306
+ """)
307
+
 
308
 
309
+ # ANALYSE DU PRICING
310
+ with tab2:
311
+ st.header("Analyse Pricing")
312
+
313
+ df_pricing = pd.read_csv("data/pricing_clean.csv")
314
 
315
+ fig_price = px.histogram(df_pricing, x="rental_price_per_day", nbins=50,
316
+ title="Distribution des prix de location par jour")
317
+ st.plotly_chart(fig_price, use_container_width=True)
318
 
319
 
320
+ fig_car_typ = px.box(df_pricing, x="car_type", y="rental_price_per_day",
321
+ title="Prix par type de véhicule")
322
+ st.plotly_chart(fig_car_typ, use_container_width=True)
323
 
324
 
325
+ st.markdown("""
326
+ 👉 Le marché est concentré autour de **100–140 $/jour**.
327
+ Les **SUV, coupés, cabriolets** sont plus chers.
328
+ Les **citadines** et **hatchbacks** moins chers.
329
+ """)
 
 
 
 
 
 
 
330
 
 
331
 
332
+ df_corr = df_pricing.copy()
333
+ bool_cols = df_corr.select_dtypes(include="bool").columns
334
+ df_corr[bool_cols] = df_corr[bool_cols].astype(int)
 
 
 
 
 
 
335
 
336
+ num_df = df_corr.select_dtypes(include=["int64", "float64"])
337
 
338
+ corr = num_df.corr()
339
 
340
+ fig_cor = px.imshow(
341
+ corr,
342
+ text_auto=True,
343
+ color_continuous_scale="RdBu_r",
344
+ title="Matrice de corrélation",
345
+ height=800,
346
+ width=1000
347
+ )
348
+ st.plotly_chart(fig_cor, use_container_width=True)
349
+
350
+ st.markdown("""
351
+ + corrélation prix ↔ puissance moteur
352
+ – corrélation prix ↔ kilométrage
353
+ Transmission automatique = + valeur ajoutée
354
+ Présence de GPS = + valeur ajoutée """)
355
+
356
+ with tab3:
357
+ st.header("Tester l'API de prédiction des prix")
358
+
359
+ API_URL = "https://gdleds-api-get.hf.space/predict"
360
+
361
+ with st.form("api_test_form"):
362
+ st.write("### Entrez les caractéristiques du véhicule :")
363
+
364
+ model_key = st.selectbox("Marque", ["Audi", "BMW", "Renault", "Peugeot", "Citroën"])
365
+ mileage = st.number_input("Kilométrage", min_value=0, max_value=300000, value=50000)
366
+ engine_power = st.number_input("Puissance moteur (CV)", min_value=30, max_value=300, value=110)
367
+ fuel = st.selectbox("Carburant", ["diesel", "petrol", "hybrid_petrol", "electro"])
368
+ paint_color = st.selectbox("Couleur", ["black", "white", "grey", "red", "blue", "silver"])
369
+ car_type = st.selectbox("Type de véhicule", ["hatchback", "subcompact", "sedan", "suv", "estate", "convertible", "coupe", "van"])
370
+
371
+ private_parking_available = st.checkbox("Parking privé disponible", value=True)
372
+ has_gps = st.checkbox("GPS", value=True)
373
+ has_air_conditioning = st.checkbox("Climatisation", value=True)
374
+ automatic_car = st.checkbox("Boîte automatique", value=False)
375
+ has_getaround_connect = st.checkbox("Getaround Connect", value=True)
376
+ has_speed_regulator = st.checkbox("Régulateur de vitesse", value=True)
377
+ winter_tires = st.checkbox("Pneus hiver", value=False)
378
+
379
+ submitted = st.form_submit_button("Prédire le prix")
380
+
381
+ if submitted:
382
+ input_data = {
383
+ "input": [[
384
+ model_key, mileage, engine_power, fuel, paint_color, car_type,
385
+ private_parking_available, has_gps, has_air_conditioning,
386
+ automatic_car, has_getaround_connect, has_speed_regulator, winter_tires
387
+ ]]
388
+ }
389
+
390
+ try:
391
+ response = requests.post(API_URL, json=input_data)
392
+ if response.status_code == 200:
393
+ prediction = response.json().get("prediction", [None])[0]
394
+ st.success(f"💰 Prix prédit : {prediction:.2f} $ / jour")
395
+ else:
396
+ st.error(f"Erreur API ({response.status_code}) : {response.text}")
397
+ except Exception as e:
398
+ st.error(f"Impossible d'appeler l'API : {e}")
399
 
 
400
 
401