Spaces:
Runtime error
Runtime error
Upload 4 files
Browse files- README.md +17 -6
- app.py +390 -0
- requirements.txt +5 -0
- synthetic_delivery_data.csv +0 -0
README.md
CHANGED
|
@@ -1,12 +1,23 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Delivery Delay Risk Intelligence Dashboard
|
| 3 |
+
emoji: 🚚
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 5.0.0
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# Delivery Delay Risk Intelligence Dashboard
|
| 13 |
+
|
| 14 |
+
This Hugging Face Space supports a university group project on delivery delay risk.
|
| 15 |
+
|
| 16 |
+
It combines:
|
| 17 |
+
- real-world/found delivery logistics data uploaded as CSV,
|
| 18 |
+
- synthetic delivery-time generation logic,
|
| 19 |
+
- quantitative dashboard analysis,
|
| 20 |
+
- qualitative business recommendations,
|
| 21 |
+
- automation through an AI-enhanced interface.
|
| 22 |
+
|
| 23 |
+
Users can upload a delivery dataset or use the included sample dataset, filter operational conditions, identify high-risk delay factors, simulate a new delivery, and download an executive summary.
|
app.py
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
import plotly.express as px
|
| 5 |
+
from sklearn.ensemble import RandomForestClassifier
|
| 6 |
+
from sklearn.preprocessing import OneHotEncoder
|
| 7 |
+
from sklearn.compose import ColumnTransformer
|
| 8 |
+
from sklearn.pipeline import Pipeline
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
import tempfile
|
| 11 |
+
|
| 12 |
+
DATA_PATH = Path("synthetic_delivery_data.csv")
|
| 13 |
+
|
| 14 |
+
NUMERIC_COLS = [
|
| 15 |
+
"distance_km", "package_weight_kg", "delivery_time_hours",
|
| 16 |
+
"expected_time_hours", "delivery_rating", "delivery_cost"
|
| 17 |
+
]
|
| 18 |
+
CAT_COLS = [
|
| 19 |
+
"delivery_partner", "package_type", "vehicle_type", "delivery_mode",
|
| 20 |
+
"region", "weather_condition", "delayed", "delivery_status"
|
| 21 |
+
]
|
| 22 |
+
|
| 23 |
+
CUSTOM_CSS = """
|
| 24 |
+
.gradio-container {max-width: 1280px !important; margin: auto;}
|
| 25 |
+
.metric-card {background: linear-gradient(135deg, #ffffff, #f7f8fb); border: 1px solid #e8e8ef; border-radius: 18px; padding: 18px; box-shadow: 0 8px 24px rgba(0,0,0,.05);}
|
| 26 |
+
.metric-label {font-size: 13px; color: #5f6470; margin-bottom: 6px;}
|
| 27 |
+
.metric-value {font-size: 30px; font-weight: 800; color: #111827;}
|
| 28 |
+
.insight-box {background: #111827; color: white; border-radius: 18px; padding: 20px; line-height: 1.55;}
|
| 29 |
+
.small-muted {color: #6b7280; font-size: 13px;}
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _clean_time_column(series):
|
| 34 |
+
"""Convert either normal numbers or timestamp-looking duration strings into numeric hours."""
|
| 35 |
+
if pd.api.types.is_numeric_dtype(series):
|
| 36 |
+
return pd.to_numeric(series, errors="coerce")
|
| 37 |
+
s = series.astype(str)
|
| 38 |
+
# Handles values like 1970-01-01 00:00:00.000000008 by extracting last part.
|
| 39 |
+
extracted = s.str.split(".").str[-1]
|
| 40 |
+
return pd.to_numeric(extracted, errors="coerce")
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def load_and_prepare(file_obj=None):
|
| 44 |
+
if file_obj is None:
|
| 45 |
+
df = pd.read_csv(DATA_PATH)
|
| 46 |
+
else:
|
| 47 |
+
df = pd.read_csv(file_obj.name)
|
| 48 |
+
|
| 49 |
+
df = df.copy()
|
| 50 |
+
df.columns = df.columns.str.strip().str.lower()
|
| 51 |
+
df = df.drop_duplicates()
|
| 52 |
+
|
| 53 |
+
required_minimum = ["distance_km", "vehicle_type", "weather_condition", "delivery_mode", "region"]
|
| 54 |
+
missing_required = [c for c in required_minimum if c not in df.columns]
|
| 55 |
+
if missing_required:
|
| 56 |
+
raise gr.Error(f"Your file is missing these required columns: {missing_required}")
|
| 57 |
+
|
| 58 |
+
for col in ["delivery_time_hours", "expected_time_hours"]:
|
| 59 |
+
if col in df.columns:
|
| 60 |
+
df[col] = _clean_time_column(df[col])
|
| 61 |
+
|
| 62 |
+
for col in NUMERIC_COLS:
|
| 63 |
+
if col in df.columns:
|
| 64 |
+
df[col] = pd.to_numeric(df[col], errors="coerce")
|
| 65 |
+
df[col] = df[col].fillna(df[col].median())
|
| 66 |
+
|
| 67 |
+
for col in CAT_COLS:
|
| 68 |
+
if col in df.columns:
|
| 69 |
+
df[col] = df[col].astype(str).str.strip().str.lower()
|
| 70 |
+
if df[col].isna().any():
|
| 71 |
+
df[col] = df[col].fillna(df[col].mode()[0])
|
| 72 |
+
|
| 73 |
+
# If expected/delivery time are not reliable or missing, rebuild them with business logic.
|
| 74 |
+
df = create_synthetic_time_logic(df)
|
| 75 |
+
df["delay_hours"] = (df["delivery_time_hours"] - df["expected_time_hours"]).round(2)
|
| 76 |
+
df["calculated_delay"] = np.where(df["delay_hours"] > 0, "yes", "no")
|
| 77 |
+
df["delay_score"] = df["delay_hours"].apply(delay_score)
|
| 78 |
+
df["performance_label"] = df["delay_score"].apply(performance_label)
|
| 79 |
+
df["distance_category"] = pd.cut(
|
| 80 |
+
df["distance_km"],
|
| 81 |
+
bins=[0, 50, 150, 300, float("inf")],
|
| 82 |
+
labels=["short", "medium", "long", "very long"],
|
| 83 |
+
include_lowest=True,
|
| 84 |
+
).astype(str)
|
| 85 |
+
return df
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def create_synthetic_time_logic(df):
|
| 89 |
+
df = df.copy()
|
| 90 |
+
for col in ["vehicle_type", "weather_condition", "delivery_mode", "region"]:
|
| 91 |
+
df[col] = df[col].astype(str).str.strip().str.lower()
|
| 92 |
+
|
| 93 |
+
vehicle_adjustment = {"bike": 1.2, "van": 0.5, "truck": 0.8, "ev van": 0.4}
|
| 94 |
+
weather_adjustment = {"clear": 0.0, "cloudy": 0.2, "foggy": 0.6, "rainy": 0.8, "stormy": 1.2, "cold": 0.2, "hot": 0.2, "windy": 0.3}
|
| 95 |
+
mode_adjustment = {"same day": 0.3, "express": 0.2, "two day": 0.7, "standard": 0.5}
|
| 96 |
+
region_adjustment = {"central": 0.6, "north": 0.3, "south": 0.3, "east": 0.4, "west": 0.4}
|
| 97 |
+
|
| 98 |
+
expected = (
|
| 99 |
+
df["distance_km"] / 45
|
| 100 |
+
+ df["vehicle_type"].map(vehicle_adjustment).fillna(0.5)
|
| 101 |
+
+ df["weather_condition"].map(weather_adjustment).fillna(0.3)
|
| 102 |
+
+ df["delivery_mode"].map(mode_adjustment).fillna(0.4)
|
| 103 |
+
+ df["region"].map(region_adjustment).fillna(0.3)
|
| 104 |
+
).clip(lower=0.5)
|
| 105 |
+
|
| 106 |
+
vehicle_mult = {"bike": 1.05, "van": 0.95, "truck": 1.02, "ev van": 0.97}
|
| 107 |
+
weather_mult = {"clear": 0.95, "cloudy": 1.00, "foggy": 1.05, "rainy": 1.10, "stormy": 1.20, "cold": 1.02, "hot": 1.02, "windy": 1.03}
|
| 108 |
+
mode_mult = {"same day": 1.05, "express": 1.02, "two day": 0.97, "standard": 1.00}
|
| 109 |
+
region_mult = {"central": 1.08, "north": 1.00, "south": 1.01, "east": 1.02, "west": 1.03}
|
| 110 |
+
|
| 111 |
+
actual = (
|
| 112 |
+
expected
|
| 113 |
+
* df["vehicle_type"].map(vehicle_mult).fillna(1)
|
| 114 |
+
* df["weather_condition"].map(weather_mult).fillna(1)
|
| 115 |
+
* df["delivery_mode"].map(mode_mult).fillna(1)
|
| 116 |
+
* df["region"].map(region_mult).fillna(1)
|
| 117 |
+
).clip(lower=0.5)
|
| 118 |
+
|
| 119 |
+
ratio = actual / expected
|
| 120 |
+
balanced_actual = np.where(
|
| 121 |
+
ratio < 0.98, expected * 0.95,
|
| 122 |
+
np.where(ratio < 1.05, expected * 1.00,
|
| 123 |
+
np.where(ratio < 1.15, expected * 1.10, expected * 1.25))
|
| 124 |
+
)
|
| 125 |
+
df["expected_time_hours"] = expected.round(2)
|
| 126 |
+
df["delivery_time_hours"] = pd.Series(balanced_actual).round(2)
|
| 127 |
+
return df
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def delay_score(delay):
|
| 131 |
+
if delay <= 0: return 5
|
| 132 |
+
if delay <= 2: return 4
|
| 133 |
+
if delay <= 5: return 3
|
| 134 |
+
if delay <= 8: return 2
|
| 135 |
+
return 1
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def performance_label(score):
|
| 139 |
+
return {5: "excellent", 4: "good", 3: "average", 2: "poor", 1: "critical"}.get(int(score), "unknown")
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def filter_df(df, vehicle, weather, mode, region):
|
| 143 |
+
out = df.copy()
|
| 144 |
+
filters = {"vehicle_type": vehicle, "weather_condition": weather, "delivery_mode": mode, "region": region}
|
| 145 |
+
for col, selected in filters.items():
|
| 146 |
+
if selected and "all" not in selected:
|
| 147 |
+
out = out[out[col].isin(selected)]
|
| 148 |
+
return out
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def kpi_html(df):
|
| 152 |
+
total = len(df)
|
| 153 |
+
delay_rate = (df["calculated_delay"].eq("yes").mean() * 100) if total else 0
|
| 154 |
+
avg_delay = df["delay_hours"].mean() if total else 0
|
| 155 |
+
avg_score = df["delay_score"].mean() if total else 0
|
| 156 |
+
cost = df["delivery_cost"].mean() if "delivery_cost" in df.columns and total else 0
|
| 157 |
+
return f"""
|
| 158 |
+
<div style='display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;'>
|
| 159 |
+
<div class='metric-card'><div class='metric-label'>Deliveries analyzed</div><div class='metric-value'>{total:,.0f}</div></div>
|
| 160 |
+
<div class='metric-card'><div class='metric-label'>Delay rate</div><div class='metric-value'>{delay_rate:.1f}%</div></div>
|
| 161 |
+
<div class='metric-card'><div class='metric-label'>Average delay hours</div><div class='metric-value'>{avg_delay:.2f}</div></div>
|
| 162 |
+
<div class='metric-card'><div class='metric-label'>Avg. delay score</div><div class='metric-value'>{avg_score:.2f}/5</div></div>
|
| 163 |
+
</div>
|
| 164 |
+
<p class='small-muted'>Average delivery cost in filtered data: {cost:,.2f}</p>
|
| 165 |
+
"""
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def group_summary(df, col):
|
| 169 |
+
return (
|
| 170 |
+
df.groupby(col, observed=False)
|
| 171 |
+
.agg(
|
| 172 |
+
deliveries=(col, "size"),
|
| 173 |
+
delay_rate=("calculated_delay", lambda x: round((x.eq("yes").mean() * 100), 2)),
|
| 174 |
+
avg_delay_hours=("delay_hours", "mean"),
|
| 175 |
+
avg_delay_score=("delay_score", "mean"),
|
| 176 |
+
avg_distance_km=("distance_km", "mean"),
|
| 177 |
+
)
|
| 178 |
+
.round(2)
|
| 179 |
+
.sort_values(["delay_rate", "avg_delay_hours"], ascending=False)
|
| 180 |
+
.reset_index()
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def make_charts(df):
|
| 185 |
+
by_vehicle = group_summary(df, "vehicle_type")
|
| 186 |
+
by_weather = group_summary(df, "weather_condition")
|
| 187 |
+
by_region = group_summary(df, "region")
|
| 188 |
+
by_mode = group_summary(df, "delivery_mode")
|
| 189 |
+
|
| 190 |
+
fig_vehicle = px.bar(by_vehicle, x="vehicle_type", y="delay_rate", text="delay_rate", title="Delay Risk by Vehicle Type")
|
| 191 |
+
fig_weather = px.bar(by_weather, x="weather_condition", y="avg_delay_hours", text="avg_delay_hours", title="Average Delay Hours by Weather")
|
| 192 |
+
fig_region = px.bar(by_region, x="region", y="delay_rate", text="delay_rate", title="Delay Rate by Region")
|
| 193 |
+
fig_mode = px.bar(by_mode, x="delivery_mode", y="avg_delay_score", text="avg_delay_score", title="Performance Score by Delivery Mode")
|
| 194 |
+
fig_scatter = px.scatter(df.sample(min(len(df), 2000), random_state=42), x="distance_km", y="delay_hours", color="weather_condition", hover_data=["vehicle_type", "delivery_mode", "region"], title="Distance vs Delay Hours")
|
| 195 |
+
|
| 196 |
+
for fig in [fig_vehicle, fig_weather, fig_region, fig_mode, fig_scatter]:
|
| 197 |
+
fig.update_layout(template="plotly_white", height=430, margin=dict(l=40, r=20, t=60, b=40))
|
| 198 |
+
return fig_vehicle, fig_weather, fig_region, fig_mode, fig_scatter
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def train_feature_importance(df):
|
| 202 |
+
model_cols = ["vehicle_type", "weather_condition", "delivery_mode", "region", "distance_category", "distance_km", "package_weight_kg"]
|
| 203 |
+
model_cols = [c for c in model_cols if c in df.columns]
|
| 204 |
+
X = df[model_cols]
|
| 205 |
+
y = df["calculated_delay"].eq("yes").astype(int)
|
| 206 |
+
cat = [c for c in model_cols if X[c].dtype == "object" or str(X[c].dtype) == "category"]
|
| 207 |
+
num = [c for c in model_cols if c not in cat]
|
| 208 |
+
pre = ColumnTransformer([("cat", OneHotEncoder(handle_unknown="ignore"), cat), ("num", "passthrough", num)])
|
| 209 |
+
clf = RandomForestClassifier(n_estimators=80, random_state=42, max_depth=7)
|
| 210 |
+
pipe = Pipeline([("pre", pre), ("clf", clf)])
|
| 211 |
+
pipe.fit(X, y)
|
| 212 |
+
names = list(pipe.named_steps["pre"].get_feature_names_out())
|
| 213 |
+
importances = pipe.named_steps["clf"].feature_importances_
|
| 214 |
+
imp = pd.DataFrame({"factor": names, "importance": importances}).sort_values("importance", ascending=False).head(12)
|
| 215 |
+
imp["factor"] = imp["factor"].str.replace("cat__", "", regex=False).str.replace("num__", "", regex=False)
|
| 216 |
+
fig = px.bar(imp.sort_values("importance"), x="importance", y="factor", orientation="h", title="AI Model: Most Important Delay-Risk Drivers")
|
| 217 |
+
fig.update_layout(template="plotly_white", height=470, margin=dict(l=120, r=20, t=60, b=40))
|
| 218 |
+
return fig, imp
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def auto_insights(df):
|
| 222 |
+
if len(df) == 0:
|
| 223 |
+
return "<div class='insight-box'>No data available for the selected filters.</div>"
|
| 224 |
+
summaries = {c: group_summary(df, c) for c in ["vehicle_type", "weather_condition", "delivery_mode", "region", "distance_category"] if c in df.columns}
|
| 225 |
+
worst = {k: v.iloc[0] for k, v in summaries.items() if len(v) > 0}
|
| 226 |
+
best = {k: v.sort_values(["delay_rate", "avg_delay_hours"], ascending=True).iloc[0] for k, v in summaries.items() if len(v) > 0}
|
| 227 |
+
top_risk_text = "<br>".join([f"• <b>{k.replace('_',' ').title()}</b>: highest risk = <b>{row[k]}</b> ({row['delay_rate']:.1f}% delay rate, {row['avg_delay_hours']:.2f} avg delay hours)" for k, row in worst.items()])
|
| 228 |
+
best_text = "<br>".join([f"• <b>{k.replace('_',' ').title()}</b>: best performer = <b>{row[k]}</b> ({row['delay_rate']:.1f}% delay rate)" for k, row in best.items()])
|
| 229 |
+
delay_rate = df["calculated_delay"].eq("yes").mean() * 100
|
| 230 |
+
recommendation = "Prioritize operational buffers for the highest-risk combinations, especially where bad weather, central routes, same-day delivery, or slower vehicle types overlap."
|
| 231 |
+
if delay_rate > 35:
|
| 232 |
+
recommendation += " The current filtered scenario has a high delay rate, so management should add contingency capacity and proactively communicate expected delays to customers."
|
| 233 |
+
else:
|
| 234 |
+
recommendation += " The current filtered scenario is relatively manageable, so management can focus on monitoring and selective process improvements."
|
| 235 |
+
return f"""
|
| 236 |
+
<div class='insight-box'>
|
| 237 |
+
<h3>AI-enhanced executive interpretation</h3>
|
| 238 |
+
<p><b>Business challenge:</b> Which operational factors create the highest delivery-delay risk, and what should management do?</p>
|
| 239 |
+
<p><b>Highest-risk factors found in the filtered data:</b><br>{top_risk_text}</p>
|
| 240 |
+
<p><b>Best-performing conditions:</b><br>{best_text}</p>
|
| 241 |
+
<p><b>Management action:</b> {recommendation}</p>
|
| 242 |
+
<p><b>Qualitative interpretation:</b> Delay risk is not only a numeric issue. It affects customer trust, service reliability, driver planning, and cost control. The dashboard therefore combines quantitative KPIs with qualitative business recommendations.</p>
|
| 243 |
+
</div>
|
| 244 |
+
"""
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def update_dashboard(file_obj, vehicle, weather, mode, region):
|
| 248 |
+
df = load_and_prepare(file_obj)
|
| 249 |
+
fdf = filter_df(df, vehicle, weather, mode, region)
|
| 250 |
+
if len(fdf) == 0:
|
| 251 |
+
raise gr.Error("Your filters produced no rows. Select fewer filters.")
|
| 252 |
+
figs = make_charts(fdf)
|
| 253 |
+
model_fig, imp = train_feature_importance(fdf)
|
| 254 |
+
sample = fdf.head(15)
|
| 255 |
+
tables = [group_summary(fdf, c) for c in ["vehicle_type", "weather_condition", "region", "delivery_mode", "distance_category"]]
|
| 256 |
+
return (kpi_html(fdf), auto_insights(fdf), *figs, model_fig, *tables, sample)
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def choices_from_data(file_obj=None):
|
| 260 |
+
df = load_and_prepare(file_obj)
|
| 261 |
+
return [
|
| 262 |
+
gr.update(choices=sorted(df["vehicle_type"].dropna().unique().tolist()), value=[]),
|
| 263 |
+
gr.update(choices=sorted(df["weather_condition"].dropna().unique().tolist()), value=[]),
|
| 264 |
+
gr.update(choices=sorted(df["delivery_mode"].dropna().unique().tolist()), value=[]),
|
| 265 |
+
gr.update(choices=sorted(df["region"].dropna().unique().tolist()), value=[]),
|
| 266 |
+
]
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
def simulate_delivery(distance, weight, vehicle, weather, mode, region):
|
| 270 |
+
row = pd.DataFrame({
|
| 271 |
+
"distance_km": [distance], "package_weight_kg": [weight], "vehicle_type": [vehicle],
|
| 272 |
+
"weather_condition": [weather], "delivery_mode": [mode], "region": [region]
|
| 273 |
+
})
|
| 274 |
+
row = create_synthetic_time_logic(row)
|
| 275 |
+
row["delay_hours"] = (row["delivery_time_hours"] - row["expected_time_hours"]).round(2)
|
| 276 |
+
row["delay_score"] = row["delay_hours"].apply(delay_score)
|
| 277 |
+
row["performance_label"] = row["delay_score"].apply(performance_label)
|
| 278 |
+
risk = "HIGH RISK" if row.loc[0, "delay_hours"] > 0 else "LOW RISK"
|
| 279 |
+
return f"""
|
| 280 |
+
### Simulation Result
|
| 281 |
+
- Expected delivery time: **{row.loc[0, 'expected_time_hours']:.2f} hours**
|
| 282 |
+
- Predicted actual delivery time: **{row.loc[0, 'delivery_time_hours']:.2f} hours**
|
| 283 |
+
- Predicted delay: **{row.loc[0, 'delay_hours']:.2f} hours**
|
| 284 |
+
- Delay score: **{row.loc[0, 'delay_score']}/5**
|
| 285 |
+
- Performance label: **{row.loc[0, 'performance_label'].title()}**
|
| 286 |
+
- Risk classification: **{risk}**
|
| 287 |
+
"""
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
def download_summary(file_obj, vehicle, weather, mode, region):
|
| 291 |
+
df = load_and_prepare(file_obj)
|
| 292 |
+
fdf = filter_df(df, vehicle, weather, mode, region)
|
| 293 |
+
summary = {
|
| 294 |
+
"rows_analyzed": len(fdf),
|
| 295 |
+
"delay_rate_percent": round(fdf["calculated_delay"].eq("yes").mean() * 100, 2),
|
| 296 |
+
"average_delay_hours": round(fdf["delay_hours"].mean(), 2),
|
| 297 |
+
"average_delay_score": round(fdf["delay_score"].mean(), 2),
|
| 298 |
+
}
|
| 299 |
+
lines = ["Delivery Delay Risk Executive Summary", "", "KPIs:"]
|
| 300 |
+
for k, v in summary.items():
|
| 301 |
+
lines.append(f"- {k.replace('_', ' ').title()}: {v}")
|
| 302 |
+
lines += ["", "Highest-risk groups:"]
|
| 303 |
+
for c in ["vehicle_type", "weather_condition", "delivery_mode", "region", "distance_category"]:
|
| 304 |
+
tab = group_summary(fdf, c)
|
| 305 |
+
row = tab.iloc[0]
|
| 306 |
+
lines.append(f"- {c}: {row[c]} | delay rate {row['delay_rate']}% | avg delay {row['avg_delay_hours']}h")
|
| 307 |
+
lines += ["", "Recommended actions:", "- Add operational buffers for high-risk weather and region combinations.", "- Match faster vehicle types to same-day and express deliveries.", "- Use the simulator before accepting risky delivery promises.", "- Monitor delay score weekly as an operational KPI."]
|
| 308 |
+
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".txt", mode="w", encoding="utf-8")
|
| 309 |
+
tmp.write("\n".join(lines))
|
| 310 |
+
tmp.close()
|
| 311 |
+
return tmp.name
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
base_df = load_and_prepare(None)
|
| 315 |
+
vehicle_choices = sorted(base_df["vehicle_type"].dropna().unique().tolist())
|
| 316 |
+
weather_choices = sorted(base_df["weather_condition"].dropna().unique().tolist())
|
| 317 |
+
mode_choices = sorted(base_df["delivery_mode"].dropna().unique().tolist())
|
| 318 |
+
region_choices = sorted(base_df["region"].dropna().unique().tolist())
|
| 319 |
+
|
| 320 |
+
with gr.Blocks(theme=gr.themes.Soft(primary_hue="indigo", neutral_hue="slate"), css=CUSTOM_CSS, title="Delivery Delay Risk Dashboard") as demo:
|
| 321 |
+
gr.Markdown("""
|
| 322 |
+
# 🚚 Delivery Delay Risk Intelligence Dashboard
|
| 323 |
+
**AI-enhanced operations dashboard for identifying delivery delay risk factors and management actions.**
|
| 324 |
+
|
| 325 |
+
Upload your CSV or use the included dataset. The app cleans the data, generates realistic delivery-time logic, calculates delay risk, visualizes operational drivers, simulates new deliveries, and creates an executive summary.
|
| 326 |
+
""")
|
| 327 |
+
|
| 328 |
+
with gr.Row():
|
| 329 |
+
file_input = gr.File(label="Optional: upload your real-world/found delivery CSV", file_types=[".csv"])
|
| 330 |
+
refresh_btn = gr.Button("Load / refresh data", variant="primary")
|
| 331 |
+
|
| 332 |
+
with gr.Accordion("Filters", open=True):
|
| 333 |
+
with gr.Row():
|
| 334 |
+
vehicle_filter = gr.Dropdown(vehicle_choices, label="Vehicle type", multiselect=True)
|
| 335 |
+
weather_filter = gr.Dropdown(weather_choices, label="Weather condition", multiselect=True)
|
| 336 |
+
mode_filter = gr.Dropdown(mode_choices, label="Delivery mode", multiselect=True)
|
| 337 |
+
region_filter = gr.Dropdown(region_choices, label="Region", multiselect=True)
|
| 338 |
+
|
| 339 |
+
kpis = gr.HTML()
|
| 340 |
+
insights = gr.HTML()
|
| 341 |
+
|
| 342 |
+
with gr.Tab("Interactive dashboard"):
|
| 343 |
+
with gr.Row():
|
| 344 |
+
fig_vehicle = gr.Plot()
|
| 345 |
+
fig_weather = gr.Plot()
|
| 346 |
+
with gr.Row():
|
| 347 |
+
fig_region = gr.Plot()
|
| 348 |
+
fig_mode = gr.Plot()
|
| 349 |
+
fig_scatter = gr.Plot()
|
| 350 |
+
|
| 351 |
+
with gr.Tab("AI risk-driver model"):
|
| 352 |
+
model_fig = gr.Plot()
|
| 353 |
+
gr.Markdown("This section trains a simple Random Forest model inside the app to estimate which factors are most important for predicting delays.")
|
| 354 |
+
|
| 355 |
+
with gr.Tab("Summary tables"):
|
| 356 |
+
with gr.Row():
|
| 357 |
+
vehicle_table = gr.Dataframe(label="Vehicle performance")
|
| 358 |
+
weather_table = gr.Dataframe(label="Weather performance")
|
| 359 |
+
with gr.Row():
|
| 360 |
+
region_table = gr.Dataframe(label="Region performance")
|
| 361 |
+
mode_table = gr.Dataframe(label="Delivery mode performance")
|
| 362 |
+
distance_table = gr.Dataframe(label="Distance category performance")
|
| 363 |
+
sample_table = gr.Dataframe(label="Cleaned sample data")
|
| 364 |
+
|
| 365 |
+
with gr.Tab("Delivery risk simulator"):
|
| 366 |
+
with gr.Row():
|
| 367 |
+
sim_distance = gr.Slider(1, 500, value=120, label="Distance km")
|
| 368 |
+
sim_weight = gr.Slider(0.1, 60, value=10, label="Package weight kg")
|
| 369 |
+
with gr.Row():
|
| 370 |
+
sim_vehicle = gr.Dropdown(vehicle_choices, value=vehicle_choices[0], label="Vehicle")
|
| 371 |
+
sim_weather = gr.Dropdown(weather_choices, value=weather_choices[0], label="Weather")
|
| 372 |
+
sim_mode = gr.Dropdown(mode_choices, value=mode_choices[0], label="Mode")
|
| 373 |
+
sim_region = gr.Dropdown(region_choices, value=region_choices[0], label="Region")
|
| 374 |
+
sim_btn = gr.Button("Simulate delivery risk", variant="primary")
|
| 375 |
+
sim_output = gr.Markdown()
|
| 376 |
+
|
| 377 |
+
with gr.Tab("Download executive summary"):
|
| 378 |
+
gr.Markdown("Generate a short text summary for your presentation/report.")
|
| 379 |
+
download_btn = gr.Button("Create executive summary file")
|
| 380 |
+
download_file = gr.File(label="Download summary")
|
| 381 |
+
|
| 382 |
+
outputs = [kpis, insights, fig_vehicle, fig_weather, fig_region, fig_mode, fig_scatter, model_fig, vehicle_table, weather_table, region_table, mode_table, distance_table, sample_table]
|
| 383 |
+
refresh_btn.click(update_dashboard, inputs=[file_input, vehicle_filter, weather_filter, mode_filter, region_filter], outputs=outputs)
|
| 384 |
+
file_input.change(choices_from_data, inputs=[file_input], outputs=[vehicle_filter, weather_filter, mode_filter, region_filter])
|
| 385 |
+
sim_btn.click(simulate_delivery, inputs=[sim_distance, sim_weight, sim_vehicle, sim_weather, sim_mode, sim_region], outputs=sim_output)
|
| 386 |
+
download_btn.click(download_summary, inputs=[file_input, vehicle_filter, weather_filter, mode_filter, region_filter], outputs=download_file)
|
| 387 |
+
demo.load(update_dashboard, inputs=[file_input, vehicle_filter, weather_filter, mode_filter, region_filter], outputs=outputs)
|
| 388 |
+
|
| 389 |
+
if __name__ == "__main__":
|
| 390 |
+
demo.launch()
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio
|
| 2 |
+
pandas
|
| 3 |
+
numpy
|
| 4 |
+
plotly
|
| 5 |
+
scikit-learn
|
synthetic_delivery_data.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|