# 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('
Dataset Overview
', 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('
Current Problem Scope
', 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('
How Often Are Drivers Late & Impact on Next Driver
', 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('
Delay Patterns
', 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('
Time Gaps Between Rentals
', 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('
Cancellation Impact
', 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('
Impact at Current Settings
', 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('
Threshold Analysis
', 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('
Scope Comparison: All Cars vs Connect Only
', 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('
Business Decision Framework
', 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)