sony9316 commited on
Commit
e9d239b
Β·
verified Β·
1 Parent(s): c5bc2b2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +402 -308
app.py CHANGED
@@ -2,333 +2,427 @@
2
  import streamlit as st
3
  import pandas as pd
4
  import numpy as np
5
- from pathlib import Path
6
  import matplotlib.pyplot as plt
7
  import seaborn as sns
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
- # ------- Page setup -------
10
- st.set_page_config(page_title="Getaround β€” Threshold & Scope Decision", layout="wide")
11
- sns.set_style("whitegrid")
12
-
13
- # ------- Data loading -------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  @st.cache_data(show_spinner=False)
15
  def load_data():
16
- for p in ["/mnt/data/get_around_delay_analysis.csv", "get_around_delay_analysis.csv"]:
17
- if Path(p).exists():
18
- df = pd.read_csv(p)
19
- break
20
- else:
21
- st.error("CSV not found. Put 'get_around_delay_analysis.csv' in the app folder or /mnt/data.")
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  st.stop()
23
-
24
- # drop unnamed artifacts
 
25
  df = df.drop(columns=[c for c in df.columns if c.lower().startswith("unnamed")], errors="ignore")
26
-
27
- # helpers
28
- df["has_gap"] = df["time_delta_with_previous_rental_in_minutes"].notnull()
29
- clean = df["delay_at_checkout_in_minutes"].copy()
30
- clean = clean.where(clean.between(-720, 720)) # cap to +/-12h
31
- df["clean_delay"] = clean
32
-
33
- # overlap & wait for next
34
- cond = df["has_gap"] & df["clean_delay"].notnull()
35
- df["overlap_problem"] = False
36
- df.loc[cond, "overlap_problem"] = df.loc[cond, "clean_delay"] > df.loc[cond, "time_delta_with_previous_rental_in_minutes"]
37
- df["wait_for_next_minutes"] = 0.0
38
- df.loc[cond, "wait_for_next_minutes"] = (df.loc[cond, "clean_delay"] - df.loc[cond, "time_delta_with_previous_rental_in_minutes"]).clip(lower=0)
39
-
40
- # detect a revenue column if available
41
- candidates = {"owner_revenue","rental_price","price","price_eur","revenue"}
42
- rev_col = next((c for c in df.columns if c.lower() in candidates), None)
43
- df.attrs["revenue_col"] = rev_col
 
 
 
 
 
44
  return df
45
 
46
- df = load_data()
47
- REVENUE_COL = df.attrs.get("revenue_col")
48
-
49
- # ------- Helpers -------
50
- def filter_scope(data, scope):
51
  if scope == "connect":
52
- return data[data["checkin_type"].str.lower() == "connect"]
53
- return data
54
-
55
- def metrics_for(data, threshold):
56
- """Compute all decision metrics for given data scope + threshold."""
57
- d = data.copy()
58
- d_gap = d[d["has_gap"]].copy()
59
- if d_gap.empty:
60
- return dict(
61
- rentals_with_gap=0, blocked=0, blocked_rate=0.0,
62
- problems_today=0, overlap_rate=0.0, solved=0, solve_rate=0.0,
63
- avg_wait=np.nan, revenue_share_blocked=np.nan
64
- )
65
- d_gap["blocked"] = d_gap["time_delta_with_previous_rental_in_minutes"] < threshold
66
- d_gap["solved"] = d_gap["overlap_problem"] & d_gap["blocked"]
67
-
68
- rentals_with_gap = len(d_gap)
69
- blocked = int(d_gap["blocked"].sum())
70
- blocked_rate = blocked / rentals_with_gap
71
- problems_today = int(d_gap["overlap_problem"].sum())
72
- overlap_rate = problems_today / rentals_with_gap
73
- solved = int(d_gap["solved"].sum())
74
- solve_rate = solved / blocked if blocked > 0 else 0.0
75
- avg_wait = d_gap.loc[d_gap["overlap_problem"], "wait_for_next_minutes"].mean()
76
-
77
- # revenue share blocked
78
- if REVENUE_COL:
79
- rev = pd.to_numeric(d[REVENUE_COL], errors="coerce")
80
- rev_total = rev.sum(skipna=True)
81
- rev_blocked = pd.to_numeric(d_gap.loc[d_gap["blocked"], REVENUE_COL], errors="coerce").sum(skipna=True)
82
- revenue_share_blocked = (rev_blocked / rev_total) if rev_total > 0 else np.nan
83
  else:
84
- revenue_share_blocked = np.nan
85
-
86
- return dict(
87
- rentals_with_gap=rentals_with_gap,
88
- blocked=blocked,
89
- blocked_rate=blocked_rate,
90
- problems_today=problems_today,
91
- overlap_rate=overlap_rate,
92
- solved=solved,
93
- solve_rate=solve_rate,
94
- avg_wait=avg_wait,
95
- revenue_share_blocked=revenue_share_blocked
96
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
- def sweep_thresholds(data, thresholds):
99
- rows = []
100
- for t in thresholds:
101
- m = metrics_for(data, t)
102
- rows.append({"threshold": t, **m})
103
- return pd.DataFrame(rows)
104
-
105
- # ------- Sidebar controls -------
106
- st.sidebar.header("Controls")
107
- threshold = st.sidebar.slider("Minimum delay (minutes)", 15, 180, step=15, value=90)
108
- scope = st.sidebar.radio("Scope", options=["all", "connect"], format_func=lambda s: "All cars" if s=="all" else "Connect only", horizontal=True)
109
- thresholds = list(range(15, 181, 15))
110
- df_scope = filter_scope(df, scope)
111
-
112
- # =========================================================
113
- # SECTION 1 β€” THRESHOLD DECISION
114
- # =========================================================
115
- st.title("Decision 1 β€” Threshold (minimum delay between rentals)")
116
-
117
- # Current-threshold KPIs
118
- m_now = metrics_for(df_scope, threshold)
119
- k1,k2,k3,k4,k5 = st.columns(5)
120
- k1.metric("Rentals with gap", f"{m_now['rentals_with_gap']:,}")
121
- k2.metric("Blocked rentals", f"{m_now['blocked']:,}")
122
- k3.metric("Blocked rate", f"{m_now['blocked_rate']:.1%}")
123
- k4.metric("Problems solved", f"{m_now['solved']:,}")
124
- k5.metric("Solve rate (given blocked)", f"{m_now['solve_rate']:.1%}")
125
-
126
- k6,k7 = st.columns(2)
127
- k6.metric("Overlap problems today", f"{m_now['problems_today']:,}")
128
- k7.metric("Avg wait among impacted (min)", f"{(m_now['avg_wait'] or 0):.1f}")
129
-
130
- if REVENUE_COL:
131
- st.caption(f"Revenue field detected: **{REVENUE_COL}**")
132
- st.metric("Share of owner revenue blocked", f"{(m_now['revenue_share_blocked'] or 0):.1%}")
133
- else:
134
- st.info("No revenue column found. Revenue impact graphs will use **% of rentals blocked** as a proxy.")
135
-
136
- # Threshold sweep (current scope)
137
- st.subheader("How metrics evolve with the threshold (current scope)")
138
- sweep_df = sweep_thresholds(df_scope, thresholds)
139
-
140
- c1, c2 = st.columns(2)
141
- with c1:
142
- fig, ax = plt.subplots(figsize=(6,4))
143
- ax.plot(sweep_df["threshold"], sweep_df["blocked_rate"], marker="o", label="Blocked rate (of with-gap)")
144
- ax.plot(sweep_df["threshold"], sweep_df["solve_rate"], marker="o", label="Solve rate (given blocked)")
145
- ax.set_xlabel("Threshold (minutes)")
146
- ax.set_ylabel("Rate")
147
- ax.legend()
148
- st.pyplot(fig, clear_figure=True)
149
-
150
- with c2:
151
- fig, ax = plt.subplots(figsize=(6,4))
152
- ax.plot(sweep_df["threshold"], sweep_df["solved"], marker="o")
153
- ax.set_xlabel("Threshold (minutes)")
154
- ax.set_ylabel("Problems solved (count)")
155
- ax.set_title("Problems solved vs threshold")
156
- st.pyplot(fig, clear_figure=True)
157
-
158
- # Required analysis: How often late & impact next driver
159
- st.subheader("How often are drivers late, and how much does it impact the next driver?")
160
- d_lat = df_scope[df_scope["overlap_problem"]]
161
- colA, colB = st.columns(2)
162
- with colA:
163
- rate = (len(d_lat) / max(1, df_scope["has_gap"].sum()))
164
- st.metric("Overlap (late beyond gap) rate", f"{rate:.1%}")
165
- with colB:
166
- st.metric("Avg wait for impacted next driver (min)", f"{d_lat['wait_for_next_minutes'].mean():.1f}")
167
-
168
- fig, ax = plt.subplots(figsize=(6,4))
169
- ax.hist(d_lat["wait_for_next_minutes"].dropna(), bins=30)
170
- ax.set_xlabel("Wait for next driver (minutes)")
171
- ax.set_ylabel("Count of rentals")
172
- ax.set_title("Distribution of wait time when overlaps occur")
173
- st.pyplot(fig, clear_figure=True)
174
-
175
- # Required analysis: Share of owner revenue affected (or proxy)
176
- st.subheader("Which share of owner revenue would be affected?")
177
- if REVENUE_COL:
178
- tmp = []
179
- for t in thresholds:
180
- m = metrics_for(df_scope, t)
181
- tmp.append((t, m["revenue_share_blocked"]))
182
- rev_df = pd.DataFrame(tmp, columns=["threshold","revenue_share_blocked"]).dropna()
183
- fig, ax = plt.subplots(figsize=(6,4))
184
- ax.plot(rev_df["threshold"], rev_df["revenue_share_blocked"], marker="o")
185
- ax.set_xlabel("Threshold (minutes)")
186
- ax.set_ylabel("Revenue share blocked")
187
- st.pyplot(fig, clear_figure=True)
188
- else:
189
- fig, ax = plt.subplots(figsize=(6,4))
190
- ax.plot(sweep_df["threshold"], sweep_df["blocked_rate"], marker="o")
191
- ax.set_xlabel("Threshold (minutes)")
192
- ax.set_ylabel("Share of rentals blocked (proxy)")
193
- st.pyplot(fig, clear_figure=True)
194
-
195
- # ---- Automatic, data-driven threshold suggestion (transparent rule)
196
- # Rule: choose threshold maximizing "problems solved" while keeping blocked_rate <= cap.
197
- # You can tweak cap below; default 15% of with-gap rentals.
198
- BLOCKED_RATE_CAP = 0.15
199
- candidates = sweep_df[sweep_df["blocked_rate"] <= BLOCKED_RATE_CAP]
200
- if not candidates.empty:
201
- best_row = candidates.sort_values(["solved","solve_rate","threshold"], ascending=[False,False,True]).iloc[0]
202
- suggested_threshold = int(best_row["threshold"])
203
- else:
204
- # If all thresholds exceed the cap, pick the one with best ratio solved/blocked
205
- sweep_df["efficiency"] = sweep_df["solved"] / sweep_df["blocked"].replace({0:np.nan})
206
- best_row = sweep_df.sort_values(["efficiency","solved"], ascending=False).iloc[0]
207
- suggested_threshold = int(best_row["threshold"])
208
-
209
- st.markdown("**Threshold Recommendation (based on current scope):**")
210
- st.success(
211
- f"Set the minimum delay to **{suggested_threshold} minutes** β€” "
212
- f"it solves **{int(best_row['solved']):,}** problematic cases while keeping the "
213
- f"blocked rate at **{best_row['blocked_rate']:.1%}**."
214
  )
215
 
216
- st.markdown("---")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
 
218
- # =========================================================
219
- # SECTION 2 β€” SCOPE DECISION (All cars vs Connect only)
220
- # =========================================================
221
- st.title("Decision 2 β€” Scope (apply to all cars or Connect only?)")
222
-
223
- # Build threshold sweeps for both scopes
224
- df_all = filter_scope(df, "all")
225
- df_conn = filter_scope(df, "connect")
226
- sweep_all = sweep_thresholds(df_all, thresholds).assign(scope="All cars")
227
- sweep_con = sweep_thresholds(df_conn, thresholds).assign(scope="Connect only")
228
- cmp = pd.concat([sweep_all, sweep_con], ignore_index=True)
229
-
230
- # Required analysis: rentals affected vs threshold & scope
231
- st.subheader("How many rentals would be affected by threshold & scope?")
232
- fig, ax = plt.subplots(figsize=(7,4))
233
- for label, g in cmp.groupby("scope"):
234
- ax.plot(g["threshold"], g["blocked"], marker="o", label=label)
235
- ax.set_xlabel("Threshold (minutes)")
236
- ax.set_ylabel("Blocked rentals (count, with-gap only)")
237
- ax.legend()
238
- st.pyplot(fig, clear_figure=True)
239
-
240
- # Required analysis: problematic cases solved vs threshold & scope
241
- st.subheader("How many problematic cases will be solved?")
242
- fig, ax = plt.subplots(figsize=(7,4))
243
- for label, g in cmp.groupby("scope"):
244
- ax.plot(g["threshold"], g["solved"], marker="o", label=label)
245
- ax.set_xlabel("Threshold (minutes)")
246
- ax.set_ylabel("Problems solved (count)")
247
- ax.legend()
248
- st.pyplot(fig, clear_figure=True)
249
-
250
- # Efficiency plot: solved per blocked (avoid harming availability)
251
- st.subheader("Efficiency: problems solved per blocked rental")
252
- cmp_eff = cmp.copy()
253
- cmp_eff["efficiency"] = cmp_eff["solved"] / cmp_eff["blocked"].replace({0:np.nan})
254
- fig, ax = plt.subplots(figsize=(7,4))
255
- for label, g in cmp_eff.groupby("scope"):
256
- ax.plot(g["threshold"], g["efficiency"], marker="o", label=label)
257
- ax.set_xlabel("Threshold (minutes)")
258
- ax.set_ylabel("Solved / Blocked")
259
- ax.legend()
260
- st.pyplot(fig, clear_figure=True)
261
-
262
- # Revenue share by scope (or proxy)
263
- st.subheader("Share of owner revenue affected (by scope)")
264
- if REVENUE_COL:
265
- rows = []
266
- for sc, dat in [("All cars", df_all), ("Connect only", df_conn)]:
267
- for t in thresholds:
268
- m = metrics_for(dat, t)
269
- rows.append({"scope": sc, "threshold": t, "revenue_share_blocked": m["revenue_share_blocked"]})
270
- rev_cmp = pd.DataFrame(rows).dropna()
271
- fig, ax = plt.subplots(figsize=(7,4))
272
- for label, g in rev_cmp.groupby("scope"):
273
- ax.plot(g["threshold"], g["revenue_share_blocked"], marker="o", label=label)
274
- ax.set_xlabel("Threshold (minutes)")
275
- ax.set_ylabel("Revenue share blocked")
276
- ax.legend()
277
- st.pyplot(fig, clear_figure=True)
278
- else:
279
- fig, ax = plt.subplots(figsize=(7,4))
280
- for label, g in cmp.groupby("scope"):
281
- ax.plot(g["threshold"], g["blocked_rate"], marker="o", label=label)
282
- ax.set_xlabel("Threshold (minutes)")
283
- ax.set_ylabel("Share of rentals blocked (proxy)")
284
- ax.legend()
285
- st.pyplot(fig, clear_figure=True)
286
-
287
- # ---- Scope recommendation (transparent rule)
288
- # Pick, at your chosen threshold, the scope with (a) more problems solved,
289
- # and (b) lower or comparable blocked rate; if tie, pick higher efficiency.
290
- m_all = metrics_for(df_all, threshold)
291
- m_conn = metrics_for(df_conn, threshold)
292
-
293
- def efficiency(m):
294
- return (m["solved"] / m["blocked"]) if m["blocked"] > 0 else 0.0
295
-
296
- choice = "All cars"
297
- reason = ""
298
- if (m_conn["solved"] > m_all["solved"]) and (m_conn["blocked_rate"] <= m_all["blocked_rate"]):
299
- choice = "Connect only"
300
- elif (abs(m_conn["solved"] - m_all["solved"]) <= 2) and (m_conn["blocked_rate"] + 0.01 < m_all["blocked_rate"]):
301
- choice = "Connect only"
302
- else:
303
- # tie-breaker on efficiency
304
- if efficiency(m_conn) > efficiency(m_all) and m_conn["blocked_rate"] <= m_all["blocked_rate"] + 0.01:
305
- choice = "Connect only"
306
-
307
- if choice == "Connect only":
308
- reason = (
309
- f"At **{threshold} min**, Connect-only solves **{m_conn['solved']:,}** vs **{m_all['solved']:,}** "
310
- f"problems, with a blocked rate of **{m_conn['blocked_rate']:.1%}** vs **{m_all['blocked_rate']:.1%}**."
311
  )
312
- else:
313
- reason = (
314
- f"At **{threshold} min**, All cars solves **{m_all['solved']:,}** vs **{m_conn['solved']:,}** "
315
- f"problems, with a blocked rate of **{m_all['blocked_rate']:.1%}** vs **{m_conn['blocked_rate']:.1%}**."
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  )
317
 
318
- st.markdown("**Scope Recommendation (at your selected threshold):**")
319
- st.success(f"Apply to **{choice}**. {reason}")
320
 
321
- # ------- Method note (expandable) -------
322
- with st.expander("How we compute metrics (transparency)"):
323
- st.markdown("""
324
- - **Overlap problem (today):** `delay_at_checkout_in_minutes (cleaned) > time_delta_with_previous_rental_in_minutes`
325
- - **Next-driver wait:** `max(delay - gap, 0)`
326
- - **Blocked by buffer:** `gap < threshold`
327
- - **Solved by buffer:** `overlap_problem AND blocked`
328
- - **Blocked rate:** `blocked / rentals_with_gap`
329
- - **Solve rate:** `solved / blocked`
330
- - **Revenue share blocked:** sum(revenue for blocked) / sum(revenue all), if a revenue column exists.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  """)
332
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  st.markdown("---")
334
- st.caption("Built for Getaround β€” answers focused on Threshold & Scope only, with transparent calculations.")
 
2
  import streamlit as st
3
  import pandas as pd
4
  import numpy as np
 
5
  import matplotlib.pyplot as plt
6
  import seaborn as sns
7
+ from pathlib import Path
8
+ import plotly.express as px
9
+ import plotly.graph_objects as go
10
+ from plotly.subplots import make_subplots
11
+
12
+ # Page configuration
13
+ st.set_page_config(
14
+ page_title="Getaround Delay Analysis Dashboard",
15
+ page_icon="πŸš—",
16
+ layout="wide",
17
+ initial_sidebar_state="expanded"
18
+ )
19
 
20
+ # Custom CSS for better styling
21
+ st.markdown("""
22
+ <style>
23
+ .main-header {
24
+ font-size: 2.5rem;
25
+ font-weight: 700;
26
+ color: #1f77b4;
27
+ text-align: center;
28
+ margin-bottom: 2rem;
29
+ }
30
+ .section-header {
31
+ font-size: 1.8rem;
32
+ font-weight: 600;
33
+ color: #2c3e50;
34
+ margin: 2rem 0 1rem 0;
35
+ border-bottom: 2px solid #3498db;
36
+ padding-bottom: 0.5rem;
37
+ }
38
+ .metric-card {
39
+ background-color: #f8f9fa;
40
+ padding: 1rem;
41
+ border-radius: 0.5rem;
42
+ border-left: 4px solid #3498db;
43
+ }
44
+ .insight-box {
45
+ background-color: #e8f4f8;
46
+ padding: 1rem;
47
+ border-radius: 0.5rem;
48
+ border-left: 4px solid #17a2b8;
49
+ margin: 1rem 0;
50
+ }
51
+ </style>
52
+ """, unsafe_allow_html=True)
53
+
54
+ # Data loading function
55
  @st.cache_data(show_spinner=False)
56
  def load_data():
57
+ """Load and preprocess the rental data"""
58
+ # Try multiple possible file locations
59
+ possible_paths = [
60
+ "/mnt/data/get_around_delay_analysis.csv",
61
+ "get_around_delay_analysis.csv",
62
+ "data/get_around_delay_analysis.csv"
63
+ ]
64
+
65
+ df = None
66
+ for path in possible_paths:
67
+ if Path(path).exists():
68
+ try:
69
+ df = pd.read_csv(path)
70
+ break
71
+ except Exception as e:
72
+ continue
73
+
74
+ if df is None:
75
+ st.error("❌ CSV file not found. Please ensure 'get_around_delay_analysis.csv' is in the correct location.")
76
  st.stop()
77
+
78
+ # Clean the data
79
+ # Remove unnamed columns
80
  df = df.drop(columns=[c for c in df.columns if c.lower().startswith("unnamed")], errors="ignore")
81
+
82
+ # Create helper columns
83
+ df["has_previous_rental"] = df["time_delta_with_previous_rental_in_minutes"].notnull()
84
+
85
+ # Clean delay data (remove extreme outliers - more than 12 hours)
86
+ df["clean_delay"] = df["delay_at_checkout_in_minutes"].copy()
87
+ df["clean_delay"] = df["clean_delay"].where(df["clean_delay"].between(-720, 720))
88
+
89
+ # Calculate problematic cases
90
+ df["is_late"] = (df["clean_delay"] > 0) & df["has_previous_rental"]
91
+ df["causes_problem"] = False
92
+ df["wait_time_next_driver"] = 0
93
+
94
+ # For rentals with previous rentals, check if delay causes overlap
95
+ mask = df["has_previous_rental"] & df["clean_delay"].notnull()
96
+ df.loc[mask, "causes_problem"] = (
97
+ df.loc[mask, "clean_delay"] > df.loc[mask, "time_delta_with_previous_rental_in_minutes"]
98
+ )
99
+ df.loc[mask, "wait_time_next_driver"] = np.maximum(
100
+ 0,
101
+ df.loc[mask, "clean_delay"] - df.loc[mask, "time_delta_with_previous_rental_in_minutes"]
102
+ )
103
+
104
  return df
105
 
106
+ # Analysis functions
107
+ def calculate_impact_metrics(df, threshold_minutes, scope="all"):
108
+ """Calculate key metrics for given threshold and scope"""
109
+
110
+ # Filter by scope
111
  if scope == "connect":
112
+ df_filtered = df[df["checkin_type"].str.lower() == "connect"].copy()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  else:
114
+ df_filtered = df.copy()
115
+
116
+ # Only consider rentals with previous rentals
117
+ df_with_prev = df_filtered[df_filtered["has_previous_rental"]].copy()
118
+
119
+ if len(df_with_prev) == 0:
120
+ return {
121
+ "total_rentals": len(df_filtered),
122
+ "rentals_with_previous": 0,
123
+ "blocked_rentals": 0,
124
+ "blocked_percentage": 0.0,
125
+ "current_problems": 0,
126
+ "problems_solved": 0,
127
+ "problem_solve_rate": 0.0,
128
+ "avg_wait_time": 0.0,
129
+ "revenue_impact": 0.0
130
+ }
131
+
132
+ # Calculate blocked rentals
133
+ df_with_prev["would_be_blocked"] = df_with_prev["time_delta_with_previous_rental_in_minutes"] < threshold_minutes
134
+
135
+ # Calculate solved problems
136
+ df_with_prev["problem_solved"] = df_with_prev["causes_problem"] & df_with_prev["would_be_blocked"]
137
+
138
+ # Calculate metrics
139
+ total_rentals = len(df_filtered)
140
+ rentals_with_previous = len(df_with_prev)
141
+ blocked_rentals = df_with_prev["would_be_blocked"].sum()
142
+ blocked_percentage = (blocked_rentals / rentals_with_previous) * 100
143
+ current_problems = df_with_prev["causes_problem"].sum()
144
+ problems_solved = df_with_prev["problem_solved"].sum()
145
+ problem_solve_rate = (problems_solved / blocked_rentals * 100) if blocked_rentals > 0 else 0
146
+ avg_wait_time = df_with_prev[df_with_prev["causes_problem"]]["wait_time_next_driver"].mean()
147
+
148
+ # Revenue impact (approximate as percentage of blocked rentals)
149
+ revenue_impact = blocked_percentage
150
+
151
+ return {
152
+ "total_rentals": total_rentals,
153
+ "rentals_with_previous": rentals_with_previous,
154
+ "blocked_rentals": blocked_rentals,
155
+ "blocked_percentage": blocked_percentage,
156
+ "current_problems": current_problems,
157
+ "problems_solved": problems_solved,
158
+ "problem_solve_rate": problem_solve_rate,
159
+ "avg_wait_time": avg_wait_time if not pd.isna(avg_wait_time) else 0,
160
+ "revenue_impact": revenue_impact
161
+ }
162
+
163
+ def create_threshold_comparison(df):
164
+ """Create comparison data for different thresholds"""
165
+ thresholds = range(0, 301, 30) # 0 to 300 minutes, step 30
166
+
167
+ results = []
168
+ for threshold in thresholds:
169
+ for scope in ["all", "connect"]:
170
+ metrics = calculate_impact_metrics(df, threshold, scope)
171
+ results.append({
172
+ "threshold": threshold,
173
+ "scope": "All Cars" if scope == "all" else "Connect Only",
174
+ **metrics
175
+ })
176
+
177
+ return pd.DataFrame(results)
178
+
179
+ # Load data
180
+ with st.spinner("Loading data..."):
181
+ df = load_data()
182
+
183
+ # Main title
184
+ st.markdown('<h1 class="main-header">πŸš— Getaround Delay Analysis Dashboard</h1>', unsafe_allow_html=True)
185
+
186
+ # Sidebar controls
187
+ st.sidebar.markdown("## πŸŽ›οΈ Dashboard Controls")
188
+ threshold = st.sidebar.slider(
189
+ "Minimum Delay Threshold (minutes)",
190
+ min_value=0, max_value=300, value=120, step=15,
191
+ help="Select the minimum delay between consecutive rentals"
192
+ )
193
 
194
+ scope = st.sidebar.selectbox(
195
+ "Scope of Implementation",
196
+ options=["all", "connect"],
197
+ format_func=lambda x: "All Cars" if x == "all" else "Connect Cars Only",
198
+ help="Choose whether to apply the delay to all cars or just Connect cars"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  )
200
 
201
+ # Data overview
202
+ st.markdown('<div class="section-header">πŸ“Š Data Overview</div>', unsafe_allow_html=True)
203
+
204
+ col1, col2, col3, col4 = st.columns(4)
205
+ with col1:
206
+ st.metric("Total Rentals", f"{len(df):,}")
207
+ with col2:
208
+ st.metric("Connect Rentals", f"{len(df[df['checkin_type'].str.lower() == 'connect']):,}")
209
+ with col3:
210
+ st.metric("Mobile Rentals", f"{len(df[df['checkin_type'].str.lower() == 'mobile']):,}")
211
+ with col4:
212
+ st.metric("Rentals with Previous", f"{df['has_previous_rental'].sum():,}")
213
+
214
+ # Current situation analysis
215
+ st.markdown('<div class="section-header">πŸ” Current Situation Analysis</div>', unsafe_allow_html=True)
216
+
217
+ current_metrics = calculate_impact_metrics(df, 0, scope) # 0 threshold = current situation
218
+
219
+ col1, col2 = st.columns(2)
220
+
221
+ with col1:
222
+ st.markdown("### Late Return Frequency")
223
+
224
+ # Calculate late return stats
225
+ late_returns = df[df["is_late"] & df["has_previous_rental"]]
226
+ total_with_prev = df[df["has_previous_rental"]]
227
+ late_percentage = (len(late_returns) / len(total_with_prev)) * 100 if len(total_with_prev) > 0 else 0
228
+
229
+ st.metric("Late Returns", f"{len(late_returns):,} ({late_percentage:.1f}%)")
230
+ st.metric("Cause Problems", f"{current_metrics['current_problems']:,}")
231
+ st.metric("Average Wait Time", f"{current_metrics['avg_wait_time']:.1f} min")
232
+
233
+ with col2:
234
+ st.markdown("### Delay Distribution")
235
+
236
+ # Create delay distribution plot
237
+ fig = px.histogram(
238
+ df[df["clean_delay"].notnull() & df["has_previous_rental"]],
239
+ x="clean_delay",
240
+ nbins=50,
241
+ title="Distribution of Checkout Delays",
242
+ labels={"clean_delay": "Delay at Checkout (minutes)", "count": "Number of Rentals"}
243
+ )
244
+ fig.add_vline(x=0, line_dash="dash", line_color="red", annotation_text="On Time")
245
+ st.plotly_chart(fig, use_container_width=True)
246
 
247
+ # Impact analysis with selected threshold
248
+ st.markdown('<div class="section-header">βš–οΈ Impact Analysis</div>', unsafe_allow_html=True)
249
+
250
+ metrics = calculate_impact_metrics(df, threshold, scope)
251
+
252
+ col1, col2, col3, col4 = st.columns(4)
253
+ with col1:
254
+ st.metric(
255
+ "Blocked Rentals",
256
+ f"{metrics['blocked_rentals']:,}",
257
+ help="Number of rentals that would be blocked by the minimum delay"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  )
259
+ with col2:
260
+ st.metric(
261
+ "Blocked Rate",
262
+ f"{metrics['blocked_percentage']:.1f}%",
263
+ help="Percentage of rentals with previous rental that would be blocked"
264
+ )
265
+ with col3:
266
+ st.metric(
267
+ "Problems Solved",
268
+ f"{metrics['problems_solved']:,}",
269
+ help="Number of current problematic cases that would be prevented"
270
+ )
271
+ with col4:
272
+ st.metric(
273
+ "Solve Efficiency",
274
+ f"{metrics['problem_solve_rate']:.1f}%",
275
+ help="Percentage of blocked rentals that actually solve a problem"
276
  )
277
 
278
+ # Threshold comparison analysis
279
+ st.markdown('<div class="section-header">πŸ“ˆ Threshold Comparison Analysis</div>', unsafe_allow_html=True)
280
 
281
+ with st.spinner("Calculating threshold impacts..."):
282
+ comparison_df = create_threshold_comparison(df)
283
+
284
+ # Create comparison visualizations
285
+ fig = make_subplots(
286
+ rows=2, cols=2,
287
+ subplot_titles=("Blocked Rentals vs Threshold", "Problems Solved vs Threshold",
288
+ "Efficiency (Solve Rate) vs Threshold", "Revenue Impact vs Threshold"),
289
+ specs=[[{"secondary_y": False}, {"secondary_y": False}],
290
+ [{"secondary_y": False}, {"secondary_y": False}]]
291
+ )
292
+
293
+ colors = {"All Cars": "#1f77b4", "Connect Only": "#ff7f0e"}
294
+
295
+ for scope_name in ["All Cars", "Connect Only"]:
296
+ scope_data = comparison_df[comparison_df["scope"] == scope_name]
297
+
298
+ # Blocked rentals
299
+ fig.add_trace(
300
+ go.Scatter(x=scope_data["threshold"], y=scope_data["blocked_rentals"],
301
+ mode="lines+markers", name=f"{scope_name}",
302
+ line=dict(color=colors[scope_name]), legendgroup=scope_name),
303
+ row=1, col=1
304
+ )
305
+
306
+ # Problems solved
307
+ fig.add_trace(
308
+ go.Scatter(x=scope_data["threshold"], y=scope_data["problems_solved"],
309
+ mode="lines+markers", name=f"{scope_name}",
310
+ line=dict(color=colors[scope_name]), legendgroup=scope_name, showlegend=False),
311
+ row=1, col=2
312
+ )
313
+
314
+ # Efficiency
315
+ fig.add_trace(
316
+ go.Scatter(x=scope_data["threshold"], y=scope_data["problem_solve_rate"],
317
+ mode="lines+markers", name=f"{scope_name}",
318
+ line=dict(color=colors[scope_name]), legendgroup=scope_name, showlegend=False),
319
+ row=2, col=1
320
+ )
321
+
322
+ # Revenue impact
323
+ fig.add_trace(
324
+ go.Scatter(x=scope_data["threshold"], y=scope_data["revenue_impact"],
325
+ mode="lines+markers", name=f"{scope_name}",
326
+ line=dict(color=colors[scope_name]), legendgroup=scope_name, showlegend=False),
327
+ row=2, col=2
328
+ )
329
+
330
+ fig.update_xaxes(title_text="Threshold (minutes)")
331
+ fig.update_yaxes(title_text="Count", row=1, col=1)
332
+ fig.update_yaxes(title_text="Count", row=1, col=2)
333
+ fig.update_yaxes(title_text="Percentage (%)", row=2, col=1)
334
+ fig.update_yaxes(title_text="Percentage (%)", row=2, col=2)
335
+
336
+ fig.update_layout(height=600, showlegend=True)
337
+ st.plotly_chart(fig, use_container_width=True)
338
+
339
+ # Business recommendations
340
+ st.markdown('<div class="section-header">πŸ’‘ Business Recommendations</div>', unsafe_allow_html=True)
341
+
342
+ # Find optimal threshold for each scope
343
+ all_cars_data = comparison_df[comparison_df["scope"] == "All Cars"]
344
+ connect_data = comparison_df[comparison_df["scope"] == "Connect Only"]
345
+
346
+ # Simple optimization: maximize problems solved while keeping blocked rate reasonable
347
+ def find_optimal_threshold(data, max_blocked_rate=15):
348
+ """Find optimal threshold balancing problem solving and availability"""
349
+ viable = data[data["blocked_percentage"] <= max_blocked_rate]
350
+ if len(viable) == 0:
351
+ viable = data
352
+ return viable.loc[viable["problems_solved"].idxmax()]
353
+
354
+ optimal_all = find_optimal_threshold(all_cars_data)
355
+ optimal_connect = find_optimal_threshold(connect_data)
356
+
357
+ col1, col2 = st.columns(2)
358
+
359
+ with col1:
360
+ st.markdown("### 🎯 Recommended Settings - All Cars")
361
+ st.markdown(f"""
362
+ <div class="insight-box">
363
+ <strong>Optimal Threshold:</strong> {optimal_all['threshold']} minutes<br>
364
+ <strong>Problems Solved:</strong> {optimal_all['problems_solved']:,.0f}<br>
365
+ <strong>Blocked Rentals:</strong> {optimal_all['blocked_rentals']:,.0f} ({optimal_all['blocked_percentage']:.1f}%)<br>
366
+ <strong>Efficiency:</strong> {optimal_all['problem_solve_rate']:.1f}%
367
+ </div>
368
+ """, unsafe_allow_html=True)
369
+
370
+ with col2:
371
+ st.markdown("### πŸ”Œ Recommended Settings - Connect Only")
372
+ st.markdown(f"""
373
+ <div class="insight-box">
374
+ <strong>Optimal Threshold:</strong> {optimal_connect['threshold']} minutes<br>
375
+ <strong>Problems Solved:</strong> {optimal_connect['problems_solved']:,.0f}<br>
376
+ <strong>Blocked Rentals:</strong> {optimal_connect['blocked_rentals']:,.0f} ({optimal_connect['blocked_percentage']:.1f}%)<br>
377
+ <strong>Efficiency:</strong> {optimal_connect['problem_solve_rate']:.1f}%
378
+ </div>
379
+ """, unsafe_allow_html=True)
380
+
381
+ # Final recommendation
382
+ if optimal_connect['problem_solve_rate'] > optimal_all['problem_solve_rate'] and optimal_connect['blocked_percentage'] < optimal_all['blocked_percentage']:
383
+ recommendation = "Connect Only"
384
+ rec_data = optimal_connect
385
+ else:
386
+ recommendation = "All Cars"
387
+ rec_data = optimal_all
388
+
389
+ st.markdown("### πŸ† Final Recommendation")
390
+ st.success(f"""
391
+ **Recommended Strategy:** Apply {rec_data['threshold']:.0f}-minute minimum delay to **{recommendation}**
392
+
393
+ **Key Benefits:**
394
+ - Solves {rec_data['problems_solved']:.0f} problematic cases
395
+ - Blocks only {rec_data['blocked_percentage']:.1f}% of consecutive rentals
396
+ - Achieves {rec_data['problem_solve_rate']:.1f}% efficiency in problem solving
397
+ - Minimizes impact on rental availability and revenue
398
  """)
399
 
400
+ # Detailed analysis section
401
+ with st.expander("πŸ“‹ Detailed Analysis & Methodology"):
402
+ st.markdown("""
403
+ ### Analysis Methodology
404
+
405
+ **Problem Definition:**
406
+ - A "problematic case" occurs when a driver returns late AND the delay exceeds the gap to the next rental
407
+ - This causes the next driver to wait or potentially cancel their reservation
408
+
409
+ **Key Metrics:**
410
+ - **Blocked Rentals:** Consecutive rentals with gap < threshold that would be prevented
411
+ - **Problems Solved:** Current problematic cases that would be prevented by the threshold
412
+ - **Efficiency:** Percentage of blocked rentals that actually solve a problem
413
+ - **Revenue Impact:** Approximate percentage of rentals affected (proxy for revenue)
414
+
415
+ **Optimization Logic:**
416
+ - Maximize problems solved while keeping blocked rate reasonable (≀15%)
417
+ - Balance customer satisfaction improvements vs. availability reduction
418
+ - Consider implementation scope (all cars vs. Connect only)
419
+
420
+ **Data Quality Notes:**
421
+ - Extreme delays (>12 hours) filtered out as likely data errors
422
+ - Only rentals with previous rentals considered for blocking analysis
423
+ - Revenue impact estimated based on rental volume (actual revenue data not available)
424
+ """)
425
+
426
+ # Footer
427
  st.markdown("---")
428
+ st.markdown("*Dashboard built for Getaround delay analysis and minimum threshold optimization*")