Spaces:
Runtime error
Runtime error
Update app.py
Browse files
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
|
| 133 |
-
df_with_prev["
|
| 134 |
df_with_prev["causes_problem"] = (
|
| 135 |
-
df_with_prev["
|
| 136 |
-
(df_with_prev["
|
| 137 |
)
|
| 138 |
|
| 139 |
-
# Wait time calculation
|
| 140 |
df_with_prev["wait_time_next_driver"] = np.maximum(
|
| 141 |
0,
|
| 142 |
-
df_with_prev["
|
| 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 |
-
#
|
| 152 |
-
df_with_prev["
|
| 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["
|
| 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["
|
| 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["
|
| 351 |
-
(df_analysis["
|
| 352 |
)
|
| 353 |
df_analysis["wait_time"] = np.maximum(
|
| 354 |
0,
|
| 355 |
-
df_analysis["
|
| 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
|
| 503 |
-
elif selected == "
|
| 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["
|
| 655 |
-
(df_problems["
|
| 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("""
|