sony9316 commited on
Commit
9f55fd3
·
verified ·
1 Parent(s): a3dec8c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +344 -270
app.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import streamlit as st
2
  import pandas as pd
3
  import numpy as np
@@ -126,36 +127,48 @@ def calculate_threshold_metrics(df, threshold_minutes, scope="all"):
126
  "cancellation_rate": 0.0
127
  }
128
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  # Calculate blocked rentals (gap < threshold)
130
  df_with_prev["would_be_blocked"] = df_with_prev["time_delta_with_previous_rental_in_minutes"] < threshold_minutes
131
 
132
- # Calculate current problems (delay > gap)
133
- df_with_prev["has_delay_data"] = df_with_prev["delay_at_checkout_in_minutes"].notnull()
134
  df_with_prev["causes_problem"] = (
135
- df_with_prev["has_delay_data"] &
136
- (df_with_prev["clean_delay"] > df_with_prev["time_delta_with_previous_rental_in_minutes"])
137
  )
138
 
139
- # Wait time calculation
140
  df_with_prev["wait_time_next_driver"] = np.maximum(
141
  0,
142
- df_with_prev["clean_delay"] - df_with_prev["time_delta_with_previous_rental_in_minutes"]
143
  ).fillna(0)
144
 
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
@@ -169,8 +182,8 @@ def calculate_threshold_metrics(df, threshold_minutes, scope="all"):
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
 
@@ -256,6 +269,287 @@ with st.sidebar:
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("""
@@ -346,13 +640,28 @@ if selected == "📊 Data Overview":
346
 
347
  # Analysis of current problems
348
  df_analysis = df[df["has_previous_rental"]].copy()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  df_analysis["causes_problem"] = (
350
- df_analysis["delay_at_checkout_in_minutes"].notnull() &
351
- (df_analysis["clean_delay"] > df_analysis["time_delta_with_previous_rental_in_minutes"])
352
  )
353
  df_analysis["wait_time"] = np.maximum(
354
  0,
355
- df_analysis["clean_delay"] - df_analysis["time_delta_with_previous_rental_in_minutes"]
356
  ).fillna(0)
357
 
358
  problematic_cases = df_analysis[df_analysis["causes_problem"]]
@@ -494,13 +803,14 @@ if selected == "📊 Data Overview":
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>
 
497
  </ul>
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("""
@@ -650,9 +960,24 @@ elif selected == "🕐 Threshold Analysis":
650
  st.markdown("### Current Problems by Type")
651
 
652
  df_problems = df[df["has_previous_rental"]].copy()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
653
  df_problems["causes_problem"] = (
654
- df_problems["delay_at_checkout_in_minutes"].notnull() &
655
- (df_problems["clean_delay"] > df_problems["time_delta_with_previous_rental_in_minutes"])
656
  )
657
 
658
  problem_by_type = df_problems.groupby("checkin_type")["causes_problem"].agg(["sum", "count"]).reset_index()
@@ -715,257 +1040,6 @@ elif selected == "🕐 Threshold Analysis":
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)
723
-
724
- # Basic statistics
725
- col1, col2, col3, col4 = st.columns(4)
726
- with col1:
727
- st.metric("Total Rentals", f"{len(df):,}")
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)
739
-
740
- col1, col2 = st.columns(2)
741
-
742
- with col1:
743
- checkin_counts = df["checkin_type"].value_counts()
744
- fig_checkin = px.pie(
745
- values=checkin_counts.values,
746
- names=checkin_counts.index,
747
- title="Distribution by Checkin Type",
748
- color_discrete_sequence=px.colors.qualitative.Set3
749
- )
750
- st.plotly_chart(fig_checkin, use_container_width=True)
751
-
752
- with col2:
753
- state_counts = df["state"].value_counts()
754
- fig_state = px.pie(
755
- values=state_counts.values,
756
- names=state_counts.index,
757
- title="Distribution by Rental State",
758
- color_discrete_sequence=px.colors.qualitative.Pastel
759
- )
760
- st.plotly_chart(fig_state, use_container_width=True)
761
-
762
- # Delay analysis
763
- st.markdown('<div class="section-header">⏰ Delay Analysis</div>', unsafe_allow_html=True)
764
-
765
- col1, col2 = st.columns(2)
766
-
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
-
794
- fig_delay_hist = px.histogram(
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
809
- st.markdown('<div class="section-header">📏 Gap Between Rentals Analysis</div>', unsafe_allow_html=True)
810
-
811
- col1, col2 = st.columns(2)
812
-
813
- with col1:
814
- # Gap distribution
815
- gap_data = df[df["has_previous_rental"]]
816
- gap_filtered = gap_data[gap_data["time_delta_with_previous_rental_in_minutes"].between(0, 720)]
817
-
818
- fig_gap_hist = px.histogram(
819
- gap_filtered,
820
- x="time_delta_with_previous_rental_in_minutes",
821
- nbins=40,
822
- title="Gap Distribution (0 to 720 minutes)",
823
- labels={"time_delta_with_previous_rental_in_minutes": "Gap (minutes)", "count": "Number of Rentals"}
824
- )
825
- st.plotly_chart(fig_gap_hist, use_container_width=True)
826
-
827
- with col2:
828
- # Gap by checkin type
829
- fig_gap_box = px.box(
830
- gap_filtered,
831
- x="checkin_type",
832
- y="time_delta_with_previous_rental_in_minutes",
833
- title="Gap Distribution by Checkin Type",
834
- labels={"time_delta_with_previous_rental_in_minutes": "Gap (minutes)"}
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()
902
- df_problems["causes_problem"] = (
903
- df_problems["delay_at_checkout_in_minutes"].notnull() &
904
- (df_problems["clean_delay"] > df_problems["time_delta_with_previous_rental_in_minutes"])
905
- )
906
- df_problems["wait_time"] = np.maximum(
907
- 0,
908
- df_problems["clean_delay"] - df_problems["time_delta_with_previous_rental_in_minutes"]
909
- ).fillna(0)
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)
933
-
934
- with col1:
935
- # Wait time distribution
936
- fig_wait = px.histogram(
937
- problem_cases[problem_cases["wait_time"] > 0],
938
- x="wait_time",
939
- nbins=30,
940
- title="Wait Time Distribution for Problem Cases",
941
- labels={"wait_time": "Wait Time (minutes)", "count": "Number of Cases"}
942
- )
943
- st.plotly_chart(fig_wait, use_container_width=True)
944
-
945
- with col2:
946
- # Problems by checkin type
947
- problem_by_type = problem_cases["checkin_type"].value_counts()
948
- fig_problems_type = px.bar(
949
- x=problem_by_type.index,
950
- y=problem_by_type.values,
951
- title="Problem Cases by Checkin Type",
952
- labels={"x": "Checkin Type", "y": "Number of Problems"}
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:")
959
- st.dataframe(df.head(100), use_container_width=True)
960
-
961
- st.markdown("### Dataset Info:")
962
- buffer = st.empty()
963
- with buffer.container():
964
- st.text("Dataset shape: " + str(df.shape))
965
- st.text("Columns: " + str(list(df.columns)))
966
- st.text("Data types:")
967
- st.text(str(df.dtypes))
968
-
969
  # ========== FOOTER ==========
970
  st.markdown("---")
971
  st.markdown("""
 
1
+ # app.py
2
  import streamlit as st
3
  import pandas as pd
4
  import numpy as np
 
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
 
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
  cancellation_rate = (current_cancellations / rentals_with_previous) * 100 if rentals_with_previous > 0 else 0
189
 
 
269
 
270
  # ========== PAGE 1: DATA OVERVIEW ==========
271
  if selected == "📊 Data Overview":
272
+ st.title("📊 Dataset Overview & Exploratory Analysis")
273
+
274
+ st.markdown('<div class="section-header">📈 Dataset Summary</div>', unsafe_allow_html=True)
275
+
276
+ # Basic statistics
277
+ col1, col2, col3, col4 = st.columns(4)
278
+ with col1:
279
+ st.metric("Total Rentals", f"{len(df):,}")
280
+ with col2:
281
+ st.metric("Connect Rentals", f"{len(df[df['checkin_type'].str.lower() == 'connect']):,}")
282
+ with col3:
283
+ st.metric("Cancelled Rentals", f"{len(df[df['state'] == 'canceled']):,}",
284
+ help="Total cancelled rentals in dataset")
285
+ with col4:
286
+ st.metric("With Previous Rental", f"{df['has_previous_rental'].sum():,}",
287
+ help="Rentals that had another rental on the same car before (within 12 hours)")
288
+
289
+ # Checkin type distribution
290
+ st.markdown('<div class="section-header">🚗 Rental Type Distribution</div>', unsafe_allow_html=True)
291
+
292
+ col1, col2 = st.columns(2)
293
+
294
+ with col1:
295
+ checkin_counts = df["checkin_type"].value_counts()
296
+ fig_checkin = px.pie(
297
+ values=checkin_counts.values,
298
+ names=checkin_counts.index,
299
+ title="Distribution by Checkin Type",
300
+ color_discrete_sequence=px.colors.qualitative.Set3
301
+ )
302
+ st.plotly_chart(fig_checkin, use_container_width=True)
303
+
304
+ with col2:
305
+ state_counts = df["state"].value_counts()
306
+ fig_state = px.pie(
307
+ values=state_counts.values,
308
+ names=state_counts.index,
309
+ title="Distribution by Rental State",
310
+ color_discrete_sequence=px.colors.qualitative.Pastel
311
+ )
312
+ st.plotly_chart(fig_state, use_container_width=True)
313
+
314
+ # Delay analysis
315
+ st.markdown('<div class="section-header">⏰ Delay Analysis</div>', unsafe_allow_html=True)
316
+
317
+ col1, col2 = st.columns(2)
318
+
319
+ with col1:
320
+ # Delay status distribution
321
+ df["delay_status"] = df["delay_at_checkout_in_minutes"].apply(
322
+ lambda x: "Early Return" if pd.notnull(x) and x < 0 else
323
+ "On Time" if pd.notnull(x) and x == 0 else
324
+ "Late Return" if pd.notnull(x) and x > 0 else "Missing Data"
325
+ )
326
+
327
+ delay_counts = df["delay_status"].value_counts()
328
+ fig_delay_status = px.pie(
329
+ values=delay_counts.values,
330
+ names=delay_counts.index,
331
+ title="Return Status Distribution",
332
+ color_discrete_sequence=px.colors.qualitative.Bold
333
+ )
334
+ fig_delay_status.update_layout(
335
+ annotations=[dict(text="Missing Data = no checkout delay recorded",
336
+ x=0.5, y=-0.1, xref="paper", yref="paper",
337
+ showarrow=False, font=dict(size=10))]
338
+ )
339
+ st.plotly_chart(fig_delay_status, use_container_width=True)
340
+
341
+ with col2:
342
+ # Delay distribution histogram (starting from 0, showing early returns clearly)
343
+ delay_data = df[df["delay_at_checkout_in_minutes"].notnull()]
344
+ delay_filtered = delay_data[delay_data["delay_at_checkout_in_minutes"].between(-120, 300)]
345
+
346
+ fig_delay_hist = px.histogram(
347
+ delay_filtered,
348
+ x="delay_at_checkout_in_minutes",
349
+ nbins=50,
350
+ title="Checkout Delay Distribution",
351
+ labels={"delay_at_checkout_in_minutes": "Minutes (Negative = Early Return)", "count": "Number of Rentals"}
352
+ )
353
+ fig_delay_hist.add_vline(x=0, line_dash="dash", line_color="red", annotation_text="On Time")
354
+ fig_delay_hist.add_annotation(x=-60, y=delay_filtered.shape[0]*0.1, text="Early Returns",
355
+ showarrow=True, arrowhead=2, arrowcolor="blue")
356
+ fig_delay_hist.add_annotation(x=120, y=delay_filtered.shape[0]*0.1, text="Late Returns",
357
+ showarrow=True, arrowhead=2, arrowcolor="red")
358
+ st.plotly_chart(fig_delay_hist, use_container_width=True)
359
+
360
+ # Gap analysis
361
+ st.markdown('<div class="section-header">📏 Gap Between Rentals Analysis</div>', unsafe_allow_html=True)
362
+
363
+ col1, col2 = st.columns(2)
364
+
365
+ with col1:
366
+ # Gap distribution
367
+ gap_data = df[df["has_previous_rental"]]
368
+ gap_filtered = gap_data[gap_data["time_delta_with_previous_rental_in_minutes"].between(0, 720)]
369
+
370
+ fig_gap_hist = px.histogram(
371
+ gap_filtered,
372
+ x="time_delta_with_previous_rental_in_minutes",
373
+ nbins=40,
374
+ title="Gap Distribution (0 to 720 minutes)",
375
+ labels={"time_delta_with_previous_rental_in_minutes": "Gap (minutes)", "count": "Number of Rentals"}
376
+ )
377
+ st.plotly_chart(fig_gap_hist, use_container_width=True)
378
+
379
+ with col2:
380
+ # Gap by checkin type
381
+ fig_gap_box = px.box(
382
+ gap_filtered,
383
+ x="checkin_type",
384
+ y="time_delta_with_previous_rental_in_minutes",
385
+ title="Gap Distribution by Checkin Type",
386
+ labels={"time_delta_with_previous_rental_in_minutes": "Gap (minutes)"}
387
+ )
388
+ st.plotly_chart(fig_gap_box, use_container_width=True)
389
+
390
+ # Cancellation analysis
391
+ st.markdown('<div class="section-header">❌ Cancellation Analysis</div>', unsafe_allow_html=True)
392
+
393
+ col1, col2 = st.columns(2)
394
+
395
+ with col1:
396
+ st.markdown("### Cancellation by Type")
397
+
398
+ cancellation_by_type = df.groupby(['checkin_type', 'state']).size().unstack(fill_value=0)
399
+ if 'canceled' in cancellation_by_type.columns:
400
+ cancel_rates = (cancellation_by_type['canceled'] / cancellation_by_type.sum(axis=1) * 100).round(1)
401
+
402
+ fig_cancel_type = px.bar(
403
+ x=cancel_rates.index,
404
+ y=cancel_rates.values,
405
+ title="Cancellation Rate by Checkin Type",
406
+ labels={"x": "Checkin Type", "y": "Cancellation Rate (%)"}
407
+ )
408
+ st.plotly_chart(fig_cancel_type, use_container_width=True)
409
+ else:
410
+ st.info("No cancellations found in dataset")
411
+
412
+ with col2:
413
+ st.markdown("### Delay-Related Cancellations")
414
+
415
+ # Calculate accurate delay-related cancellations using previous rental data
416
+ df_analysis = df[df["has_previous_rental"]].copy()
417
+
418
+ # Join with previous rental delay data
419
+ prev_rental_data = df[["rental_id", "delay_at_checkout_in_minutes"]].rename(
420
+ columns={"rental_id": "previous_ended_rental_id",
421
+ "delay_at_checkout_in_minutes": "previous_delay"}
422
+ )
423
+
424
+ df_analysis = df_analysis.merge(
425
+ prev_rental_data,
426
+ on="previous_ended_rental_id",
427
+ how="left"
428
+ )
429
+
430
+ # Calculate problems using actual previous rental delay
431
+ df_analysis["previous_delay_clean"] = df_analysis["previous_delay"].clip(-720, 720)
432
+ df_analysis["causes_problem"] = (
433
+ df_analysis["previous_delay"].notnull() &
434
+ (df_analysis["previous_delay_clean"] > df_analysis["time_delta_with_previous_rental_in_minutes"])
435
+ )
436
+
437
+ # Cancellations due to previous delay
438
+ df_analysis["cancelled_due_to_previous_delay"] = (
439
+ (df_analysis["state"] == "canceled") & df_analysis["causes_problem"]
440
+ )
441
+
442
+ delay_related_cancels = df_analysis["cancelled_due_to_previous_delay"].sum()
443
+ total_cancels = (df_analysis["state"] == "canceled").sum()
444
+
445
+ col2_1, col2_2 = st.columns(2)
446
+ with col2_1:
447
+ st.metric("Total Cancellations", f"{total_cancels:,}")
448
+ with col2_2:
449
+ st.metric("Due to Previous Delay", f"{delay_related_cancels:,}",
450
+ help="Cancelled rentals where the previous rental on same car was late")
451
+
452
+ if total_cancels > 0:
453
+ delay_cancel_rate = (delay_related_cancels / total_cancels * 100)
454
+ st.metric("% of Cancellations Due to Previous Delays", f"{delay_cancel_rate:.1f}%")
455
+
456
+ # Problem cases analysis (including cancellations)
457
+ st.markdown('<div class="section-header">🚨 Current Problem Cases Analysis</div>', unsafe_allow_html=True)
458
+
459
+ st.markdown("""
460
+ <div class="insight-box">
461
+ <strong>What is a "Problem Case"?</strong><br>
462
+ A problem occurs when the PREVIOUS rental on the same car returned late AND this delay exceeds the planned gap to the current rental.
463
+ This forces the next customer to either wait beyond their scheduled pickup time or cancel their rental.
464
+ <br><br>
465
+ <strong>Accurate Formula:</strong> Problem = (previous_rental_delay > gap_to_current_rental) AND (previous_rental_delay > 0)<br>
466
+ <strong>Data Linking:</strong> Uses previous_ended_rental_id to get actual delay from the previous rental.<br>
467
+ <strong>Cancellation Impact:</strong> Some of these problems result in cancellations when waiting becomes unacceptable.
468
+ </div>
469
+ """, unsafe_allow_html=True)
470
+
471
+ # Calculate problem cases using proper linking to previous rental delays
472
+ df_problems = df[df["has_previous_rental"]].copy()
473
+
474
+ # Join with previous rental delay data
475
+ prev_rental_data = df[["rental_id", "delay_at_checkout_in_minutes"]].rename(
476
+ columns={"rental_id": "previous_ended_rental_id",
477
+ "delay_at_checkout_in_minutes": "previous_delay"}
478
+ )
479
+
480
+ df_problems = df_problems.merge(
481
+ prev_rental_data,
482
+ on="previous_ended_rental_id",
483
+ how="left"
484
+ )
485
+
486
+ # Calculate problems using actual previous rental delay
487
+ df_problems["previous_delay_clean"] = df_problems["previous_delay"].clip(-720, 720)
488
+ df_problems["causes_problem"] = (
489
+ df_problems["previous_delay"].notnull() &
490
+ (df_problems["previous_delay_clean"] > df_problems["time_delta_with_previous_rental_in_minutes"])
491
+ )
492
+ df_problems["wait_time"] = np.maximum(
493
+ 0,
494
+ df_problems["previous_delay_clean"] - df_problems["time_delta_with_previous_rental_in_minutes"]
495
+ ).fillna(0)
496
+
497
+ problem_cases = df_problems[df_problems["causes_problem"]]
498
+
499
+ col1, col2, col3, col4 = st.columns(4)
500
+ with col1:
501
+ st.metric("Total Problem Cases", f"{len(problem_cases):,}",
502
+ help="Number of rentals where late return affected the next customer")
503
+ with col2:
504
+ problem_rate = (len(problem_cases) / len(df_problems)) * 100 if len(df_problems) > 0 else 0
505
+ st.metric("Problem Rate", f"{problem_rate:.1f}%",
506
+ help="Percentage of consecutive rentals that create waiting problems")
507
+ with col3:
508
+ avg_wait = problem_cases["wait_time"].mean() if len(problem_cases) > 0 else 0
509
+ st.metric("Avg Wait Time", f"{avg_wait:.1f} min",
510
+ help="Average extra wait time for affected next customers (checkout_delay - gap)")
511
+ with col4:
512
+ # Add cancellation metric
513
+ delay_cancels = problem_cases[problem_cases["state"] == "canceled"]
514
+ st.metric("Resulting Cancellations", f"{len(delay_cancels):,}",
515
+ help="Problem cases that resulted in cancellations")
516
+
517
+ if len(problem_cases) > 0:
518
+ col1, col2 = st.columns(2)
519
+
520
+ with col1:
521
+ # Wait time distribution
522
+ fig_wait = px.histogram(
523
+ problem_cases[problem_cases["wait_time"] > 0],
524
+ x="wait_time",
525
+ nbins=30,
526
+ title="Wait Time Distribution for Problem Cases",
527
+ labels={"wait_time": "Wait Time (minutes)", "count": "Number of Cases"}
528
+ )
529
+ st.plotly_chart(fig_wait, use_container_width=True)
530
+
531
+ with col2:
532
+ # Problems by checkin type
533
+ problem_by_type = problem_cases["checkin_type"].value_counts()
534
+ fig_problems_type = px.bar(
535
+ x=problem_by_type.index,
536
+ y=problem_by_type.values,
537
+ title="Problem Cases by Checkin Type",
538
+ labels={"x": "Checkin Type", "y": "Number of Problems"}
539
+ )
540
+ st.plotly_chart(fig_problems_type, use_container_width=True)
541
+
542
+ # Raw data sample
543
+ with st.expander("📋 Raw Data Sample"):
544
+ st.markdown("### First 100 rows of the dataset:")
545
+ st.dataframe(df.head(100), use_container_width=True)
546
+
547
+ st.markdown("### Dataset Info:")
548
+ st.text("Dataset shape: " + str(df.shape))
549
+ st.text("Columns: " + str(list(df.columns)))
550
+
551
+ # ========== PAGE 2: THRESHOLD ANALYSIS ==========
552
+ elif selected == "🕐 Threshold Analysis":
553
  st.title("⏰ Threshold Decision: How Long Should the Minimum Delay Be?")
554
 
555
  st.markdown("""
 
640
 
641
  # Analysis of current problems
642
  df_analysis = df[df["has_previous_rental"]].copy()
643
+
644
+ # Join with previous rental delay data
645
+ prev_rental_data = df[["rental_id", "delay_at_checkout_in_minutes"]].rename(
646
+ columns={"rental_id": "previous_ended_rental_id",
647
+ "delay_at_checkout_in_minutes": "previous_delay"}
648
+ )
649
+
650
+ df_analysis = df_analysis.merge(
651
+ prev_rental_data,
652
+ on="previous_ended_rental_id",
653
+ how="left"
654
+ )
655
+
656
+ # Calculate problems using actual previous rental delay
657
+ df_analysis["previous_delay_clean"] = df_analysis["previous_delay"].clip(-720, 720)
658
  df_analysis["causes_problem"] = (
659
+ df_analysis["previous_delay"].notnull() &
660
+ (df_analysis["previous_delay_clean"] > df_analysis["time_delta_with_previous_rental_in_minutes"])
661
  )
662
  df_analysis["wait_time"] = np.maximum(
663
  0,
664
+ df_analysis["previous_delay_clean"] - df_analysis["time_delta_with_previous_rental_in_minutes"]
665
  ).fillna(0)
666
 
667
  problematic_cases = df_analysis[df_analysis["causes_problem"]]
 
803
  <li>📉 Blocks only <strong>{optimal['blocked_percentage']:.1f}%</strong> of consecutive rentals</li>
804
  <li>⚡ Achieves <strong>{optimal['problem_solve_rate']:.1f}%</strong> efficiency in problem solving</li>
805
  <li>💰 Impacts <strong>{optimal['revenue_impact_percent']:.1f}%</strong> of total rental volume</li>
806
+ <li>❌ Prevents <strong>{optimal['cancellations_prevented']:.0f}</strong> delay-related cancellations</li>
807
  </ul>
808
  <p><em>This threshold balances customer satisfaction improvements with minimal impact on availability.</em></p>
809
  </div>
810
  """, unsafe_allow_html=True)
811
 
812
+ # ========== PAGE 3: SCOPE ANALYSIS ==========
813
+ elif selected == "🎯 Scope Analysis":
814
  st.title("🎯 Scope Decision: All Cars vs Connect Cars Only?")
815
 
816
  st.markdown("""
 
960
  st.markdown("### Current Problems by Type")
961
 
962
  df_problems = df[df["has_previous_rental"]].copy()
963
+
964
+ # Join with previous rental delay data
965
+ prev_rental_data = df[["rental_id", "delay_at_checkout_in_minutes"]].rename(
966
+ columns={"rental_id": "previous_ended_rental_id",
967
+ "delay_at_checkout_in_minutes": "previous_delay"}
968
+ )
969
+
970
+ df_problems = df_problems.merge(
971
+ prev_rental_data,
972
+ on="previous_ended_rental_id",
973
+ how="left"
974
+ )
975
+
976
+ # Calculate problems using actual previous rental delay
977
+ df_problems["previous_delay_clean"] = df_problems["previous_delay"].clip(-720, 720)
978
  df_problems["causes_problem"] = (
979
+ df_problems["previous_delay"].notnull() &
980
+ (df_problems["previous_delay_clean"] > df_problems["time_delta_with_previous_rental_in_minutes"])
981
  )
982
 
983
  problem_by_type = df_problems.groupby("checkin_type")["causes_problem"].agg(["sum", "count"]).reset_index()
 
1040
  </div>
1041
  """, unsafe_allow_html=True)
1042
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1043
  # ========== FOOTER ==========
1044
  st.markdown("---")
1045
  st.markdown("""