# app.py
import streamlit as st
import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from streamlit_option_menu import option_menu
from pathlib import Path
import warnings
warnings.filterwarnings('ignore')
# ========== PAGE CONFIGURATION ==========
st.set_page_config(
page_title="Car Rental Delay Analysis",
#page_icon="🚗",
layout="wide",
initial_sidebar_state="expanded"
)
# ========== CUSTOM CSS ==========
st.markdown("""
""", unsafe_allow_html=True)
# ========== DATA LOADING ==========
@st.cache_data(show_spinner=False)
def load_data():
"""Load and preprocess the rental data"""
possible_paths = [
"get_around_delay_analysis.xlsx",
"get_around_delay_analysis.csv",
"/mnt/data/get_around_delay_analysis.xlsx",
"/mnt/data/get_around_delay_analysis.csv"
]
df = None
for path in possible_paths:
if Path(path).exists():
try:
if path.endswith('.xlsx'):
df = pd.read_excel(path)
else:
df = pd.read_csv(path)
break
except Exception as e:
continue
if df is None:
st.error("❌ Data file not found. Please ensure 'get_around_delay_analysis.xlsx' or 'get_around_delay_analysis.csv' is available.")
st.stop()
# Clean the data
df = df.drop(columns=[c for c in df.columns if c.lower().startswith("unnamed")], errors="ignore")
df["has_previous_rental"] = df["time_delta_with_previous_rental_in_minutes"].notnull()
df["clean_delay"] = df["delay_at_checkout_in_minutes"].clip(-720, 720)
return df
# ========== ANALYSIS FUNCTIONS ==========
def calculate_metrics(df, threshold_minutes, scope="all"):
"""Calculate key metrics for threshold analysis"""
# Filter by scope
if scope == "connect":
df_filtered = df[df["checkin_type"].str.lower() == "connect"].copy()
else:
df_filtered = df.copy()
df_with_prev = df_filtered[df_filtered["has_previous_rental"]].copy()
if len(df_with_prev) == 0:
return {
"total_rentals": len(df_filtered),
"rentals_with_previous": 0,
"blocked_rentals": 0,
"blocked_percentage": 0.0,
"current_problems": 0,
"problems_solved": 0,
"solve_efficiency": 0.0,
"revenue_impact": 0.0
}
# Join with previous rental data
prev_rental_data = df[["rental_id", "delay_at_checkout_in_minutes"]].rename(
columns={"rental_id": "previous_ended_rental_id",
"delay_at_checkout_in_minutes": "previous_delay"}
)
df_with_prev = df_with_prev.merge(prev_rental_data, on="previous_ended_rental_id", how="left")
# Calculate key metrics
df_with_prev["would_be_blocked"] = df_with_prev["time_delta_with_previous_rental_in_minutes"] < threshold_minutes
df_with_prev["previous_delay_clean"] = df_with_prev["previous_delay"].clip(-720, 720)
df_with_prev["causes_problem"] = (
df_with_prev["previous_delay"].notnull() &
(df_with_prev["previous_delay_clean"] > df_with_prev["time_delta_with_previous_rental_in_minutes"])
)
df_with_prev["problem_solved"] = df_with_prev["causes_problem"] & df_with_prev["would_be_blocked"]
# Final calculations
total_rentals = len(df_filtered)
rentals_with_previous = len(df_with_prev)
blocked_rentals = int(df_with_prev["would_be_blocked"].sum())
blocked_percentage = (blocked_rentals / rentals_with_previous) * 100 if rentals_with_previous > 0 else 0
current_problems = int(df_with_prev["causes_problem"].sum())
problems_solved = int(df_with_prev["problem_solved"].sum())
solve_efficiency = (problems_solved / blocked_rentals * 100) if blocked_rentals > 0 else 0
revenue_impact = (blocked_rentals / total_rentals) * 100 if total_rentals > 0 else 0
return {
"total_rentals": total_rentals,
"rentals_with_previous": rentals_with_previous,
"blocked_rentals": blocked_rentals,
"blocked_percentage": blocked_percentage,
"current_problems": current_problems,
"problems_solved": problems_solved,
"solve_efficiency": solve_efficiency,
"availability_impact": revenue_impact # More accurate naming
}
def create_threshold_analysis(df, thresholds, scope="all"):
"""Create threshold analysis data"""
results = []
for threshold in thresholds:
metrics = calculate_metrics(df, threshold, scope)
results.append({"threshold": threshold, **metrics})
return pd.DataFrame(results)
# ========== LOAD DATA ==========
df = load_data()
# ========== MAIN DASHBOARD ==========
st.markdown('
Car Rental Delay Analysis
', unsafe_allow_html=True)
# Sidebar with controls
with st.sidebar:
# Logo placeholder - you can add your logo here
# st.image("logo.png", width=200) # Uncomment and add your logo file
st.markdown("## Analysis Controls")
selected = option_menu(
"Analysis Focus",
["Overview & Problems", "Threshold & Scope"],
icons=["graph-up", "sliders"], # More professional icons
menu_icon="house-gear",
default_index=0,
)
if selected == "Threshold & Scope":
st.markdown("### Settings")
threshold = st.slider("Threshold (minutes)", 0, 300, 90, step=30,
help="Minimum delay between consecutive rentals")
scope = st.selectbox("Implementation Scope", ["all", "connect"],
format_func=lambda x: "All Cars" if x == "all" else "Connect Cars Only")
st.markdown("---")
st.markdown("### Dataset Summary")
st.info(f"""
**Total Rentals:** {len(df):,}
**Connect Rentals:** {len(df[df['checkin_type'].str.lower() == 'connect']):,}
**Cancelled Rentals:** {len(df[df['state'] == 'canceled']):,}
**With Previous Rental:** {df['has_previous_rental'].sum():,}
""")
# ========== PAGE 1: OVERVIEW & PROBLEMS ==========
if selected == "Overview & Problems":
st.title("Understanding the Delay Problem")
# Dataset Overview
st.markdown('', unsafe_allow_html=True)
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("Total Rentals", f"{len(df):,}",
help="Complete dataset of all rental transactions")
with col2:
st.metric("Connect Rentals", f"{len(df[df['checkin_type'].str.lower() == 'connect']):,}",
help="Rentals using Connect technology (keyless entry via smartphone)")
with col3:
st.metric("Mobile Rentals", f"{len(df[df['checkin_type'].str.lower() == 'mobile']):,}",
help="Traditional rentals using mobile app (owner hands over keys)")
with col4:
st.metric("With Previous Rental", f"{df['has_previous_rental'].sum():,}",
help="Rentals where the same car had another rental within 12 hours before - these can be analyzed for delay impact")
# Rental type distribution
col1, col2 = st.columns(2)
with col1:
checkin_counts = df["checkin_type"].value_counts()
fig_checkin = px.pie(
values=checkin_counts.values,
names=checkin_counts.index,
title="Distribution by Checkin Type",
color_discrete_sequence=px.colors.qualitative.Set3
)
st.plotly_chart(fig_checkin, use_container_width=True)
with col2:
state_counts = df["state"].value_counts()
fig_state = px.pie(
values=state_counts.values,
names=state_counts.index,
title="Distribution by Rental State",
color_discrete_sequence=px.colors.qualitative.Pastel
)
st.plotly_chart(fig_state, use_container_width=True)
# Key Problem Analysis
st.markdown('', unsafe_allow_html=True)
# Calculate current problems
df_problems = df[df["has_previous_rental"]].copy()
prev_rental_data = df[["rental_id", "delay_at_checkout_in_minutes"]].rename(
columns={"rental_id": "previous_ended_rental_id",
"delay_at_checkout_in_minutes": "previous_delay"}
)
df_problems = df_problems.merge(prev_rental_data, on="previous_ended_rental_id", how="left")
df_problems["previous_delay_clean"] = df_problems["previous_delay"].clip(-720, 720)
df_problems["causes_problem"] = (
df_problems["previous_delay"].notnull() &
(df_problems["previous_delay_clean"] > df_problems["time_delta_with_previous_rental_in_minutes"])
)
df_problems["wait_time"] = np.maximum(
0,
df_problems["previous_delay_clean"] - df_problems["time_delta_with_previous_rental_in_minutes"]
).fillna(0)
problem_cases = df_problems[df_problems["causes_problem"]]
# Key metrics
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("Problem Cases", f"{len(problem_cases):,}",
help="Formula: previous_rental_delay > gap_to_current_rental AND previous_rental_delay > 0. Uses previous_ended_rental_id to link actual delay data.")
with col2:
problem_rate = (len(problem_cases) / len(df_problems)) * 100 if len(df_problems) > 0 else 0
st.metric("Problem Rate", f"{problem_rate:.1f}%",
help="Calculation: problem_cases / rentals_with_previous_rental * 100")
with col3:
avg_wait = problem_cases["wait_time"].mean() if len(problem_cases) > 0 else 0
st.metric("Avg Wait Time", f"{avg_wait:.1f} min",
help="Formula: max(0, previous_rental_delay - gap_to_current_rental). Average wait time when problems occur.")
with col4:
delay_cancels = problem_cases[problem_cases["state"] == "canceled"]
st.metric("Resulting Cancellations", f"{len(delay_cancels):,}",
help="Cancelled rentals that were also problem cases (delay caused customer to cancel)")
st.markdown("""
The Problem: Late returns create customer wait times and cancellations.
""", unsafe_allow_html=True)
# Direct answer to PM's questions
st.markdown('', unsafe_allow_html=True)
# Calculate late return frequency and impact
df_late_analysis = df[df["has_previous_rental"]].copy()
# Join with previous rental delay data
prev_rental_data = df[["rental_id", "delay_at_checkout_in_minutes"]].rename(
columns={"rental_id": "previous_ended_rental_id",
"delay_at_checkout_in_minutes": "previous_delay"}
)
df_late_analysis = df_late_analysis.merge(prev_rental_data, on="previous_ended_rental_id", how="left")
# Calculate late return frequency
df_late_analysis["previous_delay_clean"] = df_late_analysis["previous_delay"].clip(-720, 720)
df_late_analysis["previous_was_late"] = (
df_late_analysis["previous_delay"].notnull() &
(df_late_analysis["previous_delay_clean"] > 0)
)
# Calculate impact on next driver
df_late_analysis["causes_problem"] = (
df_late_analysis["previous_delay"].notnull() &
(df_late_analysis["previous_delay_clean"] > df_late_analysis["time_delta_with_previous_rental_in_minutes"])
)
df_late_analysis["wait_time"] = np.maximum(
0,
df_late_analysis["previous_delay_clean"] - df_late_analysis["time_delta_with_previous_rental_in_minutes"]
).fillna(0)
late_returns = df_late_analysis[df_late_analysis["previous_was_late"]]
impacted_next_drivers = df_late_analysis[df_late_analysis["causes_problem"]]
col1, col2 = st.columns(2)
with col1:
st.markdown("#### Late Return Frequency")
total_with_delay_data = df_late_analysis[df_late_analysis["previous_delay"].notnull()]
late_frequency = (len(late_returns) / len(total_with_delay_data)) * 100 if len(total_with_delay_data) > 0 else 0
st.metric("Late Returns", f"{len(late_returns):,}")
st.metric("Late Return Rate", f"{late_frequency:.1f}%",
help="Percentage of returns that are late (delay > 0 minutes)")
# Late return severity
if len(late_returns) > 0:
avg_late_delay = late_returns["previous_delay_clean"].mean()
st.metric("Average Late Delay", f"{avg_late_delay:.1f} min")
with col2:
st.markdown("#### Impact on Next Driver")
impact_rate = (len(impacted_next_drivers) / len(df_late_analysis)) * 100 if len(df_late_analysis) > 0 else 0
st.metric("Next Drivers Impacted", f"{len(impacted_next_drivers):,}")
st.metric("Impact Rate", f"{impact_rate:.1f}%",
help="Percentage of consecutive rentals where late return causes waiting")
if len(impacted_next_drivers) > 0:
avg_wait = impacted_next_drivers["wait_time"].mean()
st.metric("Average Wait Time", f"{avg_wait:.1f} min",
help="Average additional wait time when impacted")
# Visual analysis of the relationship
col1, col2 = st.columns(2)
with col1:
# Late return distribution
if len(late_returns) > 0:
late_delays = late_returns["previous_delay_clean"]
late_filtered = late_delays[late_delays <= 300] # Cap at 5 hours for visualization
fig_late = px.histogram(
late_filtered,
nbins=20,
title="Distribution of Late Return Delays",
labels={"value": "Delay (minutes)", "count": "Number of Late Returns"}
)
st.plotly_chart(fig_late, use_container_width=True)
with col2:
# Wait time impact distribution
if len(impacted_next_drivers) > 0:
wait_times = impacted_next_drivers[impacted_next_drivers["wait_time"] > 0]["wait_time"]
fig_wait = px.histogram(
wait_times,
nbins=20,
title="Wait Time Distribution for Impacted Next Drivers",
labels={"value": "Wait Time (minutes)", "count": "Number of Impacted Drivers"}
)
st.plotly_chart(fig_wait, use_container_width=True)
# Key insight summary
late_to_impact_ratio = (len(impacted_next_drivers) / len(late_returns)) * 100 if len(late_returns) > 0 else 0
st.markdown(f"""
Key Insights:
• {late_frequency:.1f}% of returns are late
• {impact_rate:.1f}% of consecutive rentals are impacted
• Average wait time: {avg_wait:.1f} minutes when problems occur
""", unsafe_allow_html=True)
# Visual analysis
st.markdown('', unsafe_allow_html=True)
col1, col2 = st.columns(2)
with col1:
# Return status distribution
df_with_delay_data = df[df["delay_at_checkout_in_minutes"].notnull()].copy()
df_with_delay_data["delay_status"] = df_with_delay_data["delay_at_checkout_in_minutes"].apply(
lambda x: "Early Return" if x < 0 else "On Time" if x == 0 else "Late Return"
)
delay_counts = df_with_delay_data["delay_status"].value_counts()
fig_status = px.pie(
values=delay_counts.values,
names=delay_counts.index,
title="Return Status Distribution",
color_discrete_sequence=px.colors.qualitative.Set3
)
st.plotly_chart(fig_status, use_container_width=True)
with col2:
# Delay distribution
delay_filtered = df_with_delay_data[df_with_delay_data["delay_at_checkout_in_minutes"].between(-60, 240)]
fig_hist = px.histogram(
delay_filtered,
x="delay_at_checkout_in_minutes",
nbins=30,
title="Delay Distribution",
labels={"delay_at_checkout_in_minutes": "Delay (minutes)", "count": "Rentals"}
)
fig_hist.add_vline(x=0, line_dash="dash", line_color="red", annotation_text="On Time")
st.plotly_chart(fig_hist, use_container_width=True)
# Gap analysis
st.markdown('', unsafe_allow_html=True)
gap_data = df[df["has_previous_rental"]]
gap_filtered = gap_data[gap_data["time_delta_with_previous_rental_in_minutes"].between(0, 480)]
fig_gap = px.histogram(
gap_filtered,
x="time_delta_with_previous_rental_in_minutes",
nbins=20,
title="Gap Distribution Between Consecutive Rentals",
labels={"time_delta_with_previous_rental_in_minutes": "Gap (minutes)", "count": "Rentals"}
)
st.plotly_chart(fig_gap, use_container_width=True)
# Cancellation analysis
st.markdown('', unsafe_allow_html=True)
col1, col2 = st.columns(2)
with col1:
total_cancels_all = (df["state"] == "canceled").sum()
delay_related_cancels = (df_problems["state"] == "canceled") & df_problems["causes_problem"]
delay_cancel_count = delay_related_cancels.sum()
st.metric("Total Cancellations", f"{total_cancels_all:,}",
help="All cancelled rentals in the dataset (state = 'canceled')")
st.metric("Due to Previous Delays", f"{delay_cancel_count:,}",
help="Cancelled rentals that were also problem cases (previous rental caused delay)")
if total_cancels_all > 0:
delay_cancel_rate = (delay_cancel_count / total_cancels_all * 100)
st.metric("% Due to Delays", f"{delay_cancel_rate:.1f}%",
help="Calculation: delay_related_cancellations / total_cancellations * 100")
with col2:
# Cancellation by checkin type
cancellation_by_type = df.groupby(['checkin_type', 'state']).size().unstack(fill_value=0)
if 'canceled' in cancellation_by_type.columns:
cancel_rates = (cancellation_by_type['canceled'] / cancellation_by_type.sum(axis=1) * 100).round(1)
fig_cancel = px.bar(
x=cancel_rates.index,
y=cancel_rates.values,
title="Cancellation Rate by Type",
labels={"x": "Checkin Type", "y": "Cancellation Rate (%)"}
)
st.plotly_chart(fig_cancel, use_container_width=True)
# ========== PAGE 2: THRESHOLD & SCOPE ANALYSIS ==========
elif selected == "Threshold & Scope":
st.title("Threshold & Scope Decision")
st.markdown("""
Availability Impact: Percentage of rental slots blocked due to insufficient gap between rentals.
""", unsafe_allow_html=True)
# Current impact
current_metrics = calculate_metrics(df, threshold, scope)
st.markdown('', unsafe_allow_html=True)
col1, col2, col3, col4 = st.columns(4)
with col1:
st.markdown(f"""
{current_metrics['blocked_rentals']:,}
Blocked Rentals
Gap < {threshold} min
""", unsafe_allow_html=True)
st.caption("Formula: time_delta_with_previous_rental < threshold")
with col2:
st.markdown(f"""
{current_metrics['blocked_percentage']:.1f}%
Blocked Rate
Of consecutive rentals
""", unsafe_allow_html=True)
st.caption("Calculation: blocked_rentals / rentals_with_previous * 100")
with col3:
st.markdown(f"""
{current_metrics['problems_solved']:,}
Problems Solved
Waiting eliminated
""", unsafe_allow_html=True)
st.caption("Logic: current_problems AND would_be_blocked")
with col4:
st.markdown(f"""
{current_metrics['availability_impact']:.1f}%
Availability Impact
% of rental slots blocked
""", unsafe_allow_html=True)
st.caption("Formula: blocked_rentals / total_rentals * 100")
# Threshold analysis
st.markdown('', unsafe_allow_html=True)
thresholds = list(range(0, 301, 30))
threshold_data = create_threshold_analysis(df, thresholds, scope)
# Create dual-axis chart
fig = make_subplots(
rows=1, cols=2,
subplot_titles=("Problems Solved vs Blocked Rentals", "Efficiency vs Availability Impact"),
specs=[[{"secondary_y": True}, {"secondary_y": True}]]
)
# Left chart: Problems vs Blocked
fig.add_trace(
go.Scatter(x=threshold_data["threshold"], y=threshold_data["problems_solved"],
mode="lines+markers", name="Problems Solved", line=dict(color="green")),
row=1, col=1, secondary_y=False
)
fig.add_trace(
go.Scatter(x=threshold_data["threshold"], y=threshold_data["blocked_rentals"],
mode="lines+markers", name="Blocked Rentals", line=dict(color="red")),
row=1, col=1, secondary_y=True
)
# Right chart: Efficiency vs Availability Impact
fig.add_trace(
go.Scatter(x=threshold_data["threshold"], y=threshold_data["solve_efficiency"],
mode="lines+markers", name="Efficiency (%)", line=dict(color="blue")),
row=1, col=2, secondary_y=False
)
fig.add_trace(
go.Scatter(x=threshold_data["threshold"], y=threshold_data["availability_impact"],
mode="lines+markers", name="Availability Impact (%)", line=dict(color="orange")),
row=1, col=2, secondary_y=True
)
# Add current threshold line
fig.add_vline(x=threshold, line_dash="dash", line_color="red",
annotation_text=f"Current: {threshold}min", row=1, col=1)
fig.add_vline(x=threshold, line_dash="dash", line_color="red",
annotation_text=f"Current: {threshold}min", row=1, col=2)
fig.update_layout(height=400, showlegend=True)
fig.update_xaxes(title_text="Threshold (minutes)")
fig.update_yaxes(title_text="Count", secondary_y=False, row=1, col=1)
fig.update_yaxes(title_text="Count", secondary_y=True, row=1, col=1)
fig.update_yaxes(title_text="Percentage (%)", secondary_y=False, row=1, col=2)
fig.update_yaxes(title_text="Percentage (%)", secondary_y=True, row=1, col=2)
st.plotly_chart(fig, use_container_width=True)
# Scope comparison
st.markdown('', unsafe_allow_html=True)
all_metrics = calculate_metrics(df, threshold, "all")
connect_metrics = calculate_metrics(df, threshold, "connect")
col1, col2 = st.columns(2)
with col1:
st.markdown("### All Cars")
st.markdown(f"""
**Problems Solved:** {all_metrics['problems_solved']:,}
**Blocked Rentals:** {all_metrics['blocked_rentals']:,} ({all_metrics['blocked_percentage']:.1f}%)
**Efficiency:** {all_metrics['solve_efficiency']:.1f}%
**Availability Impact:** {all_metrics['availability_impact']:.1f}%
""")
with col2:
st.markdown("### Connect Cars Only")
st.markdown(f"""
**Problems Solved:** {connect_metrics['problems_solved']:,}
**Blocked Rentals:** {connect_metrics['blocked_rentals']:,} ({connect_metrics['blocked_percentage']:.1f}%)
**Efficiency:** {connect_metrics['solve_efficiency']:.1f}%
**Availability Impact:** {connect_metrics['availability_impact']:.1f}%
""")
# Scope comparison chart
all_data = create_threshold_analysis(df, thresholds, "all")
connect_data = create_threshold_analysis(df, thresholds, "connect")
fig_scope = go.Figure()
# Calculate percentage of problems solved for fair comparison
all_data["problems_solved_percent"] = (all_data["problems_solved"] / all_data["current_problems"].iloc[0] * 100).fillna(0)
connect_data["problems_solved_percent"] = (connect_data["problems_solved"] / connect_data["current_problems"].iloc[0] * 100).fillna(0)
fig_scope.add_trace(go.Scatter(x=all_data["threshold"], y=all_data["problems_solved_percent"],
mode="lines+markers", name="All Cars - % Problems Solved"))
fig_scope.add_trace(go.Scatter(x=connect_data["threshold"], y=connect_data["problems_solved_percent"],
mode="lines+markers", name="Connect Only - % Problems Solved"))
fig_scope.add_vline(x=threshold, line_dash="dash", line_color="red",
annotation_text=f"Current: {threshold}min")
fig_scope.update_layout(title="Percentage of Problems Solved by Scope",
xaxis_title="Threshold (minutes)",
yaxis_title="% of Problems Solved")
st.plotly_chart(fig_scope, use_container_width=True)
# Recommendations
st.markdown('', unsafe_allow_html=True)
st.markdown("""
Key Trade-off: Customer experience improvements vs. availability reduction
""", unsafe_allow_html=True)
# Show current trade-offs
col1, col2 = st.columns(2)
with col1:
st.markdown("**Benefits**")
st.write(f"• Eliminates {current_metrics['problems_solved']:,} wait situations")
st.write(f"• {current_metrics['solve_efficiency']:.1f}% efficiency rate")
st.write(f"• Reduces customer complaints")
with col2:
st.markdown("**Costs**")
st.write(f"• Blocks {current_metrics['blocked_rentals']:,} booking opportunities")
st.write(f"• {current_metrics['availability_impact']:.1f}% availability reduction")
st.write(f"• Operational implementation required")
# Strategic recommendations with 120min suggestion
st.markdown("### Recommended Strategy")
# Get 120min metrics for recommendation
metrics_120 = calculate_metrics(df, 120, scope)
col1, col2 = st.columns(2)
with col1:
st.markdown(f"""
Threshold: 120 minutes
Balanced approach for customer experience
Solves {metrics_120['problems_solved']:,} problems, blocks {metrics_120['availability_impact']:.1f}% availability
""", unsafe_allow_html=True)
with col2:
# Scope recommendation logic
all_120 = calculate_metrics(df, 120, "all")
connect_120 = calculate_metrics(df, 120, "connect")
if connect_120['solve_efficiency'] > all_120['solve_efficiency'] and connect_120['problems_solved'] >= all_120['problems_solved'] * 0.7:
recommended_scope = "Connect Only"
else:
recommended_scope = "All Cars"
st.markdown(f"""
Scope: {recommended_scope}
Optimal efficiency and coverage
Based on efficiency and problem-solving balance
""", unsafe_allow_html=True)
# ========== FOOTER ==========
st.markdown("---")
st.markdown("""
Car Rental Delay Analysis - Supporting threshold and scope decisions
""", unsafe_allow_html=True)