sony9316 commited on
Commit
c5bc2b2
·
verified ·
1 Parent(s): 030806e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +264 -180
app.py CHANGED
@@ -1,250 +1,334 @@
 
1
  import streamlit as st
2
  import pandas as pd
3
  import numpy as np
4
  from pathlib import Path
5
  import matplotlib.pyplot as plt
 
6
 
7
- # ---------- Page config ----------
8
- st.set_page_config(page_title="Getaround — Delay Buffer Decision", layout="wide")
 
9
 
10
- # ---------- Data loading ----------
11
  @st.cache_data(show_spinner=False)
12
  def load_data():
13
- # Try common locations (local / sandbox)
14
- candidates = [
15
- "get_around_delay_analysis.csv",
16
- "/mnt/data/get_around_delay_analysis.csv"
17
- ]
18
- path = next((p for p in candidates if Path(p).exists()), None)
19
- if path is None:
20
- st.error("CSV not found. Place 'get_around_delay_analysis.csv' next to the app or in /mnt/data.")
21
  st.stop()
22
 
23
- df = pd.read_csv(path)
24
- # Drop unnamed columns if any
25
  df = df.drop(columns=[c for c in df.columns if c.lower().startswith("unnamed")], errors="ignore")
26
 
27
- # Standard helper flags
28
  df["has_gap"] = df["time_delta_with_previous_rental_in_minutes"].notnull()
29
- df["has_delay"] = df["delay_at_checkout_in_minutes"].notnull()
30
-
31
- # Clean improbable huge early/late returns (+/- 12h hard cap) -> set to NaN
32
  clean = df["delay_at_checkout_in_minutes"].copy()
33
- clean = clean.where(clean.between(-720, 720))
34
  df["clean_delay"] = clean
35
 
36
- # For rows with a previous rental gap and known delay, compute:
37
- # overlap_problem: a late return exceeds the previous gap -> next renter is impacted
38
- # wait_for_next_minutes: how many minutes the next renter would wait (>=0)
39
- gap = df["time_delta_with_previous_rental_in_minutes"]
40
  cond = df["has_gap"] & df["clean_delay"].notnull()
41
  df["overlap_problem"] = False
42
  df.loc[cond, "overlap_problem"] = df.loc[cond, "clean_delay"] > df.loc[cond, "time_delta_with_previous_rental_in_minutes"]
43
  df["wait_for_next_minutes"] = 0.0
44
  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)
45
 
46
- # Try to find a revenue column if present
47
- revenue_cols = [c for c in df.columns if c.lower() in {"owner_revenue","rental_price","price","revenue","price_eur"}]
48
- df.attrs["revenue_col"] = revenue_cols[0] if revenue_cols else None
49
-
50
  return df
51
 
52
  df = load_data()
53
- REVENUE_COL = df.attrs.get("revenue_col", None)
54
 
55
- # ---------- Scope filter ----------
56
- def filter_scope(data: pd.DataFrame, scope: str) -> pd.DataFrame:
57
- if scope == "Connect only":
58
  return data[data["checkin_type"].str.lower() == "connect"]
59
- return data # All cars
60
-
61
- # ---------- Core metrics for a given threshold & scope ----------
62
- def metrics_for_threshold(data: pd.DataFrame, threshold_minutes: int):
63
- """
64
- Definitions (applied only on rows with a previous-rental gap):
65
- • blocked: time_delta_with_previous_rental_in_minutes < threshold
66
- -> This rental would be hidden from search to respect the buffer.
67
-
68
- • overlap_problem: clean_delay > gap
69
- -> Next renter is impacted (today, without buffer).
70
 
71
- solved_by_buffer: overlap_problem & blocked
72
- -> The buffer would have hidden this case, avoiding the next-renter wait.
73
-
74
- • wait_for_next_minutes: max(clean_delay - gap, 0)
75
- -> Actual minutes the next renter waits in impacted cases (diagnostic only).
76
-
77
- Revenue affected (if revenue column exists):
78
- share_of_revenue_blocked = sum(revenue for blocked rentals) / sum(revenue all rentals in scope)
79
- Otherwise we report share_of_rentals_blocked as proxy.
80
- """
81
  d = data.copy()
82
- mask_gap = d["has_gap"]
83
-
84
- d_gap = d[mask_gap].copy()
85
  if d_gap.empty:
86
  return dict(
87
- total_with_gap=0, blocked=0, blocked_rate=0.0,
88
- problems_today=0, overlap_rate=0.0,
89
- solved=0, solve_rate_given_blocked=0.0,
90
- avg_wait_minutes_among_problems=np.nan,
91
- revenue_blocked=0.0, revenue_total=0.0,
92
- revenue_share_blocked=np.nan
93
  )
 
 
94
 
95
- d_gap["blocked"] = d_gap["time_delta_with_previous_rental_in_minutes"] < threshold_minutes
96
- d_gap["solved_by_buffer"] = d_gap["overlap_problem"] & d_gap["blocked"]
97
-
98
- total_with_gap = len(d_gap)
99
  blocked = int(d_gap["blocked"].sum())
100
- blocked_rate = blocked / total_with_gap
101
-
102
  problems_today = int(d_gap["overlap_problem"].sum())
103
- overlap_rate = problems_today / total_with_gap
104
-
105
- solved = int(d_gap["solved_by_buffer"].sum())
106
- solve_rate_given_blocked = solved / blocked if blocked > 0 else 0.0
107
-
108
  avg_wait = d_gap.loc[d_gap["overlap_problem"], "wait_for_next_minutes"].mean()
109
 
110
- # Revenue (if available)
111
- if REVENUE_COL and REVENUE_COL in d.columns:
112
- # For safety, coerce to numeric
113
  rev = pd.to_numeric(d[REVENUE_COL], errors="coerce")
114
  rev_total = rev.sum(skipna=True)
115
  rev_blocked = pd.to_numeric(d_gap.loc[d_gap["blocked"], REVENUE_COL], errors="coerce").sum(skipna=True)
116
- rev_share_blocked = (rev_blocked / rev_total) if rev_total > 0 else np.nan
117
  else:
118
- rev_total = 0.0
119
- rev_blocked = 0.0
120
- rev_share_blocked = np.nan
121
 
122
  return dict(
123
- total_with_gap=total_with_gap,
124
  blocked=blocked,
125
  blocked_rate=blocked_rate,
126
  problems_today=problems_today,
127
  overlap_rate=overlap_rate,
128
  solved=solved,
129
- solve_rate_given_blocked=solve_rate_given_blocked,
130
- avg_wait_minutes_among_problems=avg_wait,
131
- revenue_blocked=rev_blocked,
132
- revenue_total=rev_total,
133
- revenue_share_blocked=rev_share_blocked
134
  )
135
 
136
- # ---------- Sidebar controls ----------
137
- st.sidebar.header("Controls")
138
- threshold = st.sidebar.slider("Minimum delay (buffer) in minutes", min_value=15, max_value=180, step=15, value=90)
139
- scope = st.sidebar.radio("Scope", ["All cars", "Connect only"], horizontal=True)
 
 
140
 
 
 
 
 
 
141
  df_scope = filter_scope(df, scope)
142
 
143
- # ---------- Overview KPIs ----------
144
- st.title("Getaround Delay Buffer Decision Support")
 
 
145
 
146
- with st.expander("How we compute each metric (click to expand)"):
147
- st.markdown("""
148
- - **Has previous gap:** `has_gap = time_delta_with_previous_rental_in_minutes not null`
149
- - **Overlap problem (today):** `clean_delay > gap`
150
- - **Next-driver wait (minutes):** `max(clean_delay - gap, 0)`
151
- - **Blocked by buffer:** `gap < threshold`
152
- - **Solved by buffer:** `overlap_problem AND blocked` (hidden rental avoids the clash)
153
- - **Blocked rate:** `blocked / rentals_with_gap`
154
- - **Overlap rate:** `overlap_problems / rentals_with_gap`
155
- - **Solve rate (given blocked):** `solved / blocked`
156
- - **Share of revenue affected:** `sum(revenue for blocked rentals) / sum(revenue all rentals)` *(shown only if a revenue column exists)*
157
- """)
158
 
159
- colA, colB, colC, colD = st.columns(4)
160
- with colA:
161
- total_rentals = len(df_scope)
162
- st.metric("Total rentals (scope)", f"{total_rentals:,}")
163
- with colB:
164
- with_gap = int(df_scope["has_gap"].sum())
165
- st.metric("Rentals with previous gap", f"{with_gap:,}")
166
- with colC:
167
- overlap_now = int(df_scope["overlap_problem"].sum())
168
- st.metric("Overlap problems (today)", f"{overlap_now:,}")
169
- with colD:
170
- avg_wait_now = df_scope.loc[df_scope["overlap_problem"], "wait_for_next_minutes"].mean()
171
- st.metric("Avg wait (impacted, min)", f"{(avg_wait_now or 0):.1f}")
172
-
173
- # ---------- Threshold & Scope Impact ----------
174
- m = metrics_for_threshold(df_scope, threshold)
175
-
176
- st.subheader("Impact at selected buffer & scope")
177
- k1, k2, k3, k4, k5 = st.columns(5)
178
- k1.metric("Rentals blocked", f"{m['blocked']:,}")
179
- k2.metric("Blocked rate (of with-gap)", f"{m['blocked_rate']:.1%}")
180
- k3.metric("Problems today", f"{m['problems_today']:,}")
181
- k4.metric("Solved by buffer", f"{m['solved']:,}")
182
- k5.metric("Solve rate (given blocked)", f"{m['solve_rate_given_blocked']:.1%}")
183
-
184
- k6, k7 = st.columns(2)
185
- k6.metric("Avg wait among problems (min)", f"{(m['avg_wait_minutes_among_problems'] or 0):.1f}")
186
 
187
  if REVENUE_COL:
188
- share_txt = f"{(m['revenue_share_blocked'] or 0):.1%}"
189
- k7.metric("Share of owner revenue affected", share_txt)
190
  else:
191
- k7.metric("Share of revenue affected", "N/A")
192
- st.info("No revenue column detected; reporting revenue share is not possible. We use **rental share** as a proxy in charts below.")
193
 
194
- # ---------- Threshold Sweep (for decision curves) ----------
195
- st.subheader("Decision curves by threshold")
196
- thresholds = list(range(15, 181, 15))
197
- rows = []
198
- for t in thresholds:
199
- mt = metrics_for_threshold(df_scope, t)
200
- proxy_rev_share = mt["blocked_rate"] # proxy if no revenue
201
- rows.append({
202
- "Threshold (min)": t,
203
- "Blocked rate (of with-gap)": mt["blocked_rate"],
204
- "Solved (count)": mt["solved"],
205
- "Solve rate (given blocked)": mt["solve_rate_given_blocked"],
206
- "Overlap problems today": mt["problems_today"],
207
- "Revenue share blocked": mt["revenue_share_blocked"] if REVENUE_COL else np.nan,
208
- "Proxy: rental share blocked": proxy_rev_share if not REVENUE_COL else np.nan
209
- })
210
- curve_df = pd.DataFrame(rows)
211
 
212
  c1, c2 = st.columns(2)
213
  with c1:
214
- st.markdown("**Blocked vs Solved**")
215
- fig1, ax1 = plt.subplots(figsize=(5,4))
216
- ax1.plot(curve_df["Threshold (min)"], curve_df["Blocked rate (of with-gap)"], marker="o", label="Blocked rate")
217
- ax1.plot(curve_df["Threshold (min)"], curve_df["Solve rate (given blocked)"], marker="o", label="Solve rate (given blocked)")
218
- ax1.set_xlabel("Threshold (minutes)")
219
- ax1.set_ylabel("Rate")
220
- ax1.legend()
221
- st.pyplot(fig1, clear_figure=True)
222
 
223
  with c2:
224
- st.markdown("**Problems solved (count)**")
225
- fig2, ax2 = plt.subplots(figsize=(5,4))
226
- ax2.plot(curve_df["Threshold (min)"], curve_df["Solved (count)"], marker="o")
227
- ax2.set_xlabel("Threshold (minutes)")
228
- ax2.set_ylabel("Solved (count)")
229
- st.pyplot(fig2, clear_figure=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
 
231
  st.markdown("---")
232
 
233
- # ---------- Revenue share (if available) ----------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  if REVENUE_COL:
235
- st.subheader("Share of owner revenue blocked (by threshold)")
236
- fig3, ax3 = plt.subplots(figsize=(6,4))
237
- ax3.plot(curve_df["Threshold (min)"], curve_df["Revenue share blocked"], marker="o")
238
- ax3.set_xlabel("Threshold (minutes)")
239
- ax3.set_ylabel("Revenue share blocked")
240
- st.pyplot(fig3, clear_figure=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  else:
242
- st.subheader("Proxy: share of rentals blocked (by threshold)")
243
- fig4, ax4 = plt.subplots(figsize=(6,4))
244
- ax4.plot(curve_df["Threshold (min)"], curve_df["Proxy: rental share blocked"], marker="o")
245
- ax4.set_xlabel("Threshold (minutes)")
246
- ax4.set_ylabel("Share of rentals blocked (proxy)")
247
- st.pyplot(fig4, clear_figure=True)
248
-
249
- # ---------- Footer ----------
250
- st.caption("All metrics are computed from raw gaps and delays; no heuristic business assumptions.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
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.")