Improve UI text clarity and fix README accuracy
Browse files
README.md
CHANGED
|
@@ -43,14 +43,13 @@ Trains two ML models on 730 days of synthetic sales data:
|
|
| 43 |
- **Linear Regression** — fast and interpretable
|
| 44 |
- **Random Forest** — captures non-linear seasonal patterns
|
| 45 |
|
| 46 |
-
|
| 47 |
|
| 48 |
### Task 6 — Business Case *(Voluntary — AI Student)*
|
| 49 |
Quantifies the financial and environmental impact of the AI system with fully interactive sliders:
|
| 50 |
- **ROI calculator** — adjusts fleet size, fuel cost, wage rates and shows live annual savings
|
| 51 |
- **3-year ROI projection** — cumulative benefit vs cost with breakeven line
|
| 52 |
-
- **CO₂ impact** — tonnes saved per year
|
| 53 |
-
- **Implementation roadmap** — 5-phase Gantt chart across 8 months
|
| 54 |
|
| 55 |
---
|
| 56 |
|
|
|
|
| 43 |
- **Linear Regression** — fast and interpretable
|
| 44 |
- **Random Forest** — captures non-linear seasonal patterns
|
| 45 |
|
| 46 |
+
Trains on 730 days of data and evaluates both models on 140 unseen test days using MAE, RMSE, R², and MAPE.
|
| 47 |
|
| 48 |
### Task 6 — Business Case *(Voluntary — AI Student)*
|
| 49 |
Quantifies the financial and environmental impact of the AI system with fully interactive sliders:
|
| 50 |
- **ROI calculator** — adjusts fleet size, fuel cost, wage rates and shows live annual savings
|
| 51 |
- **3-year ROI projection** — cumulative benefit vs cost with breakeven line
|
| 52 |
+
- **CO₂ impact** — estimated tonnes of CO₂ saved per year
|
|
|
|
| 53 |
|
| 54 |
---
|
| 55 |
|
app.py
CHANGED
|
@@ -212,9 +212,9 @@ with st.sidebar:
|
|
| 212 |
</div>""", unsafe_allow_html=True)
|
| 213 |
st.divider()
|
| 214 |
|
| 215 |
-
st.markdown("**How to
|
| 216 |
-
for n, t in [("1","Pick a tab above"),("2","
|
| 217 |
-
("3","
|
| 218 |
st.markdown(f"""<div style='display:flex;gap:8px;align-items:center;margin:5px 0;'>
|
| 219 |
<div style='background:#0f172a;color:#fff;width:18px;height:18px;border-radius:50%;
|
| 220 |
font-size:.68rem;font-weight:800;display:flex;align-items:center;
|
|
@@ -236,7 +236,7 @@ with st.sidebar:
|
|
| 236 |
unsafe_allow_html=True)
|
| 237 |
|
| 238 |
st.divider()
|
| 239 |
-
st.caption("
|
| 240 |
|
| 241 |
# ── header ────────────────────────────────────────────────────────────────────
|
| 242 |
st.markdown("""
|
|
@@ -244,7 +244,7 @@ st.markdown("""
|
|
| 244 |
padding:20px 26px;margin-bottom:18px;'>
|
| 245 |
<div style='color:#f8fafc;font-size:1.5rem;font-weight:800;margin-bottom:2px;'>EcoCart AI System</div>
|
| 246 |
<div style='color:#94a3b8;font-size:.85rem;'>
|
| 247 |
-
|
| 248 |
</div>
|
| 249 |
</div>""", unsafe_allow_html=True)
|
| 250 |
|
|
@@ -263,9 +263,10 @@ T1, T2, T3, T4, T5, T6 = st.tabs([
|
|
| 263 |
with T1:
|
| 264 |
st.markdown("""
|
| 265 |
<div class='card card-navy'>
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
|
|
|
| 269 |
</div>""", unsafe_allow_html=True)
|
| 270 |
|
| 271 |
# ── route data ────────────────────────────────────────────────────────────
|
|
@@ -307,9 +308,9 @@ with T1:
|
|
| 307 |
ROUTES = _get_routes()
|
| 308 |
RCOLS = {"Reactive Agent":BLUE, "Goal-Based Agent":GREEN, "Utility-Based Agent":AMBER}
|
| 309 |
RDESC = {
|
| 310 |
-
"Reactive Agent": "
|
| 311 |
-
"Goal-Based Agent": "Plans the full route before
|
| 312 |
-
"Utility-Based Agent":"Scores
|
| 313 |
}
|
| 314 |
|
| 315 |
# ── agent selection row ───────────────────────────────────────────────────
|
|
@@ -499,9 +500,10 @@ with T1:
|
|
| 499 |
with T2:
|
| 500 |
st.markdown("""
|
| 501 |
<div class='card card-amber'>
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
|
|
|
| 505 |
</div>""", unsafe_allow_html=True)
|
| 506 |
|
| 507 |
run_t2 = st.button("▶ Run Task 2 — Segmentation & Bias Fix",
|
|
@@ -543,9 +545,10 @@ with T2:
|
|
| 543 |
|
| 544 |
st.markdown("""
|
| 545 |
<div class='insight'>
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
|
|
|
| 549 |
</div>""", unsafe_allow_html=True)
|
| 550 |
|
| 551 |
# ══════════════════════════════════════════════════════════════════════════════
|
|
@@ -554,9 +557,10 @@ with T2:
|
|
| 554 |
with T3:
|
| 555 |
st.markdown("""
|
| 556 |
<div class='card card-blue'>
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
|
|
|
| 560 |
</div>""", unsafe_allow_html=True)
|
| 561 |
|
| 562 |
run_t3 = st.button("▶ Run Task 3 — Route Optimisation",
|
|
@@ -599,16 +603,18 @@ with T3:
|
|
| 599 |
|
| 600 |
st.markdown("""
|
| 601 |
<div class='insight'>
|
| 602 |
-
A* found the
|
| 603 |
-
BFS found same path
|
| 604 |
-
|
|
|
|
|
|
|
| 605 |
</div>""", unsafe_allow_html=True)
|
| 606 |
|
| 607 |
# ── interactive route replay ──────────────────────────────────────────────
|
| 608 |
st.markdown("<br>", unsafe_allow_html=True)
|
| 609 |
st.markdown("""
|
| 610 |
<div style='font-weight:700;font-size:1rem;color:#0f172a;margin-bottom:10px;'>
|
| 611 |
-
|
| 612 |
</div>""", unsafe_allow_html=True)
|
| 613 |
|
| 614 |
NODES_R = {
|
|
@@ -849,9 +855,11 @@ with T3:
|
|
| 849 |
with T4:
|
| 850 |
st.markdown("""
|
| 851 |
<div class='card card-navy'>
|
| 852 |
-
|
| 853 |
-
A*
|
| 854 |
-
|
|
|
|
|
|
|
| 855 |
</div>""", unsafe_allow_html=True)
|
| 856 |
|
| 857 |
urban_data=[
|
|
@@ -898,10 +906,11 @@ with T4:
|
|
| 898 |
|
| 899 |
st.markdown("""
|
| 900 |
<div class='insight'>
|
| 901 |
-
Both algorithms found <b>identical optimal paths</b> on every route.
|
| 902 |
-
A* expanded fewer nodes every time
|
| 903 |
-
|
| 904 |
-
IDA*
|
|
|
|
| 905 |
</div>""", unsafe_allow_html=True)
|
| 906 |
|
| 907 |
# ══════════════════════════════════════════════════════════════════════════════
|
|
@@ -910,8 +919,10 @@ with T4:
|
|
| 910 |
with T5:
|
| 911 |
st.markdown("""
|
| 912 |
<div class='card card-green'>
|
| 913 |
-
|
| 914 |
-
|
|
|
|
|
|
|
| 915 |
</div>""", unsafe_allow_html=True)
|
| 916 |
|
| 917 |
run_t5 = st.button("▶ Run Task 5 — Demand Forecasting",
|
|
@@ -958,9 +969,11 @@ with T5:
|
|
| 958 |
|
| 959 |
st.markdown("""
|
| 960 |
<div class='insight'>
|
| 961 |
-
Linear Regression (R²=0.762)
|
| 962 |
-
|
| 963 |
-
|
|
|
|
|
|
|
| 964 |
</div>""", unsafe_allow_html=True)
|
| 965 |
|
| 966 |
# ══════════════════════════════════════════════════════════════════════════════
|
|
@@ -969,8 +982,11 @@ with T5:
|
|
| 969 |
with T6:
|
| 970 |
st.markdown("""
|
| 971 |
<div class='card card-amber'>
|
| 972 |
-
|
| 973 |
-
|
|
|
|
|
|
|
|
|
|
| 974 |
</div>""", unsafe_allow_html=True)
|
| 975 |
|
| 976 |
ctrl, main = st.columns([1, 3])
|
|
@@ -1029,7 +1045,8 @@ with T6:
|
|
| 1029 |
|
| 1030 |
st.markdown(f"""
|
| 1031 |
<div class='warn-box'>
|
| 1032 |
-
|
| 1033 |
-
|
| 1034 |
-
{rt_save}% A*
|
|
|
|
| 1035 |
</div>""", unsafe_allow_html=True)
|
|
|
|
| 212 |
</div>""", unsafe_allow_html=True)
|
| 213 |
st.divider()
|
| 214 |
|
| 215 |
+
st.markdown("**How to navigate**")
|
| 216 |
+
for n, t in [("1","Pick a task tab above"),("2","Tasks 2, 3 and 5 — press Run first"),
|
| 217 |
+
("3","Tasks 1 and 3 — press Play to animate"),("4","Task 6 — drag sliders to explore")]:
|
| 218 |
st.markdown(f"""<div style='display:flex;gap:8px;align-items:center;margin:5px 0;'>
|
| 219 |
<div style='background:#0f172a;color:#fff;width:18px;height:18px;border-radius:50%;
|
| 220 |
font-size:.68rem;font-weight:800;display:flex;align-items:center;
|
|
|
|
| 236 |
unsafe_allow_html=True)
|
| 237 |
|
| 238 |
st.divider()
|
| 239 |
+
st.caption("Every number and chart comes from running the actual Python scripts — nothing is hardcoded except Task 4 benchmark tables and Task 6 estimates.")
|
| 240 |
|
| 241 |
# ── header ────────────────────────────────────────────────────────────────────
|
| 242 |
st.markdown("""
|
|
|
|
| 244 |
padding:20px 26px;margin-bottom:18px;'>
|
| 245 |
<div style='color:#f8fafc;font-size:1.5rem;font-weight:800;margin-bottom:2px;'>EcoCart AI System</div>
|
| 246 |
<div style='color:#94a3b8;font-size:.85rem;'>
|
| 247 |
+
Six AI tasks, one logistics problem · Every result runs from real Python scripts
|
| 248 |
</div>
|
| 249 |
</div>""", unsafe_allow_html=True)
|
| 250 |
|
|
|
|
| 263 |
with T1:
|
| 264 |
st.markdown("""
|
| 265 |
<div class='card card-navy'>
|
| 266 |
+
Three AI agents tackle the same 9-stop delivery problem — but each thinks completely differently.
|
| 267 |
+
The <b>Reactive</b> agent rushes to the nearest stop. The <b>Goal-Based</b> agent plans the
|
| 268 |
+
whole route before leaving. The <b>Utility-Based</b> agent chases high-priority stops first.
|
| 269 |
+
Same map, same stops — very different outcomes. Press <b>Play</b> to watch or drag the slider to step through.
|
| 270 |
</div>""", unsafe_allow_html=True)
|
| 271 |
|
| 272 |
# ── route data ────────────────────────────────────────────────────────────
|
|
|
|
| 308 |
ROUTES = _get_routes()
|
| 309 |
RCOLS = {"Reactive Agent":BLUE, "Goal-Based Agent":GREEN, "Utility-Based Agent":AMBER}
|
| 310 |
RDESC = {
|
| 311 |
+
"Reactive Agent": "No planning — just go to the nearest stop. Fast to decide, but the total route is often longer.",
|
| 312 |
+
"Goal-Based Agent": "Plans the full route before moving using 2-opt optimisation. Always finds the shortest total distance.",
|
| 313 |
+
"Utility-Based Agent":"Scores every stop by priority ÷ distance. Gets to the most urgent ★ stops first, not just the closest.",
|
| 314 |
}
|
| 315 |
|
| 316 |
# ── agent selection row ───────────────────────────────────────────────────
|
|
|
|
| 500 |
with T2:
|
| 501 |
st.markdown("""
|
| 502 |
<div class='card card-amber'>
|
| 503 |
+
The K-Means model was quietly being unfair — not one rural customer made it to High Value.
|
| 504 |
+
Zero. This task figures out why that happened (the data was biased from the start) and applies
|
| 505 |
+
a three-step fix. The fairness test used is <b>Disparate Impact</b>: if rural customers are
|
| 506 |
+
less than 80% as likely as urban ones to reach High Value, the model fails. Press <b>Run</b> to see the before and after.
|
| 507 |
</div>""", unsafe_allow_html=True)
|
| 508 |
|
| 509 |
run_t2 = st.button("▶ Run Task 2 — Segmentation & Bias Fix",
|
|
|
|
| 545 |
|
| 546 |
st.markdown("""
|
| 547 |
<div class='insight'>
|
| 548 |
+
Before the fix, 0% of rural customers reached High Value — Disparate Impact was 0.0, a complete fairness failure.
|
| 549 |
+
After oversampling rural customers to match urban count, adjusting spend for the delivery cost premium (+€12),
|
| 550 |
+
and correcting frequency for order batching (×1.5), the Disparate Impact rose to <b>0.847</b> — above the 0.80 threshold.
|
| 551 |
+
The model now treats both groups fairly.
|
| 552 |
</div>""", unsafe_allow_html=True)
|
| 553 |
|
| 554 |
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
| 557 |
with T3:
|
| 558 |
st.markdown("""
|
| 559 |
<div class='card card-blue'>
|
| 560 |
+
Four algorithms, one problem: find the best delivery route on a custom 20-node map (10 urban, 10 rural stops).
|
| 561 |
+
Each algorithm searches differently — some are optimal, one is not, and the best one does it
|
| 562 |
+
with the fewest steps. Press <b>Run</b> to see the benchmark results, then use the
|
| 563 |
+
<b>interactive replay</b> below to watch any algorithm explore the network node by node.
|
| 564 |
</div>""", unsafe_allow_html=True)
|
| 565 |
|
| 566 |
run_t3 = st.button("▶ Run Task 3 — Route Optimisation",
|
|
|
|
| 603 |
|
| 604 |
st.markdown("""
|
| 605 |
<div class='insight'>
|
| 606 |
+
A* found the shortest path (5.69 km) using only 7 node expansions — the most efficient result.
|
| 607 |
+
BFS found the same optimal path but needed 11 expansions. DFS was the only algorithm that got
|
| 608 |
+
it wrong, returning a 6.84 km suboptimal route because it dives deep without comparing alternatives.
|
| 609 |
+
IDA* also found 5.69 km but needed 43 expansions — its advantage is near-zero memory use,
|
| 610 |
+
which matters at national scale but not here.
|
| 611 |
</div>""", unsafe_allow_html=True)
|
| 612 |
|
| 613 |
# ── interactive route replay ──────────────────────────────────────────────
|
| 614 |
st.markdown("<br>", unsafe_allow_html=True)
|
| 615 |
st.markdown("""
|
| 616 |
<div style='font-weight:700;font-size:1rem;color:#0f172a;margin-bottom:10px;'>
|
| 617 |
+
Try it yourself — pick any start, end, and algorithm, then replay the search step by step
|
| 618 |
</div>""", unsafe_allow_html=True)
|
| 619 |
|
| 620 |
NODES_R = {
|
|
|
|
| 855 |
with T4:
|
| 856 |
st.markdown("""
|
| 857 |
<div class='card card-navy'>
|
| 858 |
+
A* and IDA* always find the same shortest path — the question is <i>how</i> they get there.
|
| 859 |
+
A* keeps a record of every node it has visited (fast, but memory grows). IDA* forgets everything
|
| 860 |
+
and re-searches from scratch each pass, tightening its cost limit each time (slower, but uses
|
| 861 |
+
almost no memory). These benchmarks run <b>10 routes × 20 timing runs</b> to see which wins on
|
| 862 |
+
EcoCart's network — and when IDA* would be the better choice.
|
| 863 |
</div>""", unsafe_allow_html=True)
|
| 864 |
|
| 865 |
urban_data=[
|
|
|
|
| 906 |
|
| 907 |
st.markdown("""
|
| 908 |
<div class='insight'>
|
| 909 |
+
Both algorithms found <b>identical optimal paths</b> on every single route — path costs match exactly.
|
| 910 |
+
But A* was faster and expanded fewer nodes every time. The starkest example: R4→R9, where
|
| 911 |
+
A* needed 7 node expansions in 0.130 ms while IDA* needed 50 in 0.642 ms.
|
| 912 |
+
For EcoCart's current network, A* is the clear winner. IDA*'s value shows up at national scale —
|
| 913 |
+
when the network has millions of nodes and storing A*'s visited set would exhaust memory.
|
| 914 |
</div>""", unsafe_allow_html=True)
|
| 915 |
|
| 916 |
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
| 919 |
with T5:
|
| 920 |
st.markdown("""
|
| 921 |
<div class='card card-green'>
|
| 922 |
+
Can a simple model beat a complex one? Two models are trained on <b>730 days</b> of EcoCart
|
| 923 |
+
sales history — Linear Regression (transparent, fast) and Random Forest (200 decision trees,
|
| 924 |
+
captures non-linear patterns). Both are then tested on <b>140 days they have never seen</b>.
|
| 925 |
+
Press <b>Run</b> to find out which wins, and why the result might surprise you.
|
| 926 |
</div>""", unsafe_allow_html=True)
|
| 927 |
|
| 928 |
run_t5 = st.button("▶ Run Task 5 — Demand Forecasting",
|
|
|
|
| 969 |
|
| 970 |
st.markdown("""
|
| 971 |
<div class='insight'>
|
| 972 |
+
Linear Regression (R²=0.762) outperformed Random Forest (R²=0.716) — the simpler model won.
|
| 973 |
+
The reason: once you give the model last week's same-day sales (<b>lag_7</b>), the pattern
|
| 974 |
+
becomes mostly linear. Random Forest's extra complexity adds nothing. The top three predictors
|
| 975 |
+
were lag_7, lag_14, and is_promo — confirming that weekly rhythm and promotions drive
|
| 976 |
+
demand far more than anything else.
|
| 977 |
</div>""", unsafe_allow_html=True)
|
| 978 |
|
| 979 |
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
| 982 |
with T6:
|
| 983 |
st.markdown("""
|
| 984 |
<div class='card card-amber'>
|
| 985 |
+
What does all of this actually save EcoCart? This tab turns the technical results into
|
| 986 |
+
a financial model — the savings from A* routing, the rural revenue unlocked by fixing the
|
| 987 |
+
segmentation bias, and the CO₂ avoided. <b>None of these numbers are real</b> — they are
|
| 988 |
+
estimates based on typical fleet operations. Use the sliders on the left to plug in
|
| 989 |
+
EcoCart's actual numbers and see how the ROI changes.
|
| 990 |
</div>""", unsafe_allow_html=True)
|
| 991 |
|
| 992 |
ctrl, main = st.columns([1, 3])
|
|
|
|
| 1045 |
|
| 1046 |
st.markdown(f"""
|
| 1047 |
<div class='warn-box'>
|
| 1048 |
+
<b>Reminder:</b> these are estimates for illustration only — not measured values.
|
| 1049 |
+
Current inputs: {fleet} vehicles, {daily} deliveries/day, {avg_km} km avg route,
|
| 1050 |
+
{rt_save}% saving from A* routing, €{seg_rev}k rural revenue uplift assumed.
|
| 1051 |
+
Change the sliders to model your own scenario.
|
| 1052 |
</div>""", unsafe_allow_html=True)
|