sony9316 commited on
Commit
a3dec8c
ยท
verified ยท
1 Parent(s): 1574d42

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +215 -115
app.py CHANGED
@@ -1,4 +1,3 @@
1
- # app.py
2
  import streamlit as st
3
  import pandas as pd
4
  import numpy as np
@@ -121,7 +120,10 @@ def calculate_threshold_metrics(df, threshold_minutes, scope="all"):
121
  "problems_solved": 0,
122
  "problem_solve_rate": 0.0,
123
  "avg_wait_time": 0.0,
124
- "revenue_impact_percent": 0.0
 
 
 
125
  }
126
 
127
  # Calculate blocked rentals (gap < threshold)
@@ -143,6 +145,19 @@ def calculate_threshold_metrics(df, threshold_minutes, scope="all"):
143
  # Problems solved by blocking
144
  df_with_prev["problem_solved"] = df_with_prev["causes_problem"] & df_with_prev["would_be_blocked"]
145
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  # Calculate final metrics
147
  total_rentals = len(df_filtered)
148
  rentals_with_previous = len(df_with_prev)
@@ -154,6 +169,11 @@ def calculate_threshold_metrics(df, threshold_minutes, scope="all"):
154
  avg_wait_time = df_with_prev[df_with_prev["causes_problem"]]["wait_time_next_driver"].mean()
155
  revenue_impact_percent = (blocked_rentals / total_rentals) * 100 if total_rentals > 0 else 0
156
 
 
 
 
 
 
157
  return {
158
  "total_rentals": total_rentals,
159
  "rentals_with_previous": rentals_with_previous,
@@ -163,7 +183,10 @@ def calculate_threshold_metrics(df, threshold_minutes, scope="all"):
163
  "problems_solved": problems_solved,
164
  "problem_solve_rate": problem_solve_rate,
165
  "avg_wait_time": avg_wait_time if not pd.isna(avg_wait_time) else 0,
166
- "revenue_impact_percent": revenue_impact_percent
 
 
 
167
  }
168
 
169
  def create_threshold_sweep(df, thresholds, scope="all"):
@@ -174,20 +197,24 @@ def create_threshold_sweep(df, thresholds, scope="all"):
174
  results.append({"threshold": threshold, **metrics})
175
  return pd.DataFrame(results)
176
 
177
- def find_optimal_threshold(sweep_df, max_blocked_rate=15):
178
  """Find optimal threshold using business logic"""
179
  # Only consider thresholds that actually solve problems (> 0 problems solved)
180
  viable = sweep_df[
181
  (sweep_df["problems_solved"] > 0) &
182
- (sweep_df["blocked_percentage"] <= max_blocked_rate)
 
183
  ]
184
 
185
  if len(viable) == 0:
186
  # If no threshold meets the blocked rate criteria, find the one with best ratio
187
- viable = sweep_df[sweep_df["problems_solved"] > 0]
 
 
 
188
  if len(viable) == 0:
189
- # If no problems can be solved, return the minimum meaningful threshold
190
- return sweep_df[sweep_df["threshold"] == sweep_df["threshold"].min()].iloc[0]
191
 
192
  # Calculate efficiency and optimize
193
  viable["efficiency"] = viable["problems_solved"] / viable["blocked_rentals"].replace({0: np.nan})
@@ -212,8 +239,8 @@ with st.sidebar:
212
  st.markdown("## ๐ŸŽ›๏ธ Navigation")
213
  selected = option_menu(
214
  "Analysis Sections",
215
- ["๐Ÿ• Threshold Analysis", "๐ŸŽฏ Scope Analysis", "๐Ÿ“Š Data Overview"],
216
- icons=["clock", "target", "bar-chart"],
217
  menu_icon="cast",
218
  default_index=0,
219
  )
@@ -222,13 +249,13 @@ with st.sidebar:
222
  st.markdown("### ๐Ÿ“ˆ Dataset Info")
223
  st.info(f"""
224
  **Total Rentals:** {len(df):,}
225
- **With Previous Rental:** {df['has_previous_rental'].sum():,}
226
  **Connect Rentals:** {len(df[df['checkin_type'].str.lower() == 'connect']):,}
227
  **Mobile Rentals:** {len(df[df['checkin_type'].str.lower() == 'mobile']):,}
228
  """)
229
 
230
- # ========== PAGE 1: THRESHOLD ANALYSIS ==========
231
- if selected == "๐Ÿ• Threshold Analysis":
232
  st.title("โฐ Threshold Decision: How Long Should the Minimum Delay Be?")
233
 
234
  st.markdown("""
@@ -241,23 +268,35 @@ if selected == "๐Ÿ• Threshold Analysis":
241
  # Controls
242
  col1, col2 = st.columns(2)
243
  with col1:
244
- threshold = st.slider("๐Ÿ• Threshold (minutes)", 0, 300, 120, step=15,
245
  help="Minimum time gap required between consecutive rentals")
246
  with col2:
247
  scope_for_threshold = st.selectbox("๐Ÿš— Scope for Analysis", ["all", "connect"],
248
  format_func=lambda x: "All Cars" if x == "all" else "Connect Cars Only")
249
 
 
 
 
 
 
 
 
 
 
 
 
250
  # Current metrics at selected threshold
251
  current_metrics = calculate_threshold_metrics(df, threshold, scope_for_threshold)
252
 
253
  st.markdown('<div class="section-header">๐Ÿ“Š Current Impact at Selected Threshold</div>', unsafe_allow_html=True)
254
 
255
- col1, col2, col3, col4 = st.columns(4)
256
  with col1:
257
  st.markdown(f"""
258
  <div class="metric-card">
259
  <h3 style="color: #e74c3c;">{current_metrics['blocked_rentals']:,}</h3>
260
  <p>Blocked Rentals</p>
 
261
  </div>
262
  """, unsafe_allow_html=True)
263
 
@@ -266,6 +305,7 @@ if selected == "๐Ÿ• Threshold Analysis":
266
  <div class="metric-card">
267
  <h3 style="color: #f39c12;">{current_metrics['blocked_percentage']:.1f}%</h3>
268
  <p>Blocked Rate</p>
 
269
  </div>
270
  """, unsafe_allow_html=True)
271
 
@@ -274,6 +314,7 @@ if selected == "๐Ÿ• Threshold Analysis":
274
  <div class="metric-card">
275
  <h3 style="color: #27ae60;">{current_metrics['problems_solved']:,}</h3>
276
  <p>Problems Solved</p>
 
277
  </div>
278
  """, unsafe_allow_html=True)
279
 
@@ -282,6 +323,16 @@ if selected == "๐Ÿ• Threshold Analysis":
282
  <div class="metric-card">
283
  <h3 style="color: #3498db;">{current_metrics['problem_solve_rate']:.1f}%</h3>
284
  <p>Solve Efficiency</p>
 
 
 
 
 
 
 
 
 
 
285
  </div>
286
  """, unsafe_allow_html=True)
287
 
@@ -335,12 +386,14 @@ if selected == "๐Ÿ• Threshold Analysis":
335
 
336
  # Create comprehensive threshold analysis chart
337
  fig = make_subplots(
338
- rows=2, cols=2,
339
  subplot_titles=(
340
  "Blocked Rentals vs Threshold",
341
  "Problems Solved vs Threshold",
 
342
  "Efficiency (Solve Rate) vs Threshold",
343
- "Revenue Impact vs Threshold"
 
344
  )
345
  )
346
 
@@ -360,6 +413,14 @@ if selected == "๐Ÿ• Threshold Analysis":
360
  row=1, col=2
361
  )
362
 
 
 
 
 
 
 
 
 
363
  # Efficiency
364
  fig.add_trace(
365
  go.Scatter(x=sweep_df["threshold"], y=sweep_df["problem_solve_rate"],
@@ -376,9 +437,17 @@ if selected == "๐Ÿ• Threshold Analysis":
376
  row=2, col=2
377
  )
378
 
 
 
 
 
 
 
 
 
379
  # Add current threshold line to all subplots
380
  for i in range(1, 3):
381
- for j in range(1, 3):
382
  fig.add_vline(x=threshold, line_dash="dash", line_color="red",
383
  annotation_text=f"Current: {threshold}min", row=i, col=j)
384
 
@@ -386,8 +455,10 @@ if selected == "๐Ÿ• Threshold Analysis":
386
  fig.update_xaxes(title_text="Threshold (minutes)")
387
  fig.update_yaxes(title_text="Count", row=1, col=1)
388
  fig.update_yaxes(title_text="Count", row=1, col=2)
 
389
  fig.update_yaxes(title_text="Percentage (%)", row=2, col=1)
390
  fig.update_yaxes(title_text="Percentage (%)", row=2, col=2)
 
391
 
392
  st.plotly_chart(fig, use_container_width=True)
393
 
@@ -419,7 +490,7 @@ if selected == "๐Ÿ• Threshold Analysis":
419
  <h3>๐ŸŽฏ Recommended Threshold: {optimal['threshold']:.0f} minutes</h3>
420
  <p><strong>Why this threshold?</strong></p>
421
  <ul>
422
- <li>โœ… Solves <strong>{optimal['problems_solved']:.0f}</strong> problematic cases</li>
423
  <li>๐Ÿ“‰ Blocks only <strong>{optimal['blocked_percentage']:.1f}%</strong> of consecutive rentals</li>
424
  <li>โšก Achieves <strong>{optimal['problem_solve_rate']:.1f}%</strong> efficiency in problem solving</li>
425
  <li>๐Ÿ’ฐ Impacts <strong>{optimal['revenue_impact_percent']:.1f}%</strong> of total rental volume</li>
@@ -427,26 +498,9 @@ if selected == "๐Ÿ• Threshold Analysis":
427
  <p><em>This threshold balances customer satisfaction improvements with minimal impact on availability.</em></p>
428
  </div>
429
  """, unsafe_allow_html=True)
430
-
431
- # Additional insights
432
- if optimal['threshold'] > 0:
433
- st.markdown("### ๐Ÿ“ˆ Business Impact Analysis")
434
-
435
- col1, col2, col3 = st.columns(3)
436
- with col1:
437
- st.metric("Customer Experience", "Improved",
438
- help=f"Eliminates {optimal['problems_solved']:.0f} cases where next customer waits")
439
- with col2:
440
- availability_impact = f"-{optimal['blocked_percentage']:.1f}%"
441
- st.metric("Availability Impact", availability_impact,
442
- help="Reduction in bookable consecutive rental slots")
443
- with col3:
444
- efficiency_rating = "High" if optimal['problem_solve_rate'] > 50 else "Medium" if optimal['problem_solve_rate'] > 25 else "Low"
445
- st.metric("Implementation Efficiency", efficiency_rating,
446
- help=f"{optimal['problem_solve_rate']:.1f}% of blocked rentals actually solve problems")
447
 
448
- # ========== PAGE 2: SCOPE ANALYSIS ==========
449
- elif selected == "๐ŸŽฏ Scope Analysis":
450
  st.title("๐ŸŽฏ Scope Decision: All Cars vs Connect Cars Only?")
451
 
452
  st.markdown("""
@@ -475,7 +529,8 @@ elif selected == "๐ŸŽฏ Scope Analysis":
475
  <strong>Blocked Rentals:</strong> {all_metrics['blocked_rentals']:,} ({all_metrics['blocked_percentage']:.1f}%)<br>
476
  <strong>Problems Solved:</strong> {all_metrics['problems_solved']:,}<br>
477
  <strong>Efficiency:</strong> {all_metrics['problem_solve_rate']:.1f}%<br>
478
- <strong>Revenue Impact:</strong> {all_metrics['revenue_impact_percent']:.1f}%
 
479
  </div>
480
  """, unsafe_allow_html=True)
481
 
@@ -486,7 +541,8 @@ elif selected == "๐ŸŽฏ Scope Analysis":
486
  <strong>Blocked Rentals:</strong> {connect_metrics['blocked_rentals']:,} ({connect_metrics['blocked_percentage']:.1f}%)<br>
487
  <strong>Problems Solved:</strong> {connect_metrics['problems_solved']:,}<br>
488
  <strong>Efficiency:</strong> {connect_metrics['problem_solve_rate']:.1f}%<br>
489
- <strong>Revenue Impact:</strong> {connect_metrics['revenue_impact_percent']:.1f}%
 
490
  </div>
491
  """, unsafe_allow_html=True)
492
 
@@ -502,14 +558,16 @@ elif selected == "๐ŸŽฏ Scope Analysis":
502
  connect_sweep["scope"] = "Connect Only"
503
  combined_sweep = pd.concat([all_sweep, connect_sweep], ignore_index=True)
504
 
505
- # Create comparison visualizations
506
  fig = make_subplots(
507
- rows=2, cols=2,
508
  subplot_titles=(
509
  "Blocked Rentals by Scope",
510
  "Problems Solved by Scope",
 
511
  "Efficiency by Scope",
512
- "Revenue Impact by Scope"
 
513
  )
514
  )
515
 
@@ -534,6 +592,14 @@ elif selected == "๐ŸŽฏ Scope Analysis":
534
  row=1, col=2
535
  )
536
 
 
 
 
 
 
 
 
 
537
  # Efficiency
538
  fig.add_trace(
539
  go.Scatter(x=scope_data["threshold"], y=scope_data["problem_solve_rate"],
@@ -549,10 +615,18 @@ elif selected == "๐ŸŽฏ Scope Analysis":
549
  line=dict(color=colors[scope_name]), legendgroup=scope_name, showlegend=False),
550
  row=2, col=2
551
  )
 
 
 
 
 
 
 
 
552
 
553
  # Add current threshold line
554
  for i in range(1, 3):
555
- for j in range(1, 3):
556
  fig.add_vline(x=threshold_for_scope, line_dash="dash", line_color="red",
557
  annotation_text=f"Analysis: {threshold_for_scope}min", row=i, col=j)
558
 
@@ -560,8 +634,10 @@ elif selected == "๐ŸŽฏ Scope Analysis":
560
  fig.update_xaxes(title_text="Threshold (minutes)")
561
  fig.update_yaxes(title_text="Count", row=1, col=1)
562
  fig.update_yaxes(title_text="Count", row=1, col=2)
 
563
  fig.update_yaxes(title_text="Percentage (%)", row=2, col=1)
564
  fig.update_yaxes(title_text="Percentage (%)", row=2, col=2)
 
565
 
566
  st.plotly_chart(fig, use_container_width=True)
567
 
@@ -634,12 +710,13 @@ elif selected == "๐ŸŽฏ Scope Analysis":
634
  <li>๐Ÿ“‰ Blocks <strong>{rec_metrics['blocked_percentage']:.1f}%</strong> of consecutive rentals</li>
635
  <li>โšก Achieves <strong>{rec_metrics['problem_solve_rate']:.1f}%</strong> efficiency</li>
636
  <li>๐Ÿ’ฐ Impacts <strong>{rec_metrics['revenue_impact_percent']:.1f}%</strong> of total rental volume</li>
 
637
  </ul>
638
  </div>
639
  """, unsafe_allow_html=True)
640
 
641
- # ========== PAGE 3: DATA OVERVIEW ==========
642
- elif selected == "๐Ÿ“Š Data Overview":
643
  st.title("๐Ÿ“Š Dataset Overview & Exploratory Analysis")
644
 
645
  st.markdown('<div class="section-header">๐Ÿ“ˆ Dataset Summary</div>', unsafe_allow_html=True)
@@ -651,9 +728,11 @@ elif selected == "๐Ÿ“Š Data Overview":
651
  with col2:
652
  st.metric("Connect Rentals", f"{len(df[df['checkin_type'].str.lower() == 'connect']):,}")
653
  with col3:
654
- st.metric("Mobile Rentals", f"{len(df[df['checkin_type'].str.lower() == 'mobile']):,}")
 
655
  with col4:
656
- st.metric("With Previous Rental", f"{df['has_previous_rental'].sum():,}")
 
657
 
658
  # Checkin type distribution
659
  st.markdown('<div class="section-header">๐Ÿš— Rental Type Distribution</div>', unsafe_allow_html=True)
@@ -688,22 +767,27 @@ elif selected == "๐Ÿ“Š Data Overview":
688
  with col1:
689
  # Delay status distribution
690
  df["delay_status"] = df["delay_at_checkout_in_minutes"].apply(
691
- lambda x: "Early" if pd.notnull(x) and x < 0 else
692
  "On Time" if pd.notnull(x) and x == 0 else
693
- "Late" if pd.notnull(x) and x > 0 else "No Data"
694
  )
695
 
696
  delay_counts = df["delay_status"].value_counts()
697
  fig_delay_status = px.pie(
698
  values=delay_counts.values,
699
  names=delay_counts.index,
700
- title="Delay Status Distribution",
701
  color_discrete_sequence=px.colors.qualitative.Bold
702
  )
 
 
 
 
 
703
  st.plotly_chart(fig_delay_status, use_container_width=True)
704
 
705
  with col2:
706
- # Delay distribution histogram
707
  delay_data = df[df["delay_at_checkout_in_minutes"].notnull()]
708
  delay_filtered = delay_data[delay_data["delay_at_checkout_in_minutes"].between(-120, 300)]
709
 
@@ -711,10 +795,14 @@ elif selected == "๐Ÿ“Š Data Overview":
711
  delay_filtered,
712
  x="delay_at_checkout_in_minutes",
713
  nbins=50,
714
- title="Delay Distribution (-120 to 300 minutes)",
715
- labels={"delay_at_checkout_in_minutes": "Delay (minutes)", "count": "Number of Rentals"}
716
  )
717
  fig_delay_hist.add_vline(x=0, line_dash="dash", line_color="red", annotation_text="On Time")
 
 
 
 
718
  st.plotly_chart(fig_delay_hist, use_container_width=True)
719
 
720
  # Gap analysis
@@ -747,8 +835,67 @@ elif selected == "๐Ÿ“Š Data Overview":
747
  )
748
  st.plotly_chart(fig_gap_box, use_container_width=True)
749
 
750
- # Problem cases analysis
751
- st.markdown('<div class="section-header">๐Ÿšจ Current Problem Cases</div>', unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
752
 
753
  # Calculate problem cases
754
  df_problems = df[df["has_previous_rental"]].copy()
@@ -763,15 +910,23 @@ elif selected == "๐Ÿ“Š Data Overview":
763
 
764
  problem_cases = df_problems[df_problems["causes_problem"]]
765
 
766
- col1, col2, col3 = st.columns(3)
767
  with col1:
768
- st.metric("Total Problem Cases", f"{len(problem_cases):,}")
 
769
  with col2:
770
  problem_rate = (len(problem_cases) / len(df_problems)) * 100 if len(df_problems) > 0 else 0
771
- st.metric("Problem Rate", f"{problem_rate:.1f}%")
 
772
  with col3:
773
  avg_wait = problem_cases["wait_time"].mean() if len(problem_cases) > 0 else 0
774
- st.metric("Avg Wait Time", f"{avg_wait:.1f} min")
 
 
 
 
 
 
775
 
776
  if len(problem_cases) > 0:
777
  col1, col2 = st.columns(2)
@@ -798,61 +953,6 @@ elif selected == "๐Ÿ“Š Data Overview":
798
  )
799
  st.plotly_chart(fig_problems_type, use_container_width=True)
800
 
801
- # Data quality insights
802
- st.markdown('<div class="section-header">๐Ÿ” Data Quality Insights</div>', unsafe_allow_html=True)
803
-
804
- col1, col2 = st.columns(2)
805
-
806
- with col1:
807
- st.markdown("### Missing Data Analysis")
808
- missing_data = df.isnull().sum()
809
- missing_pct = (missing_data / len(df) * 100).round(1)
810
- missing_df = pd.DataFrame({
811
- 'Column': missing_data.index,
812
- 'Missing Count': missing_data.values,
813
- 'Missing %': missing_pct.values
814
- }).sort_values('Missing %', ascending=False)
815
-
816
- fig_missing = px.bar(
817
- missing_df.head(10),
818
- x='Missing %',
819
- y='Column',
820
- orientation='h',
821
- title="Missing Data by Column (Top 10)"
822
- )
823
- st.plotly_chart(fig_missing, use_container_width=True)
824
-
825
- with col2:
826
- st.markdown("### Key Statistics")
827
-
828
- stats_data = {
829
- 'Metric': [
830
- 'Total Rentals',
831
- 'Rentals with Previous',
832
- 'Connect Rentals',
833
- 'Mobile Rentals',
834
- 'Cancelled Rentals',
835
- 'Rentals with Delay Data',
836
- 'Problem Cases',
837
- 'Average Gap (min)',
838
- 'Average Delay (min)'
839
- ],
840
- 'Value': [
841
- f"{len(df):,}",
842
- f"{df['has_previous_rental'].sum():,}",
843
- f"{len(df[df['checkin_type'].str.lower() == 'connect']):,}",
844
- f"{len(df[df['checkin_type'].str.lower() == 'mobile']):,}",
845
- f"{len(df[df['state'] == 'canceled']):,}",
846
- f"{df['delay_at_checkout_in_minutes'].count():,}",
847
- f"{len(problem_cases):,}",
848
- f"{df[df['has_previous_rental']]['time_delta_with_previous_rental_in_minutes'].mean():.1f}",
849
- f"{df['clean_delay'].mean():.1f}"
850
- ]
851
- }
852
-
853
- stats_df = pd.DataFrame(stats_data)
854
- st.dataframe(stats_df, use_container_width=True, hide_index=True)
855
-
856
  # Raw data sample
857
  with st.expander("๐Ÿ“‹ Raw Data Sample"):
858
  st.markdown("### First 100 rows of the dataset:")
 
 
1
  import streamlit as st
2
  import pandas as pd
3
  import numpy as np
 
120
  "problems_solved": 0,
121
  "problem_solve_rate": 0.0,
122
  "avg_wait_time": 0.0,
123
+ "revenue_impact_percent": 0.0,
124
+ "current_cancellations": 0,
125
+ "cancellations_prevented": 0,
126
+ "cancellation_rate": 0.0
127
  }
128
 
129
  # Calculate blocked rentals (gap < threshold)
 
145
  # Problems solved by blocking
146
  df_with_prev["problem_solved"] = df_with_prev["causes_problem"] & df_with_prev["would_be_blocked"]
147
 
148
+ # Cancellation analysis
149
+ df_with_prev["is_cancelled"] = df_with_prev["state"] == "canceled"
150
+
151
+ # Potential cancellations due to delays (cancelled rentals where previous rental caused problems)
152
+ df_with_prev["likely_cancelled_due_to_delay"] = (
153
+ df_with_prev["is_cancelled"] & df_with_prev["causes_problem"]
154
+ )
155
+
156
+ # Cancellations that would be prevented by threshold
157
+ df_with_prev["cancellation_prevented"] = (
158
+ df_with_prev["likely_cancelled_due_to_delay"] & df_with_prev["would_be_blocked"]
159
+ )
160
+
161
  # Calculate final metrics
162
  total_rentals = len(df_filtered)
163
  rentals_with_previous = len(df_with_prev)
 
169
  avg_wait_time = df_with_prev[df_with_prev["causes_problem"]]["wait_time_next_driver"].mean()
170
  revenue_impact_percent = (blocked_rentals / total_rentals) * 100 if total_rentals > 0 else 0
171
 
172
+ # Cancellation metrics
173
+ current_cancellations = int(df_with_prev["likely_cancelled_due_to_delay"].sum())
174
+ cancellations_prevented = int(df_with_prev["cancellation_prevented"].sum())
175
+ cancellation_rate = (current_cancellations / rentals_with_previous) * 100 if rentals_with_previous > 0 else 0
176
+
177
  return {
178
  "total_rentals": total_rentals,
179
  "rentals_with_previous": rentals_with_previous,
 
183
  "problems_solved": problems_solved,
184
  "problem_solve_rate": problem_solve_rate,
185
  "avg_wait_time": avg_wait_time if not pd.isna(avg_wait_time) else 0,
186
+ "revenue_impact_percent": revenue_impact_percent,
187
+ "current_cancellations": current_cancellations,
188
+ "cancellations_prevented": cancellations_prevented,
189
+ "cancellation_rate": cancellation_rate
190
  }
191
 
192
  def create_threshold_sweep(df, thresholds, scope="all"):
 
197
  results.append({"threshold": threshold, **metrics})
198
  return pd.DataFrame(results)
199
 
200
+ def find_optimal_threshold(sweep_df, max_blocked_rate=20):
201
  """Find optimal threshold using business logic"""
202
  # Only consider thresholds that actually solve problems (> 0 problems solved)
203
  viable = sweep_df[
204
  (sweep_df["problems_solved"] > 0) &
205
+ (sweep_df["blocked_percentage"] <= max_blocked_rate) &
206
+ (sweep_df["threshold"] <= 180) # Don't recommend unreasonably long thresholds
207
  ]
208
 
209
  if len(viable) == 0:
210
  # If no threshold meets the blocked rate criteria, find the one with best ratio
211
+ viable = sweep_df[
212
+ (sweep_df["problems_solved"] > 0) &
213
+ (sweep_df["threshold"] <= 180)
214
+ ]
215
  if len(viable) == 0:
216
+ # If no problems can be solved, return a reasonable threshold
217
+ return sweep_df[sweep_df["threshold"] == 60].iloc[0]
218
 
219
  # Calculate efficiency and optimize
220
  viable["efficiency"] = viable["problems_solved"] / viable["blocked_rentals"].replace({0: np.nan})
 
239
  st.markdown("## ๐ŸŽ›๏ธ Navigation")
240
  selected = option_menu(
241
  "Analysis Sections",
242
+ ["๐Ÿ“Š Data Overview", "๐Ÿ• Threshold Analysis", "๐ŸŽฏ Scope Analysis"],
243
+ icons=["bar-chart", "clock", "target"],
244
  menu_icon="cast",
245
  default_index=0,
246
  )
 
249
  st.markdown("### ๐Ÿ“ˆ Dataset Info")
250
  st.info(f"""
251
  **Total Rentals:** {len(df):,}
252
+ **With Previous Rental:** {df['has_previous_rental'].sum():,} *(rentals that had another rental on the same car before)*
253
  **Connect Rentals:** {len(df[df['checkin_type'].str.lower() == 'connect']):,}
254
  **Mobile Rentals:** {len(df[df['checkin_type'].str.lower() == 'mobile']):,}
255
  """)
256
 
257
+ # ========== PAGE 1: DATA OVERVIEW ==========
258
+ if selected == "๐Ÿ“Š Data Overview":
259
  st.title("โฐ Threshold Decision: How Long Should the Minimum Delay Be?")
260
 
261
  st.markdown("""
 
268
  # Controls
269
  col1, col2 = st.columns(2)
270
  with col1:
271
+ threshold = st.slider("๐Ÿ• Threshold (minutes)", 0, 300, 60, step=15,
272
  help="Minimum time gap required between consecutive rentals")
273
  with col2:
274
  scope_for_threshold = st.selectbox("๐Ÿš— Scope for Analysis", ["all", "connect"],
275
  format_func=lambda x: "All Cars" if x == "all" else "Connect Cars Only")
276
 
277
+ st.markdown("""
278
+ <div class="insight-box">
279
+ <strong>How the Metrics Work:</strong><br>
280
+ โ€ข <strong>Blocked Rentals:</strong> Number of current consecutive rentals where gap < threshold (these would be prevented)<br>
281
+ โ€ข <strong>Blocked Rate:</strong> % of consecutive rentals that would be blocked = blocked_rentals / rentals_with_previous<br>
282
+ โ€ข <strong>Problems Solved:</strong> Current problem cases that would be prevented = problems that occur when gap < threshold<br>
283
+ โ€ข <strong>Solve Efficiency:</strong> % of blocked rentals that actually solve a problem = problems_solved / blocked_rentals<br>
284
+ โ€ข <strong>Cancellations Prevented:</strong> Delay-related cancellations that would be avoided with this threshold
285
+ </div>
286
+ """, unsafe_allow_html=True)
287
+
288
  # Current metrics at selected threshold
289
  current_metrics = calculate_threshold_metrics(df, threshold, scope_for_threshold)
290
 
291
  st.markdown('<div class="section-header">๐Ÿ“Š Current Impact at Selected Threshold</div>', unsafe_allow_html=True)
292
 
293
+ col1, col2, col3, col4, col5 = st.columns(5)
294
  with col1:
295
  st.markdown(f"""
296
  <div class="metric-card">
297
  <h3 style="color: #e74c3c;">{current_metrics['blocked_rentals']:,}</h3>
298
  <p>Blocked Rentals</p>
299
+ <small>Consecutive rentals with gap < {threshold} min</small>
300
  </div>
301
  """, unsafe_allow_html=True)
302
 
 
305
  <div class="metric-card">
306
  <h3 style="color: #f39c12;">{current_metrics['blocked_percentage']:.1f}%</h3>
307
  <p>Blocked Rate</p>
308
+ <small>{current_metrics['blocked_rentals']:,} / {current_metrics['rentals_with_previous']:,} consecutive rentals</small>
309
  </div>
310
  """, unsafe_allow_html=True)
311
 
 
314
  <div class="metric-card">
315
  <h3 style="color: #27ae60;">{current_metrics['problems_solved']:,}</h3>
316
  <p>Problems Solved</p>
317
+ <small>Current problems prevented by this threshold</small>
318
  </div>
319
  """, unsafe_allow_html=True)
320
 
 
323
  <div class="metric-card">
324
  <h3 style="color: #3498db;">{current_metrics['problem_solve_rate']:.1f}%</h3>
325
  <p>Solve Efficiency</p>
326
+ <small>{current_metrics['problems_solved']:,} / {current_metrics['blocked_rentals']:,} blocked rentals solve problems</small>
327
+ </div>
328
+ """, unsafe_allow_html=True)
329
+
330
+ with col5:
331
+ st.markdown(f"""
332
+ <div class="metric-card">
333
+ <h3 style="color: #9b59b6;">{current_metrics['cancellations_prevented']:,}</h3>
334
+ <p>Cancellations Prevented</p>
335
+ <small>Delay-related cancellations avoided</small>
336
  </div>
337
  """, unsafe_allow_html=True)
338
 
 
386
 
387
  # Create comprehensive threshold analysis chart
388
  fig = make_subplots(
389
+ rows=2, cols=3,
390
  subplot_titles=(
391
  "Blocked Rentals vs Threshold",
392
  "Problems Solved vs Threshold",
393
+ "Cancellations Prevented vs Threshold",
394
  "Efficiency (Solve Rate) vs Threshold",
395
+ "Revenue Impact vs Threshold",
396
+ "Cancellation Rate vs Threshold"
397
  )
398
  )
399
 
 
413
  row=1, col=2
414
  )
415
 
416
+ # Cancellations prevented
417
+ fig.add_trace(
418
+ go.Scatter(x=sweep_df["threshold"], y=sweep_df["cancellations_prevented"],
419
+ mode="lines+markers", name="Cancellations Prevented",
420
+ line=dict(color="#9b59b6"), showlegend=False),
421
+ row=1, col=3
422
+ )
423
+
424
  # Efficiency
425
  fig.add_trace(
426
  go.Scatter(x=sweep_df["threshold"], y=sweep_df["problem_solve_rate"],
 
437
  row=2, col=2
438
  )
439
 
440
+ # Cancellation rate
441
+ fig.add_trace(
442
+ go.Scatter(x=sweep_df["threshold"], y=sweep_df["cancellation_rate"],
443
+ mode="lines+markers", name="Cancellation Rate (%)",
444
+ line=dict(color="#e67e22"), showlegend=False),
445
+ row=2, col=3
446
+ )
447
+
448
  # Add current threshold line to all subplots
449
  for i in range(1, 3):
450
+ for j in range(1, 4):
451
  fig.add_vline(x=threshold, line_dash="dash", line_color="red",
452
  annotation_text=f"Current: {threshold}min", row=i, col=j)
453
 
 
455
  fig.update_xaxes(title_text="Threshold (minutes)")
456
  fig.update_yaxes(title_text="Count", row=1, col=1)
457
  fig.update_yaxes(title_text="Count", row=1, col=2)
458
+ fig.update_yaxes(title_text="Count", row=1, col=3)
459
  fig.update_yaxes(title_text="Percentage (%)", row=2, col=1)
460
  fig.update_yaxes(title_text="Percentage (%)", row=2, col=2)
461
+ fig.update_yaxes(title_text="Percentage (%)", row=2, col=3)
462
 
463
  st.plotly_chart(fig, use_container_width=True)
464
 
 
490
  <h3>๐ŸŽฏ Recommended Threshold: {optimal['threshold']:.0f} minutes</h3>
491
  <p><strong>Why this threshold?</strong></p>
492
  <ul>
493
+ <li>โœ… Solves <strong>{optimal['problems_solved']:.0f}</strong> problematic cases (current waiting situations)</li>
494
  <li>๐Ÿ“‰ Blocks only <strong>{optimal['blocked_percentage']:.1f}%</strong> of consecutive rentals</li>
495
  <li>โšก Achieves <strong>{optimal['problem_solve_rate']:.1f}%</strong> efficiency in problem solving</li>
496
  <li>๐Ÿ’ฐ Impacts <strong>{optimal['revenue_impact_percent']:.1f}%</strong> of total rental volume</li>
 
498
  <p><em>This threshold balances customer satisfaction improvements with minimal impact on availability.</em></p>
499
  </div>
500
  """, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
 
502
+ # ========== PAGE 2: THRESHOLD ANALYSIS ==========
503
+ elif selected == "๐Ÿ• Threshold Analysis":
504
  st.title("๐ŸŽฏ Scope Decision: All Cars vs Connect Cars Only?")
505
 
506
  st.markdown("""
 
529
  <strong>Blocked Rentals:</strong> {all_metrics['blocked_rentals']:,} ({all_metrics['blocked_percentage']:.1f}%)<br>
530
  <strong>Problems Solved:</strong> {all_metrics['problems_solved']:,}<br>
531
  <strong>Efficiency:</strong> {all_metrics['problem_solve_rate']:.1f}%<br>
532
+ <strong>Revenue Impact:</strong> {all_metrics['revenue_impact_percent']:.1f}%<br>
533
+ <strong>Cancellations Prevented:</strong> {all_metrics['cancellations_prevented']:,}
534
  </div>
535
  """, unsafe_allow_html=True)
536
 
 
541
  <strong>Blocked Rentals:</strong> {connect_metrics['blocked_rentals']:,} ({connect_metrics['blocked_percentage']:.1f}%)<br>
542
  <strong>Problems Solved:</strong> {connect_metrics['problems_solved']:,}<br>
543
  <strong>Efficiency:</strong> {connect_metrics['problem_solve_rate']:.1f}%<br>
544
+ <strong>Revenue Impact:</strong> {connect_metrics['revenue_impact_percent']:.1f}%<br>
545
+ <strong>Cancellations Prevented:</strong> {connect_metrics['cancellations_prevented']:,}
546
  </div>
547
  """, unsafe_allow_html=True)
548
 
 
558
  connect_sweep["scope"] = "Connect Only"
559
  combined_sweep = pd.concat([all_sweep, connect_sweep], ignore_index=True)
560
 
561
+ # Create comparison visualizations (with cancellations)
562
  fig = make_subplots(
563
+ rows=2, cols=3,
564
  subplot_titles=(
565
  "Blocked Rentals by Scope",
566
  "Problems Solved by Scope",
567
+ "Cancellations Prevented by Scope",
568
  "Efficiency by Scope",
569
+ "Revenue Impact by Scope",
570
+ "Cancellation Rate by Scope"
571
  )
572
  )
573
 
 
592
  row=1, col=2
593
  )
594
 
595
+ # Cancellations prevented
596
+ fig.add_trace(
597
+ go.Scatter(x=scope_data["threshold"], y=scope_data["cancellations_prevented"],
598
+ mode="lines+markers", name=f"{scope_name}",
599
+ line=dict(color=colors[scope_name]), legendgroup=scope_name, showlegend=False),
600
+ row=1, col=3
601
+ )
602
+
603
  # Efficiency
604
  fig.add_trace(
605
  go.Scatter(x=scope_data["threshold"], y=scope_data["problem_solve_rate"],
 
615
  line=dict(color=colors[scope_name]), legendgroup=scope_name, showlegend=False),
616
  row=2, col=2
617
  )
618
+
619
+ # Cancellation rate
620
+ fig.add_trace(
621
+ go.Scatter(x=scope_data["threshold"], y=scope_data["cancellation_rate"],
622
+ mode="lines+markers", name=f"{scope_name}",
623
+ line=dict(color=colors[scope_name]), legendgroup=scope_name, showlegend=False),
624
+ row=2, col=3
625
+ )
626
 
627
  # Add current threshold line
628
  for i in range(1, 3):
629
+ for j in range(1, 4):
630
  fig.add_vline(x=threshold_for_scope, line_dash="dash", line_color="red",
631
  annotation_text=f"Analysis: {threshold_for_scope}min", row=i, col=j)
632
 
 
634
  fig.update_xaxes(title_text="Threshold (minutes)")
635
  fig.update_yaxes(title_text="Count", row=1, col=1)
636
  fig.update_yaxes(title_text="Count", row=1, col=2)
637
+ fig.update_yaxes(title_text="Count", row=1, col=3)
638
  fig.update_yaxes(title_text="Percentage (%)", row=2, col=1)
639
  fig.update_yaxes(title_text="Percentage (%)", row=2, col=2)
640
+ fig.update_yaxes(title_text="Percentage (%)", row=2, col=3)
641
 
642
  st.plotly_chart(fig, use_container_width=True)
643
 
 
710
  <li>๐Ÿ“‰ Blocks <strong>{rec_metrics['blocked_percentage']:.1f}%</strong> of consecutive rentals</li>
711
  <li>โšก Achieves <strong>{rec_metrics['problem_solve_rate']:.1f}%</strong> efficiency</li>
712
  <li>๐Ÿ’ฐ Impacts <strong>{rec_metrics['revenue_impact_percent']:.1f}%</strong> of total rental volume</li>
713
+ <li>โŒ Prevents <strong>{rec_metrics['cancellations_prevented']:,}</strong> delay-related cancellations</li>
714
  </ul>
715
  </div>
716
  """, unsafe_allow_html=True)
717
 
718
+ # ========== PAGE 3: SCOPE ANALYSIS ==========
719
+ elif selected == "๐ŸŽฏ Scope Analysis":
720
  st.title("๐Ÿ“Š Dataset Overview & Exploratory Analysis")
721
 
722
  st.markdown('<div class="section-header">๐Ÿ“ˆ Dataset Summary</div>', unsafe_allow_html=True)
 
728
  with col2:
729
  st.metric("Connect Rentals", f"{len(df[df['checkin_type'].str.lower() == 'connect']):,}")
730
  with col3:
731
+ st.metric("Cancelled Rentals", f"{len(df[df['state'] == 'canceled']):,}",
732
+ help="Total cancelled rentals in dataset")
733
  with col4:
734
+ st.metric("With Previous Rental", f"{df['has_previous_rental'].sum():,}",
735
+ help="Rentals that had another rental on the same car before (within 12 hours)")
736
 
737
  # Checkin type distribution
738
  st.markdown('<div class="section-header">๐Ÿš— Rental Type Distribution</div>', unsafe_allow_html=True)
 
767
  with col1:
768
  # Delay status distribution
769
  df["delay_status"] = df["delay_at_checkout_in_minutes"].apply(
770
+ lambda x: "Early Return" if pd.notnull(x) and x < 0 else
771
  "On Time" if pd.notnull(x) and x == 0 else
772
+ "Late Return" if pd.notnull(x) and x > 0 else "Missing Data"
773
  )
774
 
775
  delay_counts = df["delay_status"].value_counts()
776
  fig_delay_status = px.pie(
777
  values=delay_counts.values,
778
  names=delay_counts.index,
779
+ title="Return Status Distribution",
780
  color_discrete_sequence=px.colors.qualitative.Bold
781
  )
782
+ fig_delay_status.update_layout(
783
+ annotations=[dict(text="Missing Data = no checkout delay recorded",
784
+ x=0.5, y=-0.1, xref="paper", yref="paper",
785
+ showarrow=False, font=dict(size=10))]
786
+ )
787
  st.plotly_chart(fig_delay_status, use_container_width=True)
788
 
789
  with col2:
790
+ # Delay distribution histogram (starting from 0, showing early returns clearly)
791
  delay_data = df[df["delay_at_checkout_in_minutes"].notnull()]
792
  delay_filtered = delay_data[delay_data["delay_at_checkout_in_minutes"].between(-120, 300)]
793
 
 
795
  delay_filtered,
796
  x="delay_at_checkout_in_minutes",
797
  nbins=50,
798
+ title="Checkout Delay Distribution",
799
+ labels={"delay_at_checkout_in_minutes": "Minutes (Negative = Early Return)", "count": "Number of Rentals"}
800
  )
801
  fig_delay_hist.add_vline(x=0, line_dash="dash", line_color="red", annotation_text="On Time")
802
+ fig_delay_hist.add_annotation(x=-60, y=delay_filtered.shape[0]*0.1, text="Early Returns",
803
+ showarrow=True, arrowhead=2, arrowcolor="blue")
804
+ fig_delay_hist.add_annotation(x=120, y=delay_filtered.shape[0]*0.1, text="Late Returns",
805
+ showarrow=True, arrowhead=2, arrowcolor="red")
806
  st.plotly_chart(fig_delay_hist, use_container_width=True)
807
 
808
  # Gap analysis
 
835
  )
836
  st.plotly_chart(fig_gap_box, use_container_width=True)
837
 
838
+ # Cancellation analysis
839
+ st.markdown('<div class="section-header">โŒ Cancellation Analysis</div>', unsafe_allow_html=True)
840
+
841
+ col1, col2 = st.columns(2)
842
+
843
+ with col1:
844
+ st.markdown("### Cancellation by Type")
845
+
846
+ cancellation_by_type = df.groupby(['checkin_type', 'state']).size().unstack(fill_value=0)
847
+ if 'canceled' in cancellation_by_type.columns:
848
+ cancel_rates = (cancellation_by_type['canceled'] / cancellation_by_type.sum(axis=1) * 100).round(1)
849
+
850
+ fig_cancel_type = px.bar(
851
+ x=cancel_rates.index,
852
+ y=cancel_rates.values,
853
+ title="Cancellation Rate by Checkin Type",
854
+ labels={"x": "Checkin Type", "y": "Cancellation Rate (%)"}
855
+ )
856
+ st.plotly_chart(fig_cancel_type, use_container_width=True)
857
+ else:
858
+ st.info("No cancellations found in dataset")
859
+
860
+ with col2:
861
+ st.markdown("### Delay-Related Cancellations")
862
+
863
+ # Calculate potential delay-related cancellations
864
+ df_analysis = df[df["has_previous_rental"]].copy()
865
+ df_analysis["causes_problem"] = (
866
+ df_analysis["delay_at_checkout_in_minutes"].notnull() &
867
+ (df_analysis["clean_delay"] > df_analysis["time_delta_with_previous_rental_in_minutes"])
868
+ )
869
+ df_analysis["likely_cancelled_due_to_delay"] = (
870
+ (df_analysis["state"] == "canceled") & df_analysis["causes_problem"]
871
+ )
872
+
873
+ delay_related_cancels = df_analysis["likely_cancelled_due_to_delay"].sum()
874
+ total_cancels = (df_analysis["state"] == "canceled").sum()
875
+
876
+ col2_1, col2_2 = st.columns(2)
877
+ with col2_1:
878
+ st.metric("Total Cancellations", f"{total_cancels:,}")
879
+ with col2_2:
880
+ st.metric("Likely Delay-Related", f"{delay_related_cancels:,}",
881
+ help="Cancelled rentals where previous rental caused waiting")
882
+
883
+ if total_cancels > 0:
884
+ delay_cancel_rate = (delay_related_cancels / total_cancels * 100)
885
+ st.metric("% of Cancellations Due to Delays", f"{delay_cancel_rate:.1f}%")
886
+ # Problem cases analysis (including cancellations)
887
+ st.markdown('<div class="section-header">๐Ÿšจ Current Problem Cases Analysis</div>', unsafe_allow_html=True)
888
+
889
+ st.markdown("""
890
+ <div class="insight-box">
891
+ <strong>What is a "Problem Case"?</strong><br>
892
+ A problem occurs when a driver returns a car late AND this delay is longer than the planned gap to the next rental.
893
+ This forces the next customer to either wait beyond their scheduled pickup time or cancel their rental.
894
+ <br><br>
895
+ <strong>Formula:</strong> Problem = (checkout_delay > gap_to_next_rental) AND (checkout_delay > 0)<br>
896
+ <strong>Cancellation Impact:</strong> Some of these problems result in cancellations when waiting becomes unacceptable.
897
+ </div>
898
+ """, unsafe_allow_html=True)
899
 
900
  # Calculate problem cases
901
  df_problems = df[df["has_previous_rental"]].copy()
 
910
 
911
  problem_cases = df_problems[df_problems["causes_problem"]]
912
 
913
+ col1, col2, col3, col4 = st.columns(4)
914
  with col1:
915
+ st.metric("Total Problem Cases", f"{len(problem_cases):,}",
916
+ help="Number of rentals where late return affected the next customer")
917
  with col2:
918
  problem_rate = (len(problem_cases) / len(df_problems)) * 100 if len(df_problems) > 0 else 0
919
+ st.metric("Problem Rate", f"{problem_rate:.1f}%",
920
+ help="Percentage of consecutive rentals that create waiting problems")
921
  with col3:
922
  avg_wait = problem_cases["wait_time"].mean() if len(problem_cases) > 0 else 0
923
+ st.metric("Avg Wait Time", f"{avg_wait:.1f} min",
924
+ help="Average extra wait time for affected next customers (checkout_delay - gap)")
925
+ with col4:
926
+ # Add cancellation metric
927
+ delay_cancels = problem_cases[problem_cases["state"] == "canceled"]
928
+ st.metric("Resulting Cancellations", f"{len(delay_cancels):,}",
929
+ help="Problem cases that resulted in cancellations")
930
 
931
  if len(problem_cases) > 0:
932
  col1, col2 = st.columns(2)
 
953
  )
954
  st.plotly_chart(fig_problems_type, use_container_width=True)
955
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
956
  # Raw data sample
957
  with st.expander("๐Ÿ“‹ Raw Data Sample"):
958
  st.markdown("### First 100 rows of the dataset:")