sony9316 commited on
Commit
e6aaa19
Β·
verified Β·
1 Parent(s): b5e0d07

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +246 -789
app.py CHANGED
@@ -12,7 +12,7 @@ warnings.filterwarnings('ignore')
12
 
13
  # ========== PAGE CONFIGURATION ==========
14
  st.set_page_config(
15
- page_title="Getaround Analysis Dashboard",
16
  page_icon="πŸš—",
17
  layout="wide",
18
  initial_sidebar_state="expanded"
@@ -60,11 +60,10 @@ st.markdown("""
60
  </style>
61
  """, unsafe_allow_html=True)
62
 
63
- # ========== DATA LOADING FUNCTION ==========
64
  @st.cache_data(show_spinner=False)
65
  def load_data():
66
  """Load and preprocess the rental data"""
67
- # Try multiple possible file locations
68
  possible_paths = [
69
  "get_around_delay_analysis.xlsx",
70
  "get_around_delay_analysis.csv",
@@ -80,7 +79,6 @@ def load_data():
80
  df = pd.read_excel(path)
81
  else:
82
  df = pd.read_csv(path)
83
- st.sidebar.success(f"βœ… Data loaded from: {path}")
84
  break
85
  except Exception as e:
86
  continue
@@ -91,16 +89,14 @@ def load_data():
91
 
92
  # Clean the data
93
  df = df.drop(columns=[c for c in df.columns if c.lower().startswith("unnamed")], errors="ignore")
94
-
95
- # Create helper columns
96
  df["has_previous_rental"] = df["time_delta_with_previous_rental_in_minutes"].notnull()
97
- df["clean_delay"] = df["delay_at_checkout_in_minutes"].clip(-720, 720) # Cap extreme values
98
 
99
  return df
100
 
101
  # ========== ANALYSIS FUNCTIONS ==========
102
- def calculate_threshold_metrics(df, threshold_minutes, scope="all"):
103
- """Calculate metrics for threshold analysis"""
104
 
105
  # Filter by scope
106
  if scope == "connect":
@@ -108,7 +104,6 @@ def calculate_threshold_metrics(df, threshold_minutes, scope="all"):
108
  else:
109
  df_filtered = df.copy()
110
 
111
- # Only consider rentals with previous rentals
112
  df_with_prev = df_filtered[df_filtered["has_previous_rental"]].copy()
113
 
114
  if len(df_with_prev) == 0:
@@ -119,76 +114,36 @@ def calculate_threshold_metrics(df, threshold_minutes, scope="all"):
119
  "blocked_percentage": 0.0,
120
  "current_problems": 0,
121
  "problems_solved": 0,
122
- "problem_solve_rate": 0.0,
123
- "avg_wait_time": 0.0,
124
- "revenue_impact_percent": 0.0,
125
- "current_cancellations": 0,
126
- "cancellations_prevented": 0,
127
- "cancellation_rate": 0.0
128
  }
129
 
130
- # Join with previous rental data to get actual delay information
131
  prev_rental_data = df[["rental_id", "delay_at_checkout_in_minutes"]].rename(
132
  columns={"rental_id": "previous_ended_rental_id",
133
  "delay_at_checkout_in_minutes": "previous_delay"}
134
  )
135
 
136
- df_with_prev = df_with_prev.merge(
137
- prev_rental_data,
138
- on="previous_ended_rental_id",
139
- how="left"
140
- )
141
 
142
- # Calculate blocked rentals (gap < threshold)
143
  df_with_prev["would_be_blocked"] = df_with_prev["time_delta_with_previous_rental_in_minutes"] < threshold_minutes
144
-
145
- # Calculate current problems using ACTUAL previous rental delay
146
  df_with_prev["previous_delay_clean"] = df_with_prev["previous_delay"].clip(-720, 720)
147
  df_with_prev["causes_problem"] = (
148
  df_with_prev["previous_delay"].notnull() &
149
  (df_with_prev["previous_delay_clean"] > df_with_prev["time_delta_with_previous_rental_in_minutes"])
150
  )
151
-
152
- # Wait time calculation using actual previous delay
153
- df_with_prev["wait_time_next_driver"] = np.maximum(
154
- 0,
155
- df_with_prev["previous_delay_clean"] - df_with_prev["time_delta_with_previous_rental_in_minutes"]
156
- ).fillna(0)
157
-
158
- # Problems solved by blocking
159
  df_with_prev["problem_solved"] = df_with_prev["causes_problem"] & df_with_prev["would_be_blocked"]
160
 
161
- # Cancellation analysis - much more accurate now
162
- df_with_prev["is_cancelled"] = df_with_prev["state"] == "canceled"
163
-
164
- # Cancellations due to previous rental delays (using actual previous delay data)
165
- df_with_prev["cancelled_due_to_previous_delay"] = (
166
- df_with_prev["is_cancelled"] & df_with_prev["causes_problem"]
167
- )
168
-
169
- # Cancellations that would be prevented by threshold
170
- df_with_prev["cancellation_prevented"] = (
171
- df_with_prev["cancelled_due_to_previous_delay"] & df_with_prev["would_be_blocked"]
172
- )
173
-
174
- # Calculate final metrics
175
  total_rentals = len(df_filtered)
176
  rentals_with_previous = len(df_with_prev)
177
  blocked_rentals = int(df_with_prev["would_be_blocked"].sum())
178
  blocked_percentage = (blocked_rentals / rentals_with_previous) * 100 if rentals_with_previous > 0 else 0
179
  current_problems = int(df_with_prev["causes_problem"].sum())
180
  problems_solved = int(df_with_prev["problem_solved"].sum())
181
- problem_solve_rate = (problems_solved / blocked_rentals * 100) if blocked_rentals > 0 else 0
182
- avg_wait_time = df_with_prev[df_with_prev["causes_problem"]]["wait_time_next_driver"].mean()
183
- revenue_impact_percent = (blocked_rentals / total_rentals) * 100 if total_rentals > 0 else 0
184
-
185
- # Cancellation metrics - now much more accurate
186
- current_cancellations = int(df_with_prev["cancelled_due_to_previous_delay"].sum())
187
- cancellations_prevented = int(df_with_prev["cancellation_prevented"].sum())
188
-
189
- # Remaining cancellation rate after implementing threshold
190
- remaining_cancellations = current_cancellations - cancellations_prevented
191
- cancellation_rate = (remaining_cancellations / rentals_with_previous) * 100 if rentals_with_previous > 0 else 0
192
 
193
  return {
194
  "total_rentals": total_rentals,
@@ -197,419 +152,201 @@ def calculate_threshold_metrics(df, threshold_minutes, scope="all"):
197
  "blocked_percentage": blocked_percentage,
198
  "current_problems": current_problems,
199
  "problems_solved": problems_solved,
200
- "problem_solve_rate": problem_solve_rate,
201
- "avg_wait_time": avg_wait_time if not pd.isna(avg_wait_time) else 0,
202
- "revenue_impact_percent": revenue_impact_percent,
203
- "current_cancellations": current_cancellations,
204
- "cancellations_prevented": cancellations_prevented,
205
- "cancellation_rate": cancellation_rate
206
  }
207
 
208
- def create_threshold_sweep(df, thresholds, scope="all"):
209
- """Create threshold sweep analysis"""
210
  results = []
211
  for threshold in thresholds:
212
- metrics = calculate_threshold_metrics(df, threshold, scope)
213
  results.append({"threshold": threshold, **metrics})
214
  return pd.DataFrame(results)
215
 
216
- def find_optimal_threshold(sweep_df, max_blocked_rate=25):
217
- """Find optimal threshold using improved business logic"""
218
- # Only consider thresholds that actually solve problems (> 0 problems solved)
219
- viable = sweep_df[
220
- (sweep_df["problems_solved"] > 0) &
221
- (sweep_df["threshold"] >= 30) & # Minimum reasonable threshold
222
- (sweep_df["threshold"] <= 180) # Don't recommend unreasonably long thresholds
223
- ]
224
-
225
- if len(viable) == 0:
226
- # If no viable options, return a reasonable default
227
- return sweep_df[sweep_df["threshold"] == 90].iloc[0]
228
-
229
- # Calculate efficiency and other metrics
230
- viable["efficiency"] = viable["problems_solved"] / viable["blocked_rentals"].replace({0: np.nan})
231
- viable["problems_per_threshold"] = viable["problems_solved"] / viable["threshold"]
232
-
233
- # Find the "knee" of the problems solved curve - where gains start diminishing
234
- viable = viable.sort_values("threshold")
235
- viable["problems_marginal_gain"] = viable["problems_solved"].diff().fillna(0)
236
-
237
- # Look for thresholds that solve significant problems with reasonable efficiency
238
- # Priority: solve at least 70% of max problems solvable, with good efficiency
239
- max_problems = viable["problems_solved"].max()
240
- good_performers = viable[
241
- (viable["problems_solved"] >= max_problems * 0.7) &
242
- (viable["efficiency"] >= 20) & # At least 20% efficiency
243
- (viable["blocked_percentage"] <= max_blocked_rate)
244
- ]
245
-
246
- if len(good_performers) == 0:
247
- # Fallback: just find best efficiency among those solving good number of problems
248
- good_problems = viable[viable["problems_solved"] >= max_problems * 0.6]
249
- if len(good_problems) > 0:
250
- optimal = good_problems.sort_values(["efficiency", "problems_solved"], ascending=[False, False]).iloc[0]
251
- else:
252
- optimal = viable.sort_values(["problems_solved", "efficiency"], ascending=[False, False]).iloc[0]
253
- else:
254
- # Among good performers, prefer lower threshold with high efficiency
255
- optimal = good_performers.sort_values(["efficiency", "threshold"], ascending=[False, True]).iloc[0]
256
-
257
- return optimal
258
-
259
  # ========== LOAD DATA ==========
260
  df = load_data()
261
 
262
- # ========== SIDEBAR NAVIGATION ==========
263
- st.markdown('<h1 class="main-header">πŸš— Getaround Analysis Dashboard</h1>', unsafe_allow_html=True)
264
 
 
265
  with st.sidebar:
266
- st.markdown("## πŸŽ›οΈ Navigation")
 
267
  selected = option_menu(
268
- "Analysis Sections",
269
- ["πŸ“Š Data Overview", "πŸ• Threshold Analysis", "🎯 Scope Analysis"],
270
- icons=["bar-chart", "clock", "target"],
271
  menu_icon="cast",
272
  default_index=0,
273
  )
274
 
 
 
 
 
 
 
 
275
  st.markdown("---")
276
  st.markdown("### πŸ“ˆ Dataset Info")
277
  st.info(f"""
278
  **Total Rentals:** {len(df):,}
279
- **With Previous Rental:** {df['has_previous_rental'].sum():,} *(rentals that had another rental on the same car before)*
280
  **Connect Rentals:** {len(df[df['checkin_type'].str.lower() == 'connect']):,}
281
- **Mobile Rentals:** {len(df[df['checkin_type'].str.lower() == 'mobile']):,}
 
282
  """)
283
 
284
- # ========== PAGE 1: DATA OVERVIEW ==========
285
- if selected == "πŸ“Š Data Overview":
286
- st.title("πŸ“Š Dataset Overview & Exploratory Analysis")
287
 
288
- st.markdown('<div class="section-header">πŸ“ˆ Dataset Summary</div>', unsafe_allow_html=True)
 
289
 
290
- # Basic statistics
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  col1, col2, col3, col4 = st.columns(4)
292
  with col1:
293
- st.metric("Total Rentals", f"{len(df):,}")
 
294
  with col2:
295
- st.metric("Connect Rentals", f"{len(df[df['checkin_type'].str.lower() == 'connect']):,}")
 
296
  with col3:
297
- st.metric("Cancelled Rentals", f"{len(df[df['state'] == 'canceled']):,}",
298
- help="Total cancelled rentals in dataset")
299
  with col4:
300
- st.metric("With Previous Rental", f"{df['has_previous_rental'].sum():,}",
301
- help="Rentals that had another rental on the same car before (within 12 hours)")
302
-
303
- # Checkin type distribution
304
- st.markdown('<div class="section-header">πŸš— Rental Type Distribution</div>', unsafe_allow_html=True)
305
-
306
- col1, col2 = st.columns(2)
307
-
308
- with col1:
309
- checkin_counts = df["checkin_type"].value_counts()
310
- fig_checkin = px.pie(
311
- values=checkin_counts.values,
312
- names=checkin_counts.index,
313
- title="Distribution by Checkin Type",
314
- color_discrete_sequence=px.colors.qualitative.Set3
315
- )
316
- st.plotly_chart(fig_checkin, use_container_width=True)
317
 
318
- with col2:
319
- state_counts = df["state"].value_counts()
320
- fig_state = px.pie(
321
- values=state_counts.values,
322
- names=state_counts.index,
323
- title="Distribution by Rental State",
324
- color_discrete_sequence=px.colors.qualitative.Pastel
325
- )
326
- st.plotly_chart(fig_state, use_container_width=True)
327
 
328
- # Delay analysis
329
- st.markdown('<div class="section-header">⏰ Delay Analysis</div>', unsafe_allow_html=True)
330
 
331
  col1, col2 = st.columns(2)
332
 
333
  with col1:
334
- # Delay status distribution (excluding missing data)
335
  df_with_delay_data = df[df["delay_at_checkout_in_minutes"].notnull()].copy()
336
  df_with_delay_data["delay_status"] = df_with_delay_data["delay_at_checkout_in_minutes"].apply(
337
  lambda x: "Early Return" if x < 0 else "On Time" if x == 0 else "Late Return"
338
  )
339
 
340
  delay_counts = df_with_delay_data["delay_status"].value_counts()
341
- fig_delay_status = px.pie(
342
  values=delay_counts.values,
343
  names=delay_counts.index,
344
- title="Return Status Distribution (Data Available Only)",
345
- color_discrete_sequence=px.colors.qualitative.Bold
346
- )
347
- fig_delay_status.update_layout(
348
- annotations=[dict(text=f"Based on {len(df_with_delay_data):,} rentals with delay data",
349
- x=0.5, y=-0.1, xref="paper", yref="paper",
350
- showarrow=False, font=dict(size=10))]
351
  )
352
- st.plotly_chart(fig_delay_status, use_container_width=True)
353
 
354
  with col2:
355
- # Delay distribution histogram (starting from 0, showing early returns clearly)
356
- delay_data = df[df["delay_at_checkout_in_minutes"].notnull()]
357
- delay_filtered = delay_data[delay_data["delay_at_checkout_in_minutes"].between(-120, 300)]
358
-
359
- fig_delay_hist = px.histogram(
360
  delay_filtered,
361
  x="delay_at_checkout_in_minutes",
362
- nbins=50,
363
- title="Checkout Delay Distribution",
364
- labels={"delay_at_checkout_in_minutes": "Minutes (Negative = Early Return)", "count": "Number of Rentals"}
365
  )
366
- fig_delay_hist.add_vline(x=0, line_dash="dash", line_color="red", annotation_text="On Time")
367
- fig_delay_hist.add_annotation(x=-60, y=delay_filtered.shape[0]*0.1, text="Early Returns",
368
- showarrow=True, arrowhead=2, arrowcolor="blue")
369
- fig_delay_hist.add_annotation(x=120, y=delay_filtered.shape[0]*0.1, text="Late Returns",
370
- showarrow=True, arrowhead=2, arrowcolor="red")
371
- st.plotly_chart(fig_delay_hist, use_container_width=True)
372
 
373
  # Gap analysis
374
- st.markdown('<div class="section-header">πŸ“ Gap Between Rentals Analysis</div>', unsafe_allow_html=True)
375
 
376
- col1, col2 = st.columns(2)
 
377
 
378
- with col1:
379
- # Gap distribution
380
- gap_data = df[df["has_previous_rental"]]
381
- gap_filtered = gap_data[gap_data["time_delta_with_previous_rental_in_minutes"].between(0, 720)]
382
-
383
- fig_gap_hist = px.histogram(
384
- gap_filtered,
385
- x="time_delta_with_previous_rental_in_minutes",
386
- nbins=40,
387
- title="Gap Distribution (0 to 720 minutes)",
388
- labels={"time_delta_with_previous_rental_in_minutes": "Gap (minutes)", "count": "Number of Rentals"}
389
- )
390
- st.plotly_chart(fig_gap_hist, use_container_width=True)
391
-
392
- with col2:
393
- # Gap by checkin type
394
- fig_gap_box = px.box(
395
- gap_filtered,
396
- x="checkin_type",
397
- y="time_delta_with_previous_rental_in_minutes",
398
- title="Gap Distribution by Checkin Type",
399
- labels={"time_delta_with_previous_rental_in_minutes": "Gap (minutes)"}
400
- )
401
- st.plotly_chart(fig_gap_box, use_container_width=True)
402
 
403
  # Cancellation analysis
404
- st.markdown('<div class="section-header">❌ Cancellation Analysis</div>', unsafe_allow_html=True)
405
 
406
  col1, col2 = st.columns(2)
407
 
408
  with col1:
409
- st.markdown("### Cancellation by Type")
 
 
410
 
 
 
 
 
 
 
 
 
411
  cancellation_by_type = df.groupby(['checkin_type', 'state']).size().unstack(fill_value=0)
412
  if 'canceled' in cancellation_by_type.columns:
413
  cancel_rates = (cancellation_by_type['canceled'] / cancellation_by_type.sum(axis=1) * 100).round(1)
414
-
415
- fig_cancel_type = px.bar(
416
  x=cancel_rates.index,
417
  y=cancel_rates.values,
418
- title="Cancellation Rate by Checkin Type",
419
  labels={"x": "Checkin Type", "y": "Cancellation Rate (%)"}
420
  )
421
- st.plotly_chart(fig_cancel_type, use_container_width=True)
422
- else:
423
- st.info("No cancellations found in dataset")
424
-
425
- with col2:
426
- st.markdown("### Delay-Related Cancellations")
427
-
428
- # Calculate accurate delay-related cancellations using previous rental data
429
- df_analysis = df[df["has_previous_rental"]].copy()
430
-
431
- # Join with previous rental delay data
432
- prev_rental_data = df[["rental_id", "delay_at_checkout_in_minutes"]].rename(
433
- columns={"rental_id": "previous_ended_rental_id",
434
- "delay_at_checkout_in_minutes": "previous_delay"}
435
- )
436
-
437
- df_analysis = df_analysis.merge(
438
- prev_rental_data,
439
- on="previous_ended_rental_id",
440
- how="left"
441
- )
442
-
443
- # Calculate problems using actual previous rental delay
444
- df_analysis["previous_delay_clean"] = df_analysis["previous_delay"].clip(-720, 720)
445
- df_analysis["causes_problem"] = (
446
- df_analysis["previous_delay"].notnull() &
447
- (df_analysis["previous_delay_clean"] > df_analysis["time_delta_with_previous_rental_in_minutes"])
448
- )
449
-
450
- # Cancellations due to previous delay
451
- df_analysis["cancelled_due_to_previous_delay"] = (
452
- (df_analysis["state"] == "canceled") & df_analysis["causes_problem"]
453
- )
454
-
455
- delay_related_cancels = df_analysis["cancelled_due_to_previous_delay"].sum()
456
- total_cancels_with_prev = (df_analysis["state"] == "canceled").sum()
457
- total_cancels_all = (df["state"] == "canceled").sum()
458
-
459
- col2_1, col2_2 = st.columns(2)
460
- with col2_1:
461
- st.metric("Total Cancellations (All)", f"{total_cancels_all:,}",
462
- help="All cancelled rentals in the dataset")
463
- with col2_2:
464
- st.metric("Due to Previous Delay", f"{delay_related_cancels:,}",
465
- help="Cancelled rentals where the previous rental on same car was late")
466
-
467
- if total_cancels_all > 0:
468
- delay_cancel_rate = (delay_related_cancels / total_cancels_all * 100)
469
- st.metric("% of All Cancellations Due to Previous Delays", f"{delay_cancel_rate:.1f}%")
470
-
471
- # Additional context
472
- st.info(f"Note: {total_cancels_with_prev:,} cancellations had previous rentals (analyzed for delay impact)")
473
 
474
-
475
- # Problem cases analysis (including cancellations)
476
- st.markdown('<div class="section-header">🚨 Current Problem Cases Analysis</div>', unsafe_allow_html=True)
477
 
478
  st.markdown("""
479
  <div class="insight-box">
480
- <strong>What is a "Problem Case"?</strong><br>
481
- A problem occurs when the PREVIOUS rental on the same car returned late AND this delay exceeds the planned gap to the current rental.
482
- This forces the next customer to either wait beyond their scheduled pickup time or cancel their rental.
483
- <br><br>
484
- <strong>Accurate Formula:</strong> Problem = (previous_rental_delay > gap_to_current_rental) AND (previous_rental_delay > 0)<br>
485
- <strong>Data Linking:</strong> Uses previous_ended_rental_id to get actual delay from the previous rental.<br>
486
- <strong>Cancellation Impact:</strong> Some of these problems result in cancellations when waiting becomes unacceptable.
487
  </div>
488
  """, unsafe_allow_html=True)
489
 
490
- # Calculate problem cases using proper linking to previous rental delays
491
- df_problems = df[df["has_previous_rental"]].copy()
492
-
493
- # Join with previous rental delay data
494
- prev_rental_data = df[["rental_id", "delay_at_checkout_in_minutes"]].rename(
495
- columns={"rental_id": "previous_ended_rental_id",
496
- "delay_at_checkout_in_minutes": "previous_delay"}
497
- )
498
-
499
- df_problems = df_problems.merge(
500
- prev_rental_data,
501
- on="previous_ended_rental_id",
502
- how="left"
503
- )
504
-
505
- # Calculate problems using actual previous rental delay
506
- df_problems["previous_delay_clean"] = df_problems["previous_delay"].clip(-720, 720)
507
- df_problems["causes_problem"] = (
508
- df_problems["previous_delay"].notnull() &
509
- (df_problems["previous_delay_clean"] > df_problems["time_delta_with_previous_rental_in_minutes"])
510
- )
511
- df_problems["wait_time"] = np.maximum(
512
- 0,
513
- df_problems["previous_delay_clean"] - df_problems["time_delta_with_previous_rental_in_minutes"]
514
- ).fillna(0)
515
 
516
- problem_cases = df_problems[df_problems["causes_problem"]]
517
 
518
  col1, col2, col3, col4 = st.columns(4)
519
- with col1:
520
- st.metric("Total Problem Cases", f"{len(problem_cases):,}",
521
- help="Number of rentals where late return affected the next customer")
522
- with col2:
523
- problem_rate = (len(problem_cases) / len(df_problems)) * 100 if len(df_problems) > 0 else 0
524
- st.metric("Problem Rate", f"{problem_rate:.1f}%",
525
- help="Percentage of consecutive rentals that create waiting problems")
526
- with col3:
527
- avg_wait = problem_cases["wait_time"].mean() if len(problem_cases) > 0 else 0
528
- st.metric("Avg Wait Time", f"{avg_wait:.1f} min",
529
- help="Average extra wait time for affected next customers (checkout_delay - gap)")
530
- with col4:
531
- # Add cancellation metric
532
- delay_cancels = problem_cases[problem_cases["state"] == "canceled"]
533
- st.metric("Resulting Cancellations", f"{len(delay_cancels):,}",
534
- help="Problem cases that resulted in cancellations")
535
-
536
- if len(problem_cases) > 0:
537
- col1, col2 = st.columns(2)
538
-
539
- with col1:
540
- # Wait time distribution
541
- fig_wait = px.histogram(
542
- problem_cases[problem_cases["wait_time"] > 0],
543
- x="wait_time",
544
- nbins=30,
545
- title="Wait Time Distribution for Problem Cases",
546
- labels={"wait_time": "Wait Time (minutes)", "count": "Number of Cases"}
547
- )
548
- st.plotly_chart(fig_wait, use_container_width=True)
549
-
550
- with col2:
551
- # Problems by checkin type
552
- problem_by_type = problem_cases["checkin_type"].value_counts()
553
- fig_problems_type = px.bar(
554
- x=problem_by_type.index,
555
- y=problem_by_type.values,
556
- title="Problem Cases by Checkin Type",
557
- labels={"x": "Checkin Type", "y": "Number of Problems"}
558
- )
559
- st.plotly_chart(fig_problems_type, use_container_width=True)
560
-
561
- # Raw data sample
562
- with st.expander("πŸ“‹ Raw Data Sample"):
563
- st.markdown("### First 100 rows of the dataset:")
564
- st.dataframe(df.head(100), use_container_width=True)
565
-
566
- st.markdown("### Dataset Info:")
567
- st.text("Dataset shape: " + str(df.shape))
568
- st.text("Columns: " + str(list(df.columns)))
569
-
570
- # ========== PAGE 2: THRESHOLD ANALYSIS ==========
571
- elif selected == "πŸ• Threshold Analysis":
572
- st.title("⏰ Threshold Decision: How Long Should the Minimum Delay Be?")
573
-
574
- st.markdown("""
575
- <div class="insight-box">
576
- <strong>Goal:</strong> Find the optimal minimum delay threshold that balances solving problematic late returns
577
- with maintaining rental availability. A higher threshold solves more problems but blocks more rentals.
578
- </div>
579
- """, unsafe_allow_html=True)
580
-
581
- # Controls
582
- col1, col2 = st.columns(2)
583
- with col1:
584
- threshold = st.slider("πŸ• Threshold (minutes)", 0, 300, 60, step=15,
585
- help="Minimum time gap required between consecutive rentals")
586
- with col2:
587
- scope_for_threshold = st.selectbox("πŸš— Scope for Analysis", ["all", "connect"],
588
- format_func=lambda x: "All Cars" if x == "all" else "Connect Cars Only")
589
-
590
- st.markdown("""
591
- <div class="insight-box">
592
- <strong>How the Metrics Work:</strong><br>
593
- β€’ <strong>Blocked Rentals:</strong> Number of current consecutive rentals where gap < threshold (these would be prevented)<br>
594
- β€’ <strong>Blocked Rate:</strong> % of consecutive rentals that would be blocked = blocked_rentals / rentals_with_previous<br>
595
- β€’ <strong>Problems Solved:</strong> Current problem cases that would be prevented = problems that occur when gap < threshold<br>
596
- β€’ <strong>Solve Efficiency:</strong> % of blocked rentals that actually solve a problem = problems_solved / blocked_rentals<br>
597
- β€’ <strong>Cancellations Prevented:</strong> Delay-related cancellations that would be avoided with this threshold
598
- </div>
599
- """, unsafe_allow_html=True)
600
-
601
- # Current metrics at selected threshold
602
- current_metrics = calculate_threshold_metrics(df, threshold, scope_for_threshold)
603
-
604
- st.markdown('<div class="section-header">πŸ“Š Current Impact at Selected Threshold</div>', unsafe_allow_html=True)
605
-
606
- col1, col2, col3, col4, col5 = st.columns(5)
607
  with col1:
608
  st.markdown(f"""
609
  <div class="metric-card">
610
  <h3 style="color: #e74c3c;">{current_metrics['blocked_rentals']:,}</h3>
611
  <p>Blocked Rentals</p>
612
- <small>Consecutive rentals with gap < {threshold} min</small>
613
  </div>
614
  """, unsafe_allow_html=True)
615
 
@@ -618,7 +355,7 @@ elif selected == "πŸ• Threshold Analysis":
618
  <div class="metric-card">
619
  <h3 style="color: #f39c12;">{current_metrics['blocked_percentage']:.1f}%</h3>
620
  <p>Blocked Rate</p>
621
- <small>{current_metrics['blocked_rentals']:,} / {current_metrics['rentals_with_previous']:,} consecutive rentals</small>
622
  </div>
623
  """, unsafe_allow_html=True)
624
 
@@ -627,465 +364,185 @@ elif selected == "πŸ• Threshold Analysis":
627
  <div class="metric-card">
628
  <h3 style="color: #27ae60;">{current_metrics['problems_solved']:,}</h3>
629
  <p>Problems Solved</p>
630
- <small>Current problems prevented by this threshold</small>
631
  </div>
632
  """, unsafe_allow_html=True)
633
 
634
  with col4:
635
  st.markdown(f"""
636
  <div class="metric-card">
637
- <h3 style="color: #3498db;">{current_metrics['problem_solve_rate']:.1f}%</h3>
638
- <p>Solve Efficiency</p>
639
- <small>{current_metrics['problems_solved']:,} / {current_metrics['blocked_rentals']:,} blocked rentals solve problems</small>
640
  </div>
641
  """, unsafe_allow_html=True)
642
 
643
- with col5:
644
- st.markdown(f"""
645
- <div class="metric-card">
646
- <h3 style="color: #9b59b6;">{current_metrics['cancellations_prevented']:,}</h3>
647
- <p>Cancellations Prevented</p>
648
- <small>Delay-related cancellations avoided</small>
649
- </div>
650
- """, unsafe_allow_html=True)
651
-
652
- # Current situation analysis
653
- st.markdown('<div class="section-header">πŸ” Current Problem Analysis</div>', unsafe_allow_html=True)
654
-
655
- col1, col2 = st.columns(2)
656
-
657
- with col1:
658
- st.markdown("### Late Return Impact")
659
-
660
- # Analysis of current problems
661
- df_analysis = df[df["has_previous_rental"]].copy()
662
-
663
- # Join with previous rental delay data
664
- prev_rental_data = df[["rental_id", "delay_at_checkout_in_minutes"]].rename(
665
- columns={"rental_id": "previous_ended_rental_id",
666
- "delay_at_checkout_in_minutes": "previous_delay"}
667
- )
668
-
669
- df_analysis = df_analysis.merge(
670
- prev_rental_data,
671
- on="previous_ended_rental_id",
672
- how="left"
673
- )
674
-
675
- # Calculate problems using actual previous rental delay
676
- df_analysis["previous_delay_clean"] = df_analysis["previous_delay"].clip(-720, 720)
677
- df_analysis["causes_problem"] = (
678
- df_analysis["previous_delay"].notnull() &
679
- (df_analysis["previous_delay_clean"] > df_analysis["time_delta_with_previous_rental_in_minutes"])
680
- )
681
- df_analysis["wait_time"] = np.maximum(
682
- 0,
683
- df_analysis["previous_delay_clean"] - df_analysis["time_delta_with_previous_rental_in_minutes"]
684
- ).fillna(0)
685
-
686
- problematic_cases = df_analysis[df_analysis["causes_problem"]]
687
- problem_rate = (len(problematic_cases) / len(df_analysis)) * 100 if len(df_analysis) > 0 else 0
688
- avg_wait = problematic_cases["wait_time"].mean() if len(problematic_cases) > 0 else 0
689
-
690
- st.metric("Current Problems", f"{len(problematic_cases):,} ({problem_rate:.1f}%)")
691
- st.metric("Average Wait Time", f"{avg_wait:.1f} min")
692
-
693
- with col2:
694
- st.markdown("### Wait Time Distribution")
695
-
696
- if len(problematic_cases) > 0:
697
- fig_wait = px.histogram(
698
- problematic_cases[problematic_cases["wait_time"] > 0],
699
- x="wait_time",
700
- nbins=30,
701
- title="Distribution of Wait Times (Current Problems)",
702
- labels={"wait_time": "Wait Time (minutes)", "count": "Number of Cases"}
703
- )
704
- fig_wait.update_layout(height=300)
705
- st.plotly_chart(fig_wait, use_container_width=True)
706
- else:
707
- st.info("No problematic cases found in current data")
708
-
709
- # Threshold sweep analysis
710
- st.markdown('<div class="section-header">πŸ“ˆ Threshold Impact Analysis</div>', unsafe_allow_html=True)
711
 
712
  thresholds = list(range(0, 301, 30))
713
- sweep_df = create_threshold_sweep(df, thresholds, scope_for_threshold)
714
 
715
- # Create comprehensive threshold analysis chart
716
  fig = make_subplots(
717
- rows=2, cols=3,
718
- subplot_titles=(
719
- "Blocked Rentals vs Threshold",
720
- "Problems Solved vs Threshold",
721
- "Cancellations Prevented vs Threshold",
722
- "Efficiency (Solve Rate) vs Threshold",
723
- "Revenue Impact vs Threshold",
724
- "Remaining Cancellation Rate vs Threshold"
725
- )
726
- )
727
-
728
- # Blocked rentals
729
- fig.add_trace(
730
- go.Scatter(x=sweep_df["threshold"], y=sweep_df["blocked_rentals"],
731
- mode="lines+markers", name="Blocked Rentals",
732
- line=dict(color="#e74c3c")),
733
- row=1, col=1
734
- )
735
-
736
- # Problems solved
737
- fig.add_trace(
738
- go.Scatter(x=sweep_df["threshold"], y=sweep_df["problems_solved"],
739
- mode="lines+markers", name="Problems Solved",
740
- line=dict(color="#27ae60"), showlegend=False),
741
- row=1, col=2
742
  )
743
 
744
- # Cancellations prevented
745
  fig.add_trace(
746
- go.Scatter(x=sweep_df["threshold"], y=sweep_df["cancellations_prevented"],
747
- mode="lines+markers", name="Cancellations Prevented",
748
- line=dict(color="#9b59b6"), showlegend=False),
749
- row=1, col=3
750
  )
751
-
752
- # Efficiency
753
  fig.add_trace(
754
- go.Scatter(x=sweep_df["threshold"], y=sweep_df["problem_solve_rate"],
755
- mode="lines+markers", name="Efficiency (%)",
756
- line=dict(color="#3498db"), showlegend=False),
757
- row=2, col=1
758
  )
759
 
760
- # Revenue impact
761
  fig.add_trace(
762
- go.Scatter(x=sweep_df["threshold"], y=sweep_df["revenue_impact_percent"],
763
- mode="lines+markers", name="Revenue Impact (%)",
764
- line=dict(color="#f39c12"), showlegend=False),
765
- row=2, col=2
766
  )
767
-
768
- # Cancellation rate
769
  fig.add_trace(
770
- go.Scatter(x=sweep_df["threshold"], y=sweep_df["cancellation_rate"],
771
- mode="lines+markers", name="Remaining Cancellation Rate (%)",
772
- line=dict(color="#e67e22"), showlegend=False),
773
- row=2, col=3
774
  )
775
 
776
- # Add current threshold line to all subplots
777
- for i in range(1, 3):
778
- for j in range(1, 4):
779
- fig.add_vline(x=threshold, line_dash="dash", line_color="red",
780
- annotation_text=f"Current: {threshold}min", row=i, col=j)
781
 
782
- fig.update_layout(height=600, showlegend=True, title_text="Comprehensive Threshold Analysis")
783
  fig.update_xaxes(title_text="Threshold (minutes)")
784
- fig.update_yaxes(title_text="Count", row=1, col=1)
785
- fig.update_yaxes(title_text="Count", row=1, col=2)
786
- fig.update_yaxes(title_text="Count", row=1, col=3)
787
- fig.update_yaxes(title_text="Percentage (%)", row=2, col=1)
788
- fig.update_yaxes(title_text="Percentage (%)", row=2, col=2)
789
- fig.update_yaxes(title_text="Percentage (%)", row=2, col=3)
790
 
791
  st.plotly_chart(fig, use_container_width=True)
792
 
793
- # Optimal threshold recommendation
794
- st.markdown('<div class="section-header">πŸ’‘ Optimal Threshold Recommendation</div>', unsafe_allow_html=True)
795
-
796
- optimal = find_optimal_threshold(sweep_df)
797
-
798
- # Validation check and business sense analysis
799
- if optimal['threshold'] < 60:
800
- st.warning("⚠️ Recommended threshold seems low. Let's analyze why:")
801
-
802
- # Show analysis of why this threshold was chosen
803
- st.markdown(f"""
804
- **Analysis:**
805
- - At {optimal['threshold']:.0f} min: Solves {optimal['problems_solved']:.0f} problems with {optimal['problem_solve_rate']:.1f}% efficiency
806
- - At 90 min: Solves {sweep_df[sweep_df['threshold']==90]['problems_solved'].iloc[0]:.0f} problems with {sweep_df[sweep_df['threshold']==90]['problem_solve_rate'].iloc[0]:.1f}% efficiency
807
- - At 120 min: Solves {sweep_df[sweep_df['threshold']==120]['problems_solved'].iloc[0]:.0f} problems with {sweep_df[sweep_df['threshold']==120]['problem_solve_rate'].iloc[0]:.1f}% efficiency
808
- """)
809
-
810
- # Suggest alternative if current seems too low
811
- alternatives = sweep_df[(sweep_df["threshold"].isin([90, 120])) & (sweep_df["problems_solved"] > 0)]
812
- if len(alternatives) > 0:
813
- best_alt = alternatives.sort_values(["problems_solved", "problem_solve_rate"], ascending=[False, False]).iloc[0]
814
- st.info(f"πŸ’‘ **Alternative consideration:** {best_alt['threshold']:.0f} minutes might be more practical for operational reasons.")
815
-
816
- st.markdown(f"""
817
- <div class="recommendation-box">
818
- <h3>🎯 Recommended Threshold: {optimal['threshold']:.0f} minutes</h3>
819
- <p><strong>Why this threshold?</strong></p>
820
- <ul>
821
- <li>βœ… Solves <strong>{optimal['problems_solved']:.0f}</strong> problematic cases (current waiting situations)</li>
822
- <li>πŸ“‰ Blocks only <strong>{optimal['blocked_percentage']:.1f}%</strong> of consecutive rentals</li>
823
- <li>⚑ Achieves <strong>{optimal['problem_solve_rate']:.1f}%</strong> efficiency in problem solving</li>
824
- <li>πŸ’° Impacts <strong>{optimal['revenue_impact_percent']:.1f}%</strong> of total rental volume</li>
825
- <li>❌ Prevents <strong>{optimal['cancellations_prevented']:.0f}</strong> delay-related cancellations</li>
826
- </ul>
827
- <p><em>This threshold balances customer satisfaction improvements with minimal impact on availability.</em></p>
828
- </div>
829
- """, unsafe_allow_html=True)
830
-
831
- # Additional insight: Show the trade-off analysis
832
- st.markdown("### πŸ“Š Threshold Trade-off Analysis")
833
-
834
- # Create a summary table of key thresholds
835
- key_thresholds = [30, 60, 90, 120, 150]
836
- comparison_data = []
837
 
838
- for t in key_thresholds:
839
- if t in sweep_df["threshold"].values:
840
- row = sweep_df[sweep_df["threshold"] == t].iloc[0]
841
- comparison_data.append({
842
- "Threshold (min)": int(t),
843
- "Problems Solved": int(row["problems_solved"]),
844
- "Blocked Rentals": int(row["blocked_rentals"]),
845
- "Efficiency (%)": f"{row['problem_solve_rate']:.1f}%",
846
- "Revenue Impact (%)": f"{row['revenue_impact_percent']:.1f}%",
847
- "Cancellations Prevented": int(row["cancellations_prevented"])
848
- })
849
-
850
- comparison_df = pd.DataFrame(comparison_data)
851
- st.dataframe(comparison_df, use_container_width=True, hide_index=True)
852
-
853
- # ========== PAGE 3: SCOPE ANALYSIS ==========
854
- elif selected == "🎯 Scope Analysis":
855
- st.title("🎯 Scope Decision: All Cars vs Connect Cars Only?")
856
-
857
- st.markdown("""
858
- <div class="insight-box">
859
- <strong>Goal:</strong> Determine whether to apply the minimum delay feature to all cars or just Connect cars.
860
- Connect cars have better technology for managing transitions, but all cars face the same late return issues.
861
- </div>
862
- """, unsafe_allow_html=True)
863
-
864
- # Controls for scope analysis
865
- threshold_for_scope = st.slider("πŸ• Analysis Threshold (minutes)", 0, 300, 120, step=15,
866
- help="Set threshold for comparing scope options")
867
-
868
- # Calculate metrics for both scopes
869
- all_metrics = calculate_threshold_metrics(df, threshold_for_scope, "all")
870
- connect_metrics = calculate_threshold_metrics(df, threshold_for_scope, "connect")
871
-
872
- st.markdown('<div class="section-header">βš–οΈ Scope Comparison at Selected Threshold</div>', unsafe_allow_html=True)
873
 
874
  col1, col2 = st.columns(2)
875
 
876
  with col1:
877
  st.markdown("### πŸš— All Cars")
878
  st.markdown(f"""
879
- <div style="background-color: #f8f9fa; padding: 1rem; border-radius: 0.5rem; margin: 1rem 0;">
880
- <strong>Blocked Rentals:</strong> {all_metrics['blocked_rentals']:,} ({all_metrics['blocked_percentage']:.1f}%)<br>
881
- <strong>Problems Solved:</strong> {all_metrics['problems_solved']:,}<br>
882
- <strong>Efficiency:</strong> {all_metrics['problem_solve_rate']:.1f}%<br>
883
- <strong>Revenue Impact:</strong> {all_metrics['revenue_impact_percent']:.1f}%<br>
884
- <strong>Cancellations Prevented:</strong> {all_metrics['cancellations_prevented']:,}
885
- </div>
886
- """, unsafe_allow_html=True)
887
 
888
  with col2:
889
  st.markdown("### πŸ”Œ Connect Cars Only")
890
  st.markdown(f"""
891
- <div style="background-color: #e8f4f8; padding: 1rem; border-radius: 0.5rem; margin: 1rem 0;">
892
- <strong>Blocked Rentals:</strong> {connect_metrics['blocked_rentals']:,} ({connect_metrics['blocked_percentage']:.1f}%)<br>
893
- <strong>Problems Solved:</strong> {connect_metrics['problems_solved']:,}<br>
894
- <strong>Efficiency:</strong> {connect_metrics['problem_solve_rate']:.1f}%<br>
895
- <strong>Revenue Impact:</strong> {connect_metrics['revenue_impact_percent']:.1f}%<br>
896
- <strong>Cancellations Prevented:</strong> {connect_metrics['cancellations_prevented']:,}
897
- </div>
898
- """, unsafe_allow_html=True)
899
-
900
- # Comprehensive scope comparison
901
- st.markdown('<div class="section-header">πŸ“Š Comprehensive Scope Analysis</div>', unsafe_allow_html=True)
902
-
903
- thresholds = list(range(0, 301, 30))
904
- all_sweep = create_threshold_sweep(df, thresholds, "all")
905
- connect_sweep = create_threshold_sweep(df, thresholds, "connect")
906
-
907
- # Add scope identifier
908
- all_sweep["scope"] = "All Cars"
909
- connect_sweep["scope"] = "Connect Only"
910
- combined_sweep = pd.concat([all_sweep, connect_sweep], ignore_index=True)
911
-
912
- # Create comparison visualizations (with cancellations)
913
- fig = make_subplots(
914
- rows=2, cols=3,
915
- subplot_titles=(
916
- "Blocked Rentals by Scope",
917
- "Problems Solved by Scope",
918
- "Cancellations Prevented by Scope",
919
- "Efficiency by Scope",
920
- "Revenue Impact by Scope",
921
- "Remaining Cancellation Rate by Scope"
922
- )
923
- )
924
 
925
- colors = {"All Cars": "#1f77b4", "Connect Only": "#ff7f0e"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
926
 
927
- for scope_name in ["All Cars", "Connect Only"]:
928
- scope_data = combined_sweep[combined_sweep["scope"] == scope_name]
929
-
930
- # Blocked rentals
931
- fig.add_trace(
932
- go.Scatter(x=scope_data["threshold"], y=scope_data["blocked_rentals"],
933
- mode="lines+markers", name=f"{scope_name}",
934
- line=dict(color=colors[scope_name]), legendgroup=scope_name),
935
- row=1, col=1
936
- )
937
-
938
- # Problems solved
939
- fig.add_trace(
940
- go.Scatter(x=scope_data["threshold"], y=scope_data["problems_solved"],
941
- mode="lines+markers", name=f"{scope_name}",
942
- line=dict(color=colors[scope_name]), legendgroup=scope_name, showlegend=False),
943
- row=1, col=2
944
- )
945
-
946
- # Cancellations prevented
947
- fig.add_trace(
948
- go.Scatter(x=scope_data["threshold"], y=scope_data["cancellations_prevented"],
949
- mode="lines+markers", name=f"{scope_name}",
950
- line=dict(color=colors[scope_name]), legendgroup=scope_name, showlegend=False),
951
- row=1, col=3
952
- )
953
-
954
- # Efficiency
955
- fig.add_trace(
956
- go.Scatter(x=scope_data["threshold"], y=scope_data["problem_solve_rate"],
957
- mode="lines+markers", name=f"{scope_name}",
958
- line=dict(color=colors[scope_name]), legendgroup=scope_name, showlegend=False),
959
- row=2, col=1
960
- )
961
-
962
- # Revenue impact
963
- fig.add_trace(
964
- go.Scatter(x=scope_data["threshold"], y=scope_data["revenue_impact_percent"],
965
- mode="lines+markers", name=f"{scope_name}",
966
- line=dict(color=colors[scope_name]), legendgroup=scope_name, showlegend=False),
967
- row=2, col=2
968
- )
969
-
970
- # Remaining cancellation rate
971
- fig.add_trace(
972
- go.Scatter(x=scope_data["threshold"], y=scope_data["cancellation_rate"],
973
- mode="lines+markers", name=f"{scope_name}",
974
- line=dict(color=colors[scope_name]), legendgroup=scope_name, showlegend=False),
975
- row=2, col=3
976
  )
 
 
 
 
977
 
978
- # Add current threshold line
979
- for i in range(1, 3):
980
- for j in range(1, 4):
981
- fig.add_vline(x=threshold_for_scope, line_dash="dash", line_color="red",
982
- annotation_text=f"Analysis: {threshold_for_scope}min", row=i, col=j)
983
-
984
- fig.update_layout(height=600, showlegend=True, title_text="Scope Comparison Analysis")
985
- fig.update_xaxes(title_text="Threshold (minutes)")
986
- fig.update_yaxes(title_text="Count", row=1, col=1)
987
- fig.update_yaxes(title_text="Count", row=1, col=2)
988
- fig.update_yaxes(title_text="Count", row=1, col=3)
989
- fig.update_yaxes(title_text="Percentage (%)", row=2, col=1)
990
- fig.update_yaxes(title_text="Percentage (%)", row=2, col=2)
991
- fig.update_yaxes(title_text="Percentage (%)", row=2, col=3)
992
-
993
- st.plotly_chart(fig, use_container_width=True)
994
-
995
- # Checkin type analysis
996
- st.markdown('<div class="section-header">πŸ” Checkin Type Analysis</div>', unsafe_allow_html=True)
997
 
998
  col1, col2 = st.columns(2)
999
 
1000
  with col1:
1001
- st.markdown("### Current Problems by Type")
1002
-
1003
- df_problems = df[df["has_previous_rental"]].copy()
1004
-
1005
- # Join with previous rental delay data
1006
- prev_rental_data = df[["rental_id", "delay_at_checkout_in_minutes"]].rename(
1007
- columns={"rental_id": "previous_ended_rental_id",
1008
- "delay_at_checkout_in_minutes": "previous_delay"}
1009
- )
1010
-
1011
- df_problems = df_problems.merge(
1012
- prev_rental_data,
1013
- on="previous_ended_rental_id",
1014
- how="left"
1015
- )
1016
-
1017
- # Calculate problems using actual previous rental delay
1018
- df_problems["previous_delay_clean"] = df_problems["previous_delay"].clip(-720, 720)
1019
- df_problems["causes_problem"] = (
1020
- df_problems["previous_delay"].notnull() &
1021
- (df_problems["previous_delay_clean"] > df_problems["time_delta_with_previous_rental_in_minutes"])
1022
- )
1023
-
1024
- problem_by_type = df_problems.groupby("checkin_type")["causes_problem"].agg(["sum", "count"]).reset_index()
1025
- problem_by_type["rate"] = (problem_by_type["sum"] / problem_by_type["count"] * 100).round(1)
1026
-
1027
- fig_problems = px.bar(
1028
- problem_by_type,
1029
- x="checkin_type",
1030
- y="sum",
1031
- title="Current Problems by Checkin Type",
1032
- labels={"sum": "Number of Problems", "checkin_type": "Checkin Type"}
1033
- )
1034
- st.plotly_chart(fig_problems, use_container_width=True)
1035
 
1036
  with col2:
1037
- st.markdown("### Gap Distribution by Type")
1038
-
1039
- gap_data = df[df["has_previous_rental"]].copy()
1040
- gap_data = gap_data[gap_data["time_delta_with_previous_rental_in_minutes"].between(0, 300)]
1041
-
1042
- fig_gaps = px.box(
1043
- gap_data,
1044
- x="checkin_type",
1045
- y="time_delta_with_previous_rental_in_minutes",
1046
- title="Gap Distribution by Checkin Type",
1047
- labels={"time_delta_with_previous_rental_in_minutes": "Gap (minutes)", "checkin_type": "Checkin Type"}
1048
- )
1049
- st.plotly_chart(fig_gaps, use_container_width=True)
1050
 
1051
- # Final scope recommendation
1052
- st.markdown('<div class="section-header">πŸ† Scope Recommendation</div>', unsafe_allow_html=True)
 
 
1053
 
1054
- # Logic for recommendation
1055
- if (connect_metrics['problem_solve_rate'] > all_metrics['problem_solve_rate'] and
1056
- connect_metrics['revenue_impact_percent'] <= all_metrics['revenue_impact_percent']):
1057
- recommendation = "Connect Only"
1058
- rec_metrics = connect_metrics
1059
- reason = "Connect cars show higher efficiency with lower revenue impact"
1060
- elif connect_metrics['problems_solved'] >= all_metrics['problems_solved'] * 0.8:
1061
- recommendation = "Connect Only"
1062
- rec_metrics = connect_metrics
1063
- reason = "Connect cars solve most problems with focused implementation"
1064
- else:
1065
- recommendation = "All Cars"
1066
- rec_metrics = all_metrics
1067
- reason = "All cars approach solves significantly more problems overall"
1068
 
1069
- st.markdown(f"""
1070
- <div class="recommendation-box">
1071
- <h3>🎯 Recommended Scope: {recommendation}</h3>
1072
- <p><strong>Reasoning:</strong> {reason}</p>
1073
- <p><strong>At {threshold_for_scope} minutes threshold:</strong></p>
1074
- <ul>
1075
- <li>βœ… Solves <strong>{rec_metrics['problems_solved']:,}</strong> problematic cases</li>
1076
- <li>πŸ“‰ Blocks <strong>{rec_metrics['blocked_percentage']:.1f}%</strong> of consecutive rentals</li>
1077
- <li>⚑ Achieves <strong>{rec_metrics['problem_solve_rate']:.1f}%</strong> efficiency</li>
1078
- <li>πŸ’° Impacts <strong>{rec_metrics['revenue_impact_percent']:.1f}%</strong> of total rental volume</li>
1079
- <li>❌ Prevents <strong>{rec_metrics['cancellations_prevented']:,}</strong> delay-related cancellations</li>
1080
- </ul>
1081
- </div>
1082
- """, unsafe_allow_html=True)
1083
 
1084
  # ========== FOOTER ==========
1085
  st.markdown("---")
1086
  st.markdown("""
1087
  <div style='text-align: center; color: #666; padding: 1rem;'>
1088
- <p>πŸš— <strong>Getaround Analysis Dashboard</strong> - Threshold & Scope Decision Support</p>
1089
- <p><em>Built with Streamlit & Plotly for data-driven product decisions</em></p>
1090
  </div>
1091
  """, unsafe_allow_html=True)
 
12
 
13
  # ========== PAGE CONFIGURATION ==========
14
  st.set_page_config(
15
+ page_title="Getaround Delay Analysis",
16
  page_icon="πŸš—",
17
  layout="wide",
18
  initial_sidebar_state="expanded"
 
60
  </style>
61
  """, unsafe_allow_html=True)
62
 
63
+ # ========== DATA LOADING ==========
64
  @st.cache_data(show_spinner=False)
65
  def load_data():
66
  """Load and preprocess the rental data"""
 
67
  possible_paths = [
68
  "get_around_delay_analysis.xlsx",
69
  "get_around_delay_analysis.csv",
 
79
  df = pd.read_excel(path)
80
  else:
81
  df = pd.read_csv(path)
 
82
  break
83
  except Exception as e:
84
  continue
 
89
 
90
  # Clean the data
91
  df = df.drop(columns=[c for c in df.columns if c.lower().startswith("unnamed")], errors="ignore")
 
 
92
  df["has_previous_rental"] = df["time_delta_with_previous_rental_in_minutes"].notnull()
93
+ df["clean_delay"] = df["delay_at_checkout_in_minutes"].clip(-720, 720)
94
 
95
  return df
96
 
97
  # ========== ANALYSIS FUNCTIONS ==========
98
+ def calculate_metrics(df, threshold_minutes, scope="all"):
99
+ """Calculate key metrics for threshold analysis"""
100
 
101
  # Filter by scope
102
  if scope == "connect":
 
104
  else:
105
  df_filtered = df.copy()
106
 
 
107
  df_with_prev = df_filtered[df_filtered["has_previous_rental"]].copy()
108
 
109
  if len(df_with_prev) == 0:
 
114
  "blocked_percentage": 0.0,
115
  "current_problems": 0,
116
  "problems_solved": 0,
117
+ "solve_efficiency": 0.0,
118
+ "revenue_impact": 0.0
 
 
 
 
119
  }
120
 
121
+ # Join with previous rental data
122
  prev_rental_data = df[["rental_id", "delay_at_checkout_in_minutes"]].rename(
123
  columns={"rental_id": "previous_ended_rental_id",
124
  "delay_at_checkout_in_minutes": "previous_delay"}
125
  )
126
 
127
+ df_with_prev = df_with_prev.merge(prev_rental_data, on="previous_ended_rental_id", how="left")
 
 
 
 
128
 
129
+ # Calculate key metrics
130
  df_with_prev["would_be_blocked"] = df_with_prev["time_delta_with_previous_rental_in_minutes"] < threshold_minutes
 
 
131
  df_with_prev["previous_delay_clean"] = df_with_prev["previous_delay"].clip(-720, 720)
132
  df_with_prev["causes_problem"] = (
133
  df_with_prev["previous_delay"].notnull() &
134
  (df_with_prev["previous_delay_clean"] > df_with_prev["time_delta_with_previous_rental_in_minutes"])
135
  )
 
 
 
 
 
 
 
 
136
  df_with_prev["problem_solved"] = df_with_prev["causes_problem"] & df_with_prev["would_be_blocked"]
137
 
138
+ # Final calculations
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  total_rentals = len(df_filtered)
140
  rentals_with_previous = len(df_with_prev)
141
  blocked_rentals = int(df_with_prev["would_be_blocked"].sum())
142
  blocked_percentage = (blocked_rentals / rentals_with_previous) * 100 if rentals_with_previous > 0 else 0
143
  current_problems = int(df_with_prev["causes_problem"].sum())
144
  problems_solved = int(df_with_prev["problem_solved"].sum())
145
+ solve_efficiency = (problems_solved / blocked_rentals * 100) if blocked_rentals > 0 else 0
146
+ revenue_impact = (blocked_rentals / total_rentals) * 100 if total_rentals > 0 else 0
 
 
 
 
 
 
 
 
 
147
 
148
  return {
149
  "total_rentals": total_rentals,
 
152
  "blocked_percentage": blocked_percentage,
153
  "current_problems": current_problems,
154
  "problems_solved": problems_solved,
155
+ "solve_efficiency": solve_efficiency,
156
+ "revenue_impact": revenue_impact
 
 
 
 
157
  }
158
 
159
+ def create_threshold_analysis(df, thresholds, scope="all"):
160
+ """Create threshold analysis data"""
161
  results = []
162
  for threshold in thresholds:
163
+ metrics = calculate_metrics(df, threshold, scope)
164
  results.append({"threshold": threshold, **metrics})
165
  return pd.DataFrame(results)
166
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  # ========== LOAD DATA ==========
168
  df = load_data()
169
 
170
+ # ========== MAIN DASHBOARD ==========
171
+ st.markdown('<h1 class="main-header">πŸš— Getaround Delay Analysis</h1>', unsafe_allow_html=True)
172
 
173
+ # Sidebar
174
  with st.sidebar:
175
+ st.markdown("## πŸŽ›οΈ Analysis Controls")
176
+
177
  selected = option_menu(
178
+ "Analysis Focus",
179
+ ["πŸ“Š Overview & Problems", "🎯 Threshold & Scope"],
180
+ icons=["bar-chart", "target"],
181
  menu_icon="cast",
182
  default_index=0,
183
  )
184
 
185
+ if selected == "🎯 Threshold & Scope":
186
+ st.markdown("### Controls")
187
+ threshold = st.slider("πŸ• Threshold (minutes)", 0, 300, 90, step=30,
188
+ help="Minimum delay between consecutive rentals")
189
+ scope = st.selectbox("πŸš— Scope", ["all", "connect"],
190
+ format_func=lambda x: "All Cars" if x == "all" else "Connect Cars Only")
191
+
192
  st.markdown("---")
193
  st.markdown("### πŸ“ˆ Dataset Info")
194
  st.info(f"""
195
  **Total Rentals:** {len(df):,}
 
196
  **Connect Rentals:** {len(df[df['checkin_type'].str.lower() == 'connect']):,}
197
+ **Cancelled Rentals:** {len(df[df['state'] == 'canceled']):,}
198
+ **With Previous Rental:** {df['has_previous_rental'].sum():,}
199
  """)
200
 
201
+ # ========== PAGE 1: OVERVIEW & PROBLEMS ==========
202
+ if selected == "πŸ“Š Overview & Problems":
203
+ st.title("πŸ“Š Understanding the Delay Problem")
204
 
205
+ # Key Problem Analysis
206
+ st.markdown('<div class="section-header">🚨 Current Problem Scope</div>', unsafe_allow_html=True)
207
 
208
+ # Calculate current problems
209
+ df_problems = df[df["has_previous_rental"]].copy()
210
+ prev_rental_data = df[["rental_id", "delay_at_checkout_in_minutes"]].rename(
211
+ columns={"rental_id": "previous_ended_rental_id",
212
+ "delay_at_checkout_in_minutes": "previous_delay"}
213
+ )
214
+ df_problems = df_problems.merge(prev_rental_data, on="previous_ended_rental_id", how="left")
215
+ df_problems["previous_delay_clean"] = df_problems["previous_delay"].clip(-720, 720)
216
+ df_problems["causes_problem"] = (
217
+ df_problems["previous_delay"].notnull() &
218
+ (df_problems["previous_delay_clean"] > df_problems["time_delta_with_previous_rental_in_minutes"])
219
+ )
220
+ df_problems["wait_time"] = np.maximum(
221
+ 0,
222
+ df_problems["previous_delay_clean"] - df_problems["time_delta_with_previous_rental_in_minutes"]
223
+ ).fillna(0)
224
+
225
+ problem_cases = df_problems[df_problems["causes_problem"]]
226
+
227
+ # Key metrics
228
  col1, col2, col3, col4 = st.columns(4)
229
  with col1:
230
+ st.metric("Problem Cases", f"{len(problem_cases):,}",
231
+ help="Rentals where previous delay caused waiting")
232
  with col2:
233
+ problem_rate = (len(problem_cases) / len(df_problems)) * 100 if len(df_problems) > 0 else 0
234
+ st.metric("Problem Rate", f"{problem_rate:.1f}%")
235
  with col3:
236
+ avg_wait = problem_cases["wait_time"].mean() if len(problem_cases) > 0 else 0
237
+ st.metric("Avg Wait Time", f"{avg_wait:.1f} min")
238
  with col4:
239
+ delay_cancels = problem_cases[problem_cases["state"] == "canceled"]
240
+ st.metric("Resulting Cancellations", f"{len(delay_cancels):,}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
 
242
+ st.markdown("""
243
+ <div class="insight-box">
244
+ <strong>The Problem:</strong> When drivers return cars late, it creates waiting time for the next customer.
245
+ In worst cases, customers cancel their rental due to delays.
246
+ </div>
247
+ """, unsafe_allow_html=True)
 
 
 
248
 
249
+ # Visual analysis
250
+ st.markdown('<div class="section-header">πŸ“ˆ Delay Patterns</div>', unsafe_allow_html=True)
251
 
252
  col1, col2 = st.columns(2)
253
 
254
  with col1:
255
+ # Return status distribution
256
  df_with_delay_data = df[df["delay_at_checkout_in_minutes"].notnull()].copy()
257
  df_with_delay_data["delay_status"] = df_with_delay_data["delay_at_checkout_in_minutes"].apply(
258
  lambda x: "Early Return" if x < 0 else "On Time" if x == 0 else "Late Return"
259
  )
260
 
261
  delay_counts = df_with_delay_data["delay_status"].value_counts()
262
+ fig_status = px.pie(
263
  values=delay_counts.values,
264
  names=delay_counts.index,
265
+ title="Return Status Distribution",
266
+ color_discrete_sequence=px.colors.qualitative.Set3
 
 
 
 
 
267
  )
268
+ st.plotly_chart(fig_status, use_container_width=True)
269
 
270
  with col2:
271
+ # Delay distribution
272
+ delay_filtered = df_with_delay_data[df_with_delay_data["delay_at_checkout_in_minutes"].between(-60, 240)]
273
+ fig_hist = px.histogram(
 
 
274
  delay_filtered,
275
  x="delay_at_checkout_in_minutes",
276
+ nbins=30,
277
+ title="Delay Distribution",
278
+ labels={"delay_at_checkout_in_minutes": "Delay (minutes)", "count": "Rentals"}
279
  )
280
+ fig_hist.add_vline(x=0, line_dash="dash", line_color="red", annotation_text="On Time")
281
+ st.plotly_chart(fig_hist, use_container_width=True)
 
 
 
 
282
 
283
  # Gap analysis
284
+ st.markdown('<div class="section-header">πŸ“ Time Gaps Between Rentals</div>', unsafe_allow_html=True)
285
 
286
+ gap_data = df[df["has_previous_rental"]]
287
+ gap_filtered = gap_data[gap_data["time_delta_with_previous_rental_in_minutes"].between(0, 480)]
288
 
289
+ fig_gap = px.histogram(
290
+ gap_filtered,
291
+ x="time_delta_with_previous_rental_in_minutes",
292
+ nbins=20,
293
+ title="Gap Distribution Between Consecutive Rentals",
294
+ labels={"time_delta_with_previous_rental_in_minutes": "Gap (minutes)", "count": "Rentals"}
295
+ )
296
+ st.plotly_chart(fig_gap, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
 
298
  # Cancellation analysis
299
+ st.markdown('<div class="section-header">❌ Cancellation Impact</div>', unsafe_allow_html=True)
300
 
301
  col1, col2 = st.columns(2)
302
 
303
  with col1:
304
+ total_cancels_all = (df["state"] == "canceled").sum()
305
+ delay_related_cancels = (df_problems["state"] == "canceled") & df_problems["causes_problem"]
306
+ delay_cancel_count = delay_related_cancels.sum()
307
 
308
+ st.metric("Total Cancellations", f"{total_cancels_all:,}")
309
+ st.metric("Due to Previous Delays", f"{delay_cancel_count:,}")
310
+ if total_cancels_all > 0:
311
+ delay_cancel_rate = (delay_cancel_count / total_cancels_all * 100)
312
+ st.metric("% Due to Delays", f"{delay_cancel_rate:.1f}%")
313
+
314
+ with col2:
315
+ # Cancellation by checkin type
316
  cancellation_by_type = df.groupby(['checkin_type', 'state']).size().unstack(fill_value=0)
317
  if 'canceled' in cancellation_by_type.columns:
318
  cancel_rates = (cancellation_by_type['canceled'] / cancellation_by_type.sum(axis=1) * 100).round(1)
319
+ fig_cancel = px.bar(
 
320
  x=cancel_rates.index,
321
  y=cancel_rates.values,
322
+ title="Cancellation Rate by Type",
323
  labels={"x": "Checkin Type", "y": "Cancellation Rate (%)"}
324
  )
325
+ st.plotly_chart(fig_cancel, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
 
327
+ # ========== PAGE 2: THRESHOLD & SCOPE ANALYSIS ==========
328
+ elif selected == "🎯 Threshold & Scope":
329
+ st.title("🎯 Threshold & Scope Decision")
330
 
331
  st.markdown("""
332
  <div class="insight-box">
333
+ <strong>Goal:</strong> Find the optimal minimum delay threshold and determine whether to apply it to all cars or just Connect cars.
334
+ Higher thresholds solve more problems but block more rentals.
 
 
 
 
 
335
  </div>
336
  """, unsafe_allow_html=True)
337
 
338
+ # Current impact
339
+ current_metrics = calculate_metrics(df, threshold, scope)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
 
341
+ st.markdown('<div class="section-header">πŸ“Š Impact at Current Settings</div>', unsafe_allow_html=True)
342
 
343
  col1, col2, col3, col4 = st.columns(4)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
  with col1:
345
  st.markdown(f"""
346
  <div class="metric-card">
347
  <h3 style="color: #e74c3c;">{current_metrics['blocked_rentals']:,}</h3>
348
  <p>Blocked Rentals</p>
349
+ <small>Gap < {threshold} min</small>
350
  </div>
351
  """, unsafe_allow_html=True)
352
 
 
355
  <div class="metric-card">
356
  <h3 style="color: #f39c12;">{current_metrics['blocked_percentage']:.1f}%</h3>
357
  <p>Blocked Rate</p>
358
+ <small>Of consecutive rentals</small>
359
  </div>
360
  """, unsafe_allow_html=True)
361
 
 
364
  <div class="metric-card">
365
  <h3 style="color: #27ae60;">{current_metrics['problems_solved']:,}</h3>
366
  <p>Problems Solved</p>
367
+ <small>Waiting eliminated</small>
368
  </div>
369
  """, unsafe_allow_html=True)
370
 
371
  with col4:
372
  st.markdown(f"""
373
  <div class="metric-card">
374
+ <h3 style="color: #3498db;">{current_metrics['solve_efficiency']:.1f}%</h3>
375
+ <p>Efficiency</p>
376
+ <small>Problems/Blocked</small>
377
  </div>
378
  """, unsafe_allow_html=True)
379
 
380
+ # Threshold analysis
381
+ st.markdown('<div class="section-header">πŸ“ˆ Threshold Analysis</div>', unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
 
383
  thresholds = list(range(0, 301, 30))
384
+ threshold_data = create_threshold_analysis(df, thresholds, scope)
385
 
386
+ # Create dual-axis chart
387
  fig = make_subplots(
388
+ rows=1, cols=2,
389
+ subplot_titles=("Problems Solved vs Blocked Rentals", "Efficiency vs Revenue Impact"),
390
+ specs=[[{"secondary_y": True}, {"secondary_y": True}]]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  )
392
 
393
+ # Left chart: Problems vs Blocked
394
  fig.add_trace(
395
+ go.Scatter(x=threshold_data["threshold"], y=threshold_data["problems_solved"],
396
+ mode="lines+markers", name="Problems Solved", line=dict(color="green")),
397
+ row=1, col=1, secondary_y=False
 
398
  )
 
 
399
  fig.add_trace(
400
+ go.Scatter(x=threshold_data["threshold"], y=threshold_data["blocked_rentals"],
401
+ mode="lines+markers", name="Blocked Rentals", line=dict(color="red")),
402
+ row=1, col=1, secondary_y=True
 
403
  )
404
 
405
+ # Right chart: Efficiency vs Impact
406
  fig.add_trace(
407
+ go.Scatter(x=threshold_data["threshold"], y=threshold_data["solve_efficiency"],
408
+ mode="lines+markers", name="Efficiency (%)", line=dict(color="blue")),
409
+ row=1, col=2, secondary_y=False
 
410
  )
 
 
411
  fig.add_trace(
412
+ go.Scatter(x=threshold_data["threshold"], y=threshold_data["revenue_impact"],
413
+ mode="lines+markers", name="Revenue Impact (%)", line=dict(color="orange")),
414
+ row=1, col=2, secondary_y=True
 
415
  )
416
 
417
+ # Add current threshold line
418
+ fig.add_vline(x=threshold, line_dash="dash", line_color="red",
419
+ annotation_text=f"Current: {threshold}min", row=1, col=1)
420
+ fig.add_vline(x=threshold, line_dash="dash", line_color="red",
421
+ annotation_text=f"Current: {threshold}min", row=1, col=2)
422
 
423
+ fig.update_layout(height=400, showlegend=True)
424
  fig.update_xaxes(title_text="Threshold (minutes)")
425
+ fig.update_yaxes(title_text="Count", secondary_y=False, row=1, col=1)
426
+ fig.update_yaxes(title_text="Count", secondary_y=True, row=1, col=1)
427
+ fig.update_yaxes(title_text="Percentage (%)", secondary_y=False, row=1, col=2)
428
+ fig.update_yaxes(title_text="Percentage (%)", secondary_y=True, row=1, col=2)
 
 
429
 
430
  st.plotly_chart(fig, use_container_width=True)
431
 
432
+ # Scope comparison
433
+ st.markdown('<div class="section-header">πŸš— Scope Comparison: All Cars vs Connect Only</div>', unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
 
435
+ all_metrics = calculate_metrics(df, threshold, "all")
436
+ connect_metrics = calculate_metrics(df, threshold, "connect")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
437
 
438
  col1, col2 = st.columns(2)
439
 
440
  with col1:
441
  st.markdown("### πŸš— All Cars")
442
  st.markdown(f"""
443
+ **Problems Solved:** {all_metrics['problems_solved']:,}
444
+ **Blocked Rentals:** {all_metrics['blocked_rentals']:,} ({all_metrics['blocked_percentage']:.1f}%)
445
+ **Efficiency:** {all_metrics['solve_efficiency']:.1f}%
446
+ **Revenue Impact:** {all_metrics['revenue_impact']:.1f}%
447
+ """)
 
 
 
448
 
449
  with col2:
450
  st.markdown("### πŸ”Œ Connect Cars Only")
451
  st.markdown(f"""
452
+ **Problems Solved:** {connect_metrics['problems_solved']:,}
453
+ **Blocked Rentals:** {connect_metrics['blocked_rentals']:,} ({connect_metrics['blocked_percentage']:.1f}%)
454
+ **Efficiency:** {connect_metrics['solve_efficiency']:.1f}%
455
+ **Revenue Impact:** {connect_metrics['revenue_impact']:.1f}%
456
+ """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
 
458
+ # Scope comparison chart
459
+ all_data = create_threshold_analysis(df, thresholds, "all")
460
+ connect_data = create_threshold_analysis(df, thresholds, "connect")
461
+
462
+ fig_scope = go.Figure()
463
+ fig_scope.add_trace(go.Scatter(x=all_data["threshold"], y=all_data["problems_solved"],
464
+ mode="lines+markers", name="All Cars - Problems Solved"))
465
+ fig_scope.add_trace(go.Scatter(x=connect_data["threshold"], y=connect_data["problems_solved"],
466
+ mode="lines+markers", name="Connect Only - Problems Solved"))
467
+ fig_scope.add_vline(x=threshold, line_dash="dash", line_color="red",
468
+ annotation_text=f"Current: {threshold}min")
469
+ fig_scope.update_layout(title="Problems Solved by Scope",
470
+ xaxis_title="Threshold (minutes)",
471
+ yaxis_title="Problems Solved")
472
+ st.plotly_chart(fig_scope, use_container_width=True)
473
+
474
+ # Recommendations
475
+ st.markdown('<div class="section-header">πŸ’‘ Recommendations</div>', unsafe_allow_html=True)
476
+
477
+ # Find optimal threshold
478
+ viable_thresholds = threshold_data[
479
+ (threshold_data["problems_solved"] > 0) &
480
+ (threshold_data["threshold"] >= 60) &
481
+ (threshold_data["threshold"] <= 180)
482
+ ]
483
 
484
+ if len(viable_thresholds) > 0:
485
+ # Find sweet spot: good problems solved with reasonable efficiency
486
+ viable_thresholds["score"] = (
487
+ viable_thresholds["problems_solved"] / viable_thresholds["problems_solved"].max() * 0.6 +
488
+ viable_thresholds["solve_efficiency"] / 100 * 0.4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
  )
490
+ optimal = viable_thresholds.sort_values("score", ascending=False).iloc[0]
491
+ optimal_threshold = int(optimal["threshold"])
492
+ else:
493
+ optimal_threshold = 90
494
 
495
+ # Scope recommendation
496
+ if connect_metrics['solve_efficiency'] > all_metrics['solve_efficiency'] and connect_metrics['problems_solved'] >= all_metrics['problems_solved'] * 0.7:
497
+ recommended_scope = "Connect Only"
498
+ scope_reason = "Higher efficiency with most problems solved"
499
+ else:
500
+ recommended_scope = "All Cars"
501
+ scope_reason = "Solves more problems overall"
 
 
 
 
 
 
 
 
 
 
 
 
502
 
503
  col1, col2 = st.columns(2)
504
 
505
  with col1:
506
+ st.markdown(f"""
507
+ <div class="recommendation-box">
508
+ <h3>🎯 Recommended Threshold</h3>
509
+ <h2>{optimal_threshold} minutes</h2>
510
+ <p>Balances problem solving with availability impact</p>
511
+ </div>
512
+ """, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
513
 
514
  with col2:
515
+ st.markdown(f"""
516
+ <div class="recommendation-box">
517
+ <h3>πŸš— Recommended Scope</h3>
518
+ <h2>{recommended_scope}</h2>
519
+ <p>{scope_reason}</p>
520
+ </div>
521
+ """, unsafe_allow_html=True)
 
 
 
 
 
 
522
 
523
+ # Summary table
524
+ st.markdown("### πŸ“‹ Key Threshold Options")
525
+ summary_thresholds = [60, 90, 120, 150]
526
+ summary_data = []
527
 
528
+ for t in summary_thresholds:
529
+ if t in threshold_data["threshold"].values:
530
+ row = threshold_data[threshold_data["threshold"] == t].iloc[0]
531
+ summary_data.append({
532
+ "Threshold": f"{int(t)} min",
533
+ "Problems Solved": int(row["problems_solved"]),
534
+ "Blocked Rentals": int(row["blocked_rentals"]),
535
+ "Efficiency": f"{row['solve_efficiency']:.1f}%",
536
+ "Revenue Impact": f"{row['revenue_impact']:.1f}%"
537
+ })
 
 
 
 
538
 
539
+ summary_df = pd.DataFrame(summary_data)
540
+ st.dataframe(summary_df, use_container_width=True, hide_index=True)
 
 
 
 
 
 
 
 
 
 
 
 
541
 
542
  # ========== FOOTER ==========
543
  st.markdown("---")
544
  st.markdown("""
545
  <div style='text-align: center; color: #666; padding: 1rem;'>
546
+ <p>πŸš— <strong>Getaround Delay Analysis</strong> - Supporting threshold and scope decisions</p>
 
547
  </div>
548
  """, unsafe_allow_html=True)