Esvanth commited on
Commit
d1c21c5
Β·
1 Parent(s): 9e9684a

Fix hero stat visibility, new icons, single dashes

Browse files
Files changed (1) hide show
  1. app.py +90 -93
app.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- EcoCart AI System β€” Streamlit App
3
  NCI MSCAI | Fundamentals of AI TABA 2026
4
  """
5
 
@@ -59,7 +59,7 @@ def _build_report(t2_text, t3_text, t5_text):
59
  r = p.add_run("EcoCart AI System"); r.font.name=TNR; r.font.size=Pt(24); r.font.bold=True
60
  p.paragraph_format.space_after = Pt(8)
61
  p2 = doc.add_paragraph(); p2.alignment = WD_ALIGN_PARAGRAPH.CENTER
62
- r2 = p2.add_run("Technical Report β€” TABA Section II"); r2.font.name=TNR; r2.font.size=Pt(14)
63
  p2.paragraph_format.space_after = Pt(20)
64
  for line in ["National College of Ireland","MSc Artificial Intelligence",
65
  "Fundamentals of Artificial Intelligence","May 2026"]:
@@ -72,14 +72,14 @@ def _build_report(t2_text, t3_text, t5_text):
72
  lr2.font.name=TNR; lr2.font.size=Pt(11); lr2.font.bold=True
73
  lr2.font.color.rgb = RGBColor(37,99,235)
74
  doc.add_page_break()
75
- H("Task 2 β€” Customer Segmentation & Bias Mitigation")
76
  P("Running task2_segmentation.py produced the following output:")
77
  if t2_text: CODE(t2_text)
78
  SP(); IMG("output/bias_before_after.png","Figure 1: Customer clusters before and after bias mitigation")
79
  SP(); IMG("output/disparate_impact.png","Figure 2: Disparate Impact and High Value rates before and after fix")
80
  SP(); P("Before: DI=0.0 (biased). After: DI=0.847 (fair, above 0.80).")
81
  doc.add_page_break()
82
- H("Tasks 3 & 4 β€” Route Optimisation and Algorithm Comparison")
83
  P("Running task3_4_routing.py produced the following output:")
84
  if t3_text: CODE(t3_text)
85
  SP(); IMG("output/network_map.png","Figure 3: EcoCart 20-node delivery network")
@@ -87,7 +87,7 @@ def _build_report(t2_text, t3_text, t5_text):
87
  SP(); IMG("output/green_vs_fast.png","Figure 5: Fastest vs lowest CO2 route")
88
  SP(); P("A* found the optimal path on every route with fewest nodes expanded.")
89
  doc.add_page_break()
90
- H("Task 5 β€” Demand Forecasting with Machine Learning")
91
  P("Running task5_forecasting.py produced the following output:")
92
  if t5_text: CODE(t5_text)
93
  SP(); IMG("output/forecast.png","Figure 6: Actual vs predicted daily sales")
@@ -375,9 +375,9 @@ with st.sidebar:
375
 
376
  st.markdown("<div class='sb-section'>How to use</div>", unsafe_allow_html=True)
377
  for n, t in [("1","Pick a task tab above"),
378
- ("2","Tasks 2, 3, 5 β€” press Run"),
379
- ("3","Tasks 1 & 3 β€” press Play"),
380
- ("4","Task 6 β€” adjust the sliders")]:
381
  st.markdown(f"""<div class='sb-step'>
382
  <div class='sb-num'>{n}</div>
383
  <span class='sb-step-txt'>{t}</span></div>""", unsafe_allow_html=True)
@@ -387,9 +387,9 @@ with st.sidebar:
387
  t3_done = st.session_state.get("t3_done", False)
388
  t5_done = st.session_state.get("t5_done", False)
389
  for lbl, icon, done in [
390
- ("Task 2 β€” Bias", "βš–οΈ", t2_done),
391
- ("Task 3 β€” Routes", "πŸ—ΊοΈ", t3_done),
392
- ("Task 5 β€” Forecast", "πŸ“ˆ", t5_done),
393
  ]:
394
  cls = "sb-done" if done else "sb-pending"
395
  mark = "βœ“" if done else "Β·"
@@ -403,50 +403,49 @@ with st.sidebar:
403
  st.markdown("""
404
  <div class='hero'>
405
  <div class='hero-title'>EcoCart AI System</div>
406
- <div class='hero-sub'>Six AI tasks built to solve one real logistics problem β€” every chart and number runs from actual Python scripts</div>
407
- <div style='display:flex;gap:10px;flex-wrap:wrap;'>
408
- <div style='background:rgba(96,165,250,.13);border:1px solid rgba(96,165,250,.22);border-radius:14px;padding:12px 20px;text-align:center;min-width:80px;'>
409
- <div style='color:#60a5fa;font-size:1.6rem;font-weight:900;letter-spacing:-.04em;line-height:1;'>6</div>
410
- <div style='color:#475569;font-size:.6rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;margin-top:5px;'>Tasks</div>
411
  </div>
412
- <div style='background:rgba(167,139,250,.13);border:1px solid rgba(167,139,250,.22);border-radius:14px;padding:12px 20px;text-align:center;min-width:80px;'>
413
- <div style='color:#a78bfa;font-size:1.6rem;font-weight:900;letter-spacing:-.04em;line-height:1;'>4</div>
414
- <div style='color:#475569;font-size:.6rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;margin-top:5px;'>Algorithms</div>
415
  </div>
416
- <div style='background:rgba(251,191,36,.13);border:1px solid rgba(251,191,36,.22);border-radius:14px;padding:12px 20px;text-align:center;min-width:80px;'>
417
- <div style='color:#fbbf24;font-size:1.6rem;font-weight:900;letter-spacing:-.04em;line-height:1;'>730</div>
418
- <div style='color:#475569;font-size:.6rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;margin-top:5px;'>Days Data</div>
419
  </div>
420
- <div style='background:rgba(52,211,153,.13);border:1px solid rgba(52,211,153,.22);border-radius:14px;padding:12px 20px;text-align:center;min-width:80px;'>
421
- <div style='color:#34d399;font-size:1.6rem;font-weight:900;letter-spacing:-.04em;line-height:1;'>20</div>
422
- <div style='color:#475569;font-size:.6rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;margin-top:5px;'>Node Network</div>
423
  </div>
424
- <div style='background:rgba(34,211,238,.13);border:1px solid rgba(34,211,238,.22);border-radius:14px;padding:12px 20px;text-align:center;min-width:80px;'>
425
- <div style='color:#22d3ee;font-size:1.6rem;font-weight:900;letter-spacing:-.04em;line-height:1;'>0.847</div>
426
- <div style='color:#475569;font-size:.6rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;margin-top:5px;'>DI Score</div>
427
  </div>
428
  </div>
429
  </div>""", unsafe_allow_html=True)
430
 
431
  T1, T2, T3, T4, T5, T6 = st.tabs([
432
- "πŸ€– Task 1 β€” AI Agents",
433
- "βš–οΈ Task 2 β€” Bias",
434
- "πŸ—ΊοΈ Task 3 β€” Routes",
435
- "πŸ“Š Task 4 β€” A* vs IDA*",
436
- "πŸ“ˆ Task 5 β€” Forecast",
437
- "πŸ’Ό Task 6 β€” Business",
438
  ])
439
 
440
  # ══════════════════════════════════════════════════════════════════════════════
441
- # TASK 1 β€” AI AGENTS (step-by-step animated map)
442
  # ══════════════════════════════════════════════════════════════════════════════
443
  with T1:
444
  st.markdown("""
445
  <div class='task-card'>
446
- <div class='task-icon' style='background:linear-gradient(135deg,#4f46e5,#6366f1);box-shadow:0 6px 20px rgba(99,102,241,.35);'>
447
  <svg width="24" height="24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
448
- <rect x="9" y="9" width="6" height="6"/><rect x="5" y="5" width="14" height="14" rx="2"/>
449
- <path d="M9 1v4M15 1v4M9 19v4M15 19v4M1 9h4M1 15h4M19 9h4M19 15h4"/>
450
  </svg>
451
  </div>
452
  <div>
@@ -497,7 +496,7 @@ with T1:
497
  ROUTES = _get_routes()
498
  RCOLS = {"Reactive Agent":BLUE, "Goal-Based Agent":GREEN, "Utility-Based Agent":AMBER}
499
  RDESC = {
500
- "Reactive Agent": "No planning β€” just go to the nearest stop. Fast to decide, but the total route is often longer.",
501
  "Goal-Based Agent": "Plans the full route before moving using 2-opt optimisation. Always finds the shortest total distance.",
502
  "Utility-Based Agent":"Scores every stop by priority Γ· distance. Gets to the most urgent β˜… stops first, not just the closest.",
503
  }
@@ -569,7 +568,7 @@ with T1:
569
  ag_speed = cb5.slider("Speed", 1, 8, 3, format="%dx",
570
  label_visibility="collapsed", key="ag_speed")
571
 
572
- # step slider β€” use value= so auto-play can write to ag_stp freely
573
  new_stp = st.slider("Step", 0, mx,
574
  value=st.session_state["ag_stp"],
575
  format="Stop %d",
@@ -606,7 +605,7 @@ with T1:
606
  fig.add_trace(go.Scatter(x=[x1,x2,None],y=[y1,y2,None],mode="lines",
607
  line=dict(color="#dde6f0",width=1.5),showlegend=False,hoverinfo="skip"))
608
 
609
- # traveled path β€” drawn so far (thick animated line)
610
  if len(path_sf) > 1:
611
  px=[STOPS[n][0] for n in path_sf]; py=[STOPS[n][1] for n in path_sf]
612
  fig.add_trace(go.Scatter(x=px,y=py,mode="lines",
@@ -621,9 +620,9 @@ with T1:
621
  text=[star+name.replace("Shop","").strip()],
622
  textposition="top center",textfont=dict(size=9,color="#94a3b8"),
623
  showlegend=False,
624
- hovertemplate=f"<b>{name}</b><br>Priority {pri}/5 β€” not visited yet<extra></extra>"))
625
 
626
- # visited nodes β€” show visit order number inside circle
627
  for i,name in enumerate(path_sf):
628
  if name=="Depot" or name==route[stp]: continue
629
  nx,ny,pri=STOPS[name]
@@ -632,9 +631,9 @@ with T1:
632
  text=[str(i)],textposition="middle center",
633
  textfont=dict(size=10,color="#fff",family="monospace"),
634
  showlegend=False,
635
- hovertemplate=f"<b>{name}</b><br>Stop #{i} β€” delivered βœ“<extra></extra>"))
636
 
637
- # current node β€” large, distinct, clearly highlighted
638
  cn=route[stp]; cx,cy,cpri=STOPS[cn]
639
  if cn!="Depot":
640
  star="β˜… " if cpri>=4 else ""
@@ -644,7 +643,7 @@ with T1:
644
  textposition="top center",
645
  textfont=dict(size=10,color=SLATE,family="system-ui",weight=700),
646
  showlegend=False,
647
- hovertemplate=f"<b>{cn}</b><br>Delivering here now β€” Priority {cpri}/5<extra></extra>"))
648
 
649
  # depot (always on top)
650
  dx,dy,_=STOPS["Depot"]
@@ -684,15 +683,15 @@ with T1:
684
  st.session_state["ag_play"] = False
685
 
686
  # ══════════════════════════════════════════════════════════════════════════════
687
- # TASK 2 β€” BIAS
688
  # ══════════════════════════════════════════════════════════════════════════════
689
  with T2:
690
  st.markdown("""
691
  <div class='task-card'>
692
- <div class='task-icon' style='background:linear-gradient(135deg,#d97706,#f59e0b);box-shadow:0 6px 20px rgba(217,119,6,.35);'>
693
  <svg width="24" height="24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
694
- <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
695
- <polyline points="9 12 11 14 15 10"/>
696
  </svg>
697
  </div>
698
  <div>
@@ -704,7 +703,7 @@ with T2:
704
  </div>
705
  </div>""", unsafe_allow_html=True)
706
 
707
- run_t2 = st.button("β–Ά Run Task 2 β€” Segmentation & Bias Fix",
708
  type="primary", use_container_width=True, key="run_t2")
709
  if run_t2 or st.session_state.get("t2_done"):
710
  st.session_state["t2_done"] = True
@@ -738,27 +737,27 @@ with T2:
738
  with c2:
739
  if os.path.exists("output/disparate_impact.png"):
740
  st.image("output/disparate_impact.png",
741
- caption="Fairness metrics β€” before vs after",
742
  use_container_width=True)
743
 
744
  st.markdown("""
745
  <div class='insight'>
746
- Before the fix, 0% of rural customers reached High Value β€” Disparate Impact was 0.0, a complete fairness failure.
747
  After oversampling rural customers to match urban count, adjusting spend for the delivery cost premium (+€12),
748
- and correcting frequency for order batching (Γ—1.5), the Disparate Impact rose to <b>0.847</b> β€” above the 0.80 threshold.
749
  The model now treats both groups fairly.
750
  </div>""", unsafe_allow_html=True)
751
 
752
  # ══════════════════════════════════════════════════════════════════════════════
753
- # TASK 3 β€” ROUTES (run + animated exploration replay)
754
  # ══════════════════════════════════════════════════════════════════════════════
755
  with T3:
756
  st.markdown("""
757
  <div class='task-card'>
758
- <div class='task-icon' style='background:linear-gradient(135deg,#0284c7,#38bdf8);box-shadow:0 6px 20px rgba(2,132,199,.35);'>
759
  <svg width="24" height="24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
760
- <polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/>
761
- <line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/>
762
  </svg>
763
  </div>
764
  <div>
@@ -770,7 +769,7 @@ with T3:
770
  </div>
771
  </div>""", unsafe_allow_html=True)
772
 
773
- run_t3 = st.button("β–Ά Run Task 3 β€” Route Optimisation",
774
  type="primary", use_container_width=True, key="run_t3")
775
  if run_t3 or st.session_state.get("t3_done"):
776
  st.session_state["t3_done"] = True
@@ -810,16 +809,16 @@ with T3:
810
 
811
  st.markdown("""
812
  <div class='insight'>
813
- A* found the shortest path (5.69 km) using only 7 node expansions β€” the most efficient result.
814
  BFS found the same optimal path but needed 11 expansions. DFS was the only algorithm that got
815
  it wrong, returning a 6.84 km suboptimal route because it dives deep without comparing alternatives.
816
- IDA* also found 5.69 km but needed 43 expansions β€” its advantage is near-zero memory use,
817
  which matters at national scale but not here.
818
  </div>""", unsafe_allow_html=True)
819
 
820
  # ── interactive route replay ──────────────────────────────────────────────
821
  st.markdown("<br>", unsafe_allow_html=True)
822
- st.markdown("<div class='sec-head'>Live search replay β€” pick start, end and algorithm, watch it think</div>",
823
  unsafe_allow_html=True)
824
 
825
  NODES_R = {
@@ -906,10 +905,10 @@ with T3:
906
  bound=t
907
 
908
  ALGO_DESC = {
909
- "A*": "Guided heuristic β€” expands fewest nodes, always optimal",
910
- "BFS": "Level-by-level β€” optimal shortest hops, explores broadly",
911
- "DFS": "Deep dive β€” fast but not guaranteed to find shortest path",
912
- "IDA*": "Iterative A* β€” optimal like A*, uses almost no memory",
913
  }
914
 
915
  # config row
@@ -919,7 +918,7 @@ with T3:
919
  en = cfg2.selectbox("End", all_n, index=19, key="r_en")
920
  algo = cfg3.radio("Algorithm", ["A*","BFS","DFS","IDA*"], key="r_algo", horizontal=True)
921
  rp_speed = cfg4.slider("Speed", 1, 8, 3, format="%dx", key="rp_spd")
922
- st.caption(f"**{algo}** β€” {ALGO_DESC[algo]}")
923
 
924
  fn = {"A*":_astar, "BFS":_bfs, "DFS":_dfs, "IDA*":_idastar}[algo]
925
  if sn != en:
@@ -955,7 +954,7 @@ with T3:
955
  if st.session_state["rp"] >= max_rp: st.session_state["rp"]=0
956
  st.session_state["rp_pl"] = not st.session_state["rp_pl"]; st.rerun()
957
 
958
- # slider β€” use value= so auto-play can write to rp freely
959
  new_rp = st.slider("Nodes explored", 0, max_rp,
960
  value=st.session_state["rp"],
961
  help="Drag to replay how the algorithm searches node by node")
@@ -1055,23 +1054,23 @@ with T3:
1055
  st.session_state["rp_pl"] = False
1056
 
1057
  # ══════════════════════════════════════════════════════════════════════════════
1058
- # TASK 4 β€” A* vs IDA*
1059
  # ══════════════════════════════════════════════════════════════════════════════
1060
  with T4:
1061
  st.markdown("""
1062
  <div class='task-card'>
1063
- <div class='task-icon' style='background:linear-gradient(135deg,#7c3aed,#a78bfa);box-shadow:0 6px 20px rgba(124,58,237,.35);'>
1064
  <svg width="24" height="24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
1065
- <line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/>
1066
- <line x1="6" y1="20" x2="6" y2="14"/><line x1="2" y1="20" x2="22" y2="20"/>
1067
  </svg>
1068
  </div>
1069
  <div>
1070
  <div class='task-title'>Same shortest path, completely different strategies</div>
1071
- <div class='task-desc'>A* remembers every node it visits β€” fast, but memory grows with the network.
1072
- IDA* forgets and re-searches from scratch each pass, tightening its cost bound each time β€” slower
1073
  but uses almost no memory. This benchmark runs <b>10 routes Γ— 20 timing runs</b> across urban
1074
- and rural pairs to find out which algorithm is right for EcoCart β€” and at what scale that answer changes.</div>
1075
  </div>
1076
  </div>""", unsafe_allow_html=True)
1077
 
@@ -1119,7 +1118,7 @@ with T4:
1119
 
1120
  st.markdown("""
1121
  <div class='insight'>
1122
- Both algorithms found <b>identical optimal paths</b> on every single route β€” path costs match exactly.
1123
  But A* was faster and expanded fewer nodes every time. The starkest example: R4β†’R9, where
1124
  A* needed 7 node expansions in 0.130 ms while IDA* needed 50 in 0.642 ms.
1125
  For EcoCart's current network, A* is the clear winner. IDA*'s value shows up at national scale β€”
@@ -1127,15 +1126,14 @@ with T4:
1127
  </div>""", unsafe_allow_html=True)
1128
 
1129
  # ══════════════════════════════════════════════════════════════════════════════
1130
- # TASK 5 β€” FORECASTING
1131
  # ══════════════════════════════════════════════════════════════════════════════
1132
  with T5:
1133
  st.markdown("""
1134
  <div class='task-card'>
1135
- <div class='task-icon' style='background:linear-gradient(135deg,#059669,#34d399);box-shadow:0 6px 20px rgba(5,150,105,.35);'>
1136
  <svg width="24" height="24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
1137
- <polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/>
1138
- <polyline points="17 6 23 6 23 12"/>
1139
  </svg>
1140
  </div>
1141
  <div>
@@ -1143,11 +1141,11 @@ with T5:
1143
  <div class='task-desc'>Linear Regression (fast, transparent) goes head-to-head against
1144
  Random Forest (200 trees, non-linear patterns). Both train on <b>730 days</b> of EcoCart
1145
  sales history and are tested blind on <b>140 days they have never seen</b>.
1146
- Press <b>Run</b> to see which model wins on MAE, RMSE, RΒ², and MAPE β€” and why the result is surprising.</div>
1147
  </div>
1148
  </div>""", unsafe_allow_html=True)
1149
 
1150
- run_t5 = st.button("β–Ά Run Task 5 β€” Demand Forecasting",
1151
  type="primary", use_container_width=True, key="run_t5")
1152
  if run_t5 or st.session_state.get("t5_done"):
1153
  st.session_state["t5_done"] = True
@@ -1173,12 +1171,12 @@ with T5:
1173
  </div><div class='term-body'>{t5_out}</div></div>""", unsafe_allow_html=True)
1174
 
1175
  m1,m2,m3,m4=st.columns(4)
1176
- m1.metric("LR β€” MAE","9.62 units"); m2.metric("LR β€” RΒ²","0.762")
1177
- m3.metric("RF β€” MAE","9.75 units"); m4.metric("RF β€” RΒ²","0.716")
1178
 
1179
  if os.path.exists("output/forecast.png"):
1180
  st.image("output/forecast.png",
1181
- caption="Actual vs predicted sales β€” 140 test days",
1182
  use_container_width=True)
1183
  c1,c2=st.columns(2)
1184
  with c1:
@@ -1191,25 +1189,24 @@ with T5:
1191
 
1192
  st.markdown("""
1193
  <div class='insight'>
1194
- Linear Regression won on <b>both accuracy and speed</b> β€” RΒ²=0.762 vs Random Forest's 0.716,
1195
  and a fraction of the training time (LR is a single matrix solve; RF trains 200 trees on
1196
  bootstrap samples). The reason LR wins here: once lag_7 (same weekday last week) is in the
1197
  features, the demand signal becomes mostly linear. Random Forest's complexity adds noise, not signal.
1198
- Top predictors: <b>lag_7</b>, <b>lag_14</b>, <b>is_promo</b> β€” weekly rhythm and promotions
1199
  drive demand more than anything else.
1200
  </div>""", unsafe_allow_html=True)
1201
 
1202
  # ══════════════════════════════════════════════════════════════════════════════
1203
- # TASK 6 β€” BUSINESS CASE
1204
  # ══════════════════════════════════════════════════════════════════════════════
1205
  with T6:
1206
  st.markdown("""
1207
  <div class='task-card'>
1208
- <div class='task-icon' style='background:linear-gradient(135deg,#ea580c,#fb923c);box-shadow:0 6px 20px rgba(234,88,12,.35);'>
1209
  <svg width="24" height="24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
1210
- <rect x="2" y="7" width="20" height="14" rx="2"/>
1211
- <path d="M16 7V5a2 2 0 00-2-2h-4a2 2 0 00-2 2v2"/>
1212
- <line x1="12" y1="12" x2="12" y2="16"/><line x1="10" y1="14" x2="14" y2="14"/>
1213
  </svg>
1214
  </div>
1215
  <div>
@@ -1278,7 +1275,7 @@ with T6:
1278
 
1279
  st.markdown(f"""
1280
  <div class='warn-box'>
1281
- <b>Reminder:</b> these are estimates for illustration only β€” not measured values.
1282
  Current inputs: {fleet} vehicles, {daily} deliveries/day, {avg_km} km avg route,
1283
  {rt_save}% saving from A* routing, €{seg_rev}k rural revenue uplift assumed.
1284
  Change the sliders to model your own scenario.
 
1
  """
2
+ EcoCart AI System - Streamlit App
3
  NCI MSCAI | Fundamentals of AI TABA 2026
4
  """
5
 
 
59
  r = p.add_run("EcoCart AI System"); r.font.name=TNR; r.font.size=Pt(24); r.font.bold=True
60
  p.paragraph_format.space_after = Pt(8)
61
  p2 = doc.add_paragraph(); p2.alignment = WD_ALIGN_PARAGRAPH.CENTER
62
+ r2 = p2.add_run("Technical Report - TABA Section II"); r2.font.name=TNR; r2.font.size=Pt(14)
63
  p2.paragraph_format.space_after = Pt(20)
64
  for line in ["National College of Ireland","MSc Artificial Intelligence",
65
  "Fundamentals of Artificial Intelligence","May 2026"]:
 
72
  lr2.font.name=TNR; lr2.font.size=Pt(11); lr2.font.bold=True
73
  lr2.font.color.rgb = RGBColor(37,99,235)
74
  doc.add_page_break()
75
+ H("Task 2 - Customer Segmentation & Bias Mitigation")
76
  P("Running task2_segmentation.py produced the following output:")
77
  if t2_text: CODE(t2_text)
78
  SP(); IMG("output/bias_before_after.png","Figure 1: Customer clusters before and after bias mitigation")
79
  SP(); IMG("output/disparate_impact.png","Figure 2: Disparate Impact and High Value rates before and after fix")
80
  SP(); P("Before: DI=0.0 (biased). After: DI=0.847 (fair, above 0.80).")
81
  doc.add_page_break()
82
+ H("Tasks 3 & 4 - Route Optimisation and Algorithm Comparison")
83
  P("Running task3_4_routing.py produced the following output:")
84
  if t3_text: CODE(t3_text)
85
  SP(); IMG("output/network_map.png","Figure 3: EcoCart 20-node delivery network")
 
87
  SP(); IMG("output/green_vs_fast.png","Figure 5: Fastest vs lowest CO2 route")
88
  SP(); P("A* found the optimal path on every route with fewest nodes expanded.")
89
  doc.add_page_break()
90
+ H("Task 5 - Demand Forecasting with Machine Learning")
91
  P("Running task5_forecasting.py produced the following output:")
92
  if t5_text: CODE(t5_text)
93
  SP(); IMG("output/forecast.png","Figure 6: Actual vs predicted daily sales")
 
375
 
376
  st.markdown("<div class='sb-section'>How to use</div>", unsafe_allow_html=True)
377
  for n, t in [("1","Pick a task tab above"),
378
+ ("2","Tasks 2, 3, 5 - press Run"),
379
+ ("3","Tasks 1 & 3 - press Play"),
380
+ ("4","Task 6 - adjust the sliders")]:
381
  st.markdown(f"""<div class='sb-step'>
382
  <div class='sb-num'>{n}</div>
383
  <span class='sb-step-txt'>{t}</span></div>""", unsafe_allow_html=True)
 
387
  t3_done = st.session_state.get("t3_done", False)
388
  t5_done = st.session_state.get("t5_done", False)
389
  for lbl, icon, done in [
390
+ ("Task 2 - Bias", "βš–οΈ", t2_done),
391
+ ("Task 3 - Routes", "πŸ—ΊοΈ", t3_done),
392
+ ("Task 5 - Forecast", "πŸ“ˆ", t5_done),
393
  ]:
394
  cls = "sb-done" if done else "sb-pending"
395
  mark = "βœ“" if done else "Β·"
 
403
  st.markdown("""
404
  <div class='hero'>
405
  <div class='hero-title'>EcoCart AI System</div>
406
+ <div class='hero-sub'>Six AI tasks built to solve one real logistics problem - every chart and number runs from actual Python scripts</div>
407
+ <div style='display:flex;gap:10px;flex-wrap:wrap;margin-top:4px;'>
408
+ <div style='background:rgba(96,165,250,.18);border:1.5px solid rgba(96,165,250,.4);border-radius:14px;padding:14px 22px;text-align:center;min-width:86px;backdrop-filter:blur(8px);'>
409
+ <div style='color:#93c5fd;font-size:1.7rem;font-weight:900;letter-spacing:-.04em;line-height:1;'>6</div>
410
+ <div style='color:#7dd3fc;font-size:.58rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;margin-top:6px;'>Tasks</div>
411
  </div>
412
+ <div style='background:rgba(167,139,250,.18);border:1.5px solid rgba(167,139,250,.4);border-radius:14px;padding:14px 22px;text-align:center;min-width:86px;backdrop-filter:blur(8px);'>
413
+ <div style='color:#c4b5fd;font-size:1.7rem;font-weight:900;letter-spacing:-.04em;line-height:1;'>4</div>
414
+ <div style='color:#c4b5fd;font-size:.58rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;margin-top:6px;'>Algorithms</div>
415
  </div>
416
+ <div style='background:rgba(251,191,36,.18);border:1.5px solid rgba(251,191,36,.4);border-radius:14px;padding:14px 22px;text-align:center;min-width:86px;backdrop-filter:blur(8px);'>
417
+ <div style='color:#fde68a;font-size:1.7rem;font-weight:900;letter-spacing:-.04em;line-height:1;'>730</div>
418
+ <div style='color:#fde68a;font-size:.58rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;margin-top:6px;'>Days Data</div>
419
  </div>
420
+ <div style='background:rgba(52,211,153,.18);border:1.5px solid rgba(52,211,153,.4);border-radius:14px;padding:14px 22px;text-align:center;min-width:86px;backdrop-filter:blur(8px);'>
421
+ <div style='color:#6ee7b7;font-size:1.7rem;font-weight:900;letter-spacing:-.04em;line-height:1;'>20</div>
422
+ <div style='color:#6ee7b7;font-size:.58rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;margin-top:6px;'>Node Network</div>
423
  </div>
424
+ <div style='background:rgba(34,211,238,.18);border:1.5px solid rgba(34,211,238,.4);border-radius:14px;padding:14px 22px;text-align:center;min-width:86px;backdrop-filter:blur(8px);'>
425
+ <div style='color:#67e8f9;font-size:1.7rem;font-weight:900;letter-spacing:-.04em;line-height:1;'>0.847</div>
426
+ <div style='color:#67e8f9;font-size:.58rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;margin-top:6px;'>DI Score</div>
427
  </div>
428
  </div>
429
  </div>""", unsafe_allow_html=True)
430
 
431
  T1, T2, T3, T4, T5, T6 = st.tabs([
432
+ "πŸ€– Task 1 - AI Agents",
433
+ "βš–οΈ Task 2 - Bias",
434
+ "πŸ—ΊοΈ Task 3 - Routes",
435
+ "πŸ“Š Task 4 - A* vs IDA*",
436
+ "πŸ“ˆ Task 5 - Forecast",
437
+ "πŸ’Ό Task 6 - Business",
438
  ])
439
 
440
  # ══════════════════════════════════════════════════════════════════════════════
441
+ # TASK 1 - AI AGENTS (step-by-step animated map)
442
  # ══════════════════════════════════════════════════════════════════════════════
443
  with T1:
444
  st.markdown("""
445
  <div class='task-card'>
446
+ <div class='task-icon' style='background:linear-gradient(135deg,#4f46e5,#818cf8);box-shadow:0 6px 20px rgba(99,102,241,.4);'>
447
  <svg width="24" height="24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
448
+ <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
 
449
  </svg>
450
  </div>
451
  <div>
 
496
  ROUTES = _get_routes()
497
  RCOLS = {"Reactive Agent":BLUE, "Goal-Based Agent":GREEN, "Utility-Based Agent":AMBER}
498
  RDESC = {
499
+ "Reactive Agent": "No planning - just go to the nearest stop. Fast to decide, but the total route is often longer.",
500
  "Goal-Based Agent": "Plans the full route before moving using 2-opt optimisation. Always finds the shortest total distance.",
501
  "Utility-Based Agent":"Scores every stop by priority Γ· distance. Gets to the most urgent β˜… stops first, not just the closest.",
502
  }
 
568
  ag_speed = cb5.slider("Speed", 1, 8, 3, format="%dx",
569
  label_visibility="collapsed", key="ag_speed")
570
 
571
+ # step slider - use value= so auto-play can write to ag_stp freely
572
  new_stp = st.slider("Step", 0, mx,
573
  value=st.session_state["ag_stp"],
574
  format="Stop %d",
 
605
  fig.add_trace(go.Scatter(x=[x1,x2,None],y=[y1,y2,None],mode="lines",
606
  line=dict(color="#dde6f0",width=1.5),showlegend=False,hoverinfo="skip"))
607
 
608
+ # traveled path - drawn so far (thick animated line)
609
  if len(path_sf) > 1:
610
  px=[STOPS[n][0] for n in path_sf]; py=[STOPS[n][1] for n in path_sf]
611
  fig.add_trace(go.Scatter(x=px,y=py,mode="lines",
 
620
  text=[star+name.replace("Shop","").strip()],
621
  textposition="top center",textfont=dict(size=9,color="#94a3b8"),
622
  showlegend=False,
623
+ hovertemplate=f"<b>{name}</b><br>Priority {pri}/5 - not visited yet<extra></extra>"))
624
 
625
+ # visited nodes - show visit order number inside circle
626
  for i,name in enumerate(path_sf):
627
  if name=="Depot" or name==route[stp]: continue
628
  nx,ny,pri=STOPS[name]
 
631
  text=[str(i)],textposition="middle center",
632
  textfont=dict(size=10,color="#fff",family="monospace"),
633
  showlegend=False,
634
+ hovertemplate=f"<b>{name}</b><br>Stop #{i} - delivered βœ“<extra></extra>"))
635
 
636
+ # current node - large, distinct, clearly highlighted
637
  cn=route[stp]; cx,cy,cpri=STOPS[cn]
638
  if cn!="Depot":
639
  star="β˜… " if cpri>=4 else ""
 
643
  textposition="top center",
644
  textfont=dict(size=10,color=SLATE,family="system-ui",weight=700),
645
  showlegend=False,
646
+ hovertemplate=f"<b>{cn}</b><br>Delivering here now - Priority {cpri}/5<extra></extra>"))
647
 
648
  # depot (always on top)
649
  dx,dy,_=STOPS["Depot"]
 
683
  st.session_state["ag_play"] = False
684
 
685
  # ══════════════════════════════════════════════════════════════════════════════
686
+ # TASK 2 - BIAS
687
  # ══════════════════════════════════════════════════════════════════════════════
688
  with T2:
689
  st.markdown("""
690
  <div class='task-card'>
691
+ <div class='task-icon' style='background:linear-gradient(135deg,#b45309,#f59e0b);box-shadow:0 6px 20px rgba(180,83,9,.4);'>
692
  <svg width="24" height="24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
693
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
694
+ <circle cx="12" cy="12" r="3"/>
695
  </svg>
696
  </div>
697
  <div>
 
703
  </div>
704
  </div>""", unsafe_allow_html=True)
705
 
706
+ run_t2 = st.button("β–Ά Run Task 2 - Segmentation & Bias Fix",
707
  type="primary", use_container_width=True, key="run_t2")
708
  if run_t2 or st.session_state.get("t2_done"):
709
  st.session_state["t2_done"] = True
 
737
  with c2:
738
  if os.path.exists("output/disparate_impact.png"):
739
  st.image("output/disparate_impact.png",
740
+ caption="Fairness metrics - before vs after",
741
  use_container_width=True)
742
 
743
  st.markdown("""
744
  <div class='insight'>
745
+ Before the fix, 0% of rural customers reached High Value - Disparate Impact was 0.0, a complete fairness failure.
746
  After oversampling rural customers to match urban count, adjusting spend for the delivery cost premium (+€12),
747
+ and correcting frequency for order batching (Γ—1.5), the Disparate Impact rose to <b>0.847</b> - above the 0.80 threshold.
748
  The model now treats both groups fairly.
749
  </div>""", unsafe_allow_html=True)
750
 
751
  # ══════════════════════════════════════════════════════════════════════════════
752
+ # TASK 3 - ROUTES (run + animated exploration replay)
753
  # ══════════════════════════════════════════════════════════════════════════════
754
  with T3:
755
  st.markdown("""
756
  <div class='task-card'>
757
+ <div class='task-icon' style='background:linear-gradient(135deg,#0369a1,#38bdf8);box-shadow:0 6px 20px rgba(3,105,161,.4);'>
758
  <svg width="24" height="24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
759
+ <circle cx="6" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/>
760
+ <path d="M6 9v6"/><path d="M9 6h6"/><path d="M9 6a9 9 0 009 9"/>
761
  </svg>
762
  </div>
763
  <div>
 
769
  </div>
770
  </div>""", unsafe_allow_html=True)
771
 
772
+ run_t3 = st.button("β–Ά Run Task 3 - Route Optimisation",
773
  type="primary", use_container_width=True, key="run_t3")
774
  if run_t3 or st.session_state.get("t3_done"):
775
  st.session_state["t3_done"] = True
 
809
 
810
  st.markdown("""
811
  <div class='insight'>
812
+ A* found the shortest path (5.69 km) using only 7 node expansions - the most efficient result.
813
  BFS found the same optimal path but needed 11 expansions. DFS was the only algorithm that got
814
  it wrong, returning a 6.84 km suboptimal route because it dives deep without comparing alternatives.
815
+ IDA* also found 5.69 km but needed 43 expansions - its advantage is near-zero memory use,
816
  which matters at national scale but not here.
817
  </div>""", unsafe_allow_html=True)
818
 
819
  # ── interactive route replay ──────────────────────────────────────────────
820
  st.markdown("<br>", unsafe_allow_html=True)
821
+ st.markdown("<div class='sec-head'>Live search replay - pick start, end and algorithm, watch it think</div>",
822
  unsafe_allow_html=True)
823
 
824
  NODES_R = {
 
905
  bound=t
906
 
907
  ALGO_DESC = {
908
+ "A*": "Guided heuristic - expands fewest nodes, always optimal",
909
+ "BFS": "Level-by-level - optimal shortest hops, explores broadly",
910
+ "DFS": "Deep dive - fast but not guaranteed to find shortest path",
911
+ "IDA*": "Iterative A* - optimal like A*, uses almost no memory",
912
  }
913
 
914
  # config row
 
918
  en = cfg2.selectbox("End", all_n, index=19, key="r_en")
919
  algo = cfg3.radio("Algorithm", ["A*","BFS","DFS","IDA*"], key="r_algo", horizontal=True)
920
  rp_speed = cfg4.slider("Speed", 1, 8, 3, format="%dx", key="rp_spd")
921
+ st.caption(f"**{algo}** - {ALGO_DESC[algo]}")
922
 
923
  fn = {"A*":_astar, "BFS":_bfs, "DFS":_dfs, "IDA*":_idastar}[algo]
924
  if sn != en:
 
954
  if st.session_state["rp"] >= max_rp: st.session_state["rp"]=0
955
  st.session_state["rp_pl"] = not st.session_state["rp_pl"]; st.rerun()
956
 
957
+ # slider - use value= so auto-play can write to rp freely
958
  new_rp = st.slider("Nodes explored", 0, max_rp,
959
  value=st.session_state["rp"],
960
  help="Drag to replay how the algorithm searches node by node")
 
1054
  st.session_state["rp_pl"] = False
1055
 
1056
  # ══════════════════════════════════════════════════════════════════════════════
1057
+ # TASK 4 - A* vs IDA*
1058
  # ══════════════════════════════════════════════════════════════════════════════
1059
  with T4:
1060
  st.markdown("""
1061
  <div class='task-card'>
1062
+ <div class='task-icon' style='background:linear-gradient(135deg,#6d28d9,#a78bfa);box-shadow:0 6px 20px rgba(109,40,217,.4);'>
1063
  <svg width="24" height="24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
1064
+ <circle cx="12" cy="12" r="10"/>
1065
+ <polyline points="12 6 12 12 16 14"/>
1066
  </svg>
1067
  </div>
1068
  <div>
1069
  <div class='task-title'>Same shortest path, completely different strategies</div>
1070
+ <div class='task-desc'>A* remembers every node it visits - fast, but memory grows with the network.
1071
+ IDA* forgets and re-searches from scratch each pass, tightening its cost bound each time - slower
1072
  but uses almost no memory. This benchmark runs <b>10 routes Γ— 20 timing runs</b> across urban
1073
+ and rural pairs to find out which algorithm is right for EcoCart - and at what scale that answer changes.</div>
1074
  </div>
1075
  </div>""", unsafe_allow_html=True)
1076
 
 
1118
 
1119
  st.markdown("""
1120
  <div class='insight'>
1121
+ Both algorithms found <b>identical optimal paths</b> on every single route - path costs match exactly.
1122
  But A* was faster and expanded fewer nodes every time. The starkest example: R4β†’R9, where
1123
  A* needed 7 node expansions in 0.130 ms while IDA* needed 50 in 0.642 ms.
1124
  For EcoCart's current network, A* is the clear winner. IDA*'s value shows up at national scale β€”
 
1126
  </div>""", unsafe_allow_html=True)
1127
 
1128
  # ══════════════════════════════════════════════════════════════════════════════
1129
+ # TASK 5 - FORECASTING
1130
  # ══════════════════════════════════════════════════════════════════════════════
1131
  with T5:
1132
  st.markdown("""
1133
  <div class='task-card'>
1134
+ <div class='task-icon' style='background:linear-gradient(135deg,#047857,#34d399);box-shadow:0 6px 20px rgba(4,120,87,.4);'>
1135
  <svg width="24" height="24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
1136
+ <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
 
1137
  </svg>
1138
  </div>
1139
  <div>
 
1141
  <div class='task-desc'>Linear Regression (fast, transparent) goes head-to-head against
1142
  Random Forest (200 trees, non-linear patterns). Both train on <b>730 days</b> of EcoCart
1143
  sales history and are tested blind on <b>140 days they have never seen</b>.
1144
+ Press <b>Run</b> to see which model wins on MAE, RMSE, RΒ², and MAPE - and why the result is surprising.</div>
1145
  </div>
1146
  </div>""", unsafe_allow_html=True)
1147
 
1148
+ run_t5 = st.button("β–Ά Run Task 5 - Demand Forecasting",
1149
  type="primary", use_container_width=True, key="run_t5")
1150
  if run_t5 or st.session_state.get("t5_done"):
1151
  st.session_state["t5_done"] = True
 
1171
  </div><div class='term-body'>{t5_out}</div></div>""", unsafe_allow_html=True)
1172
 
1173
  m1,m2,m3,m4=st.columns(4)
1174
+ m1.metric("LR - MAE","9.62 units"); m2.metric("LR - RΒ²","0.762")
1175
+ m3.metric("RF - MAE","9.75 units"); m4.metric("RF - RΒ²","0.716")
1176
 
1177
  if os.path.exists("output/forecast.png"):
1178
  st.image("output/forecast.png",
1179
+ caption="Actual vs predicted sales - 140 test days",
1180
  use_container_width=True)
1181
  c1,c2=st.columns(2)
1182
  with c1:
 
1189
 
1190
  st.markdown("""
1191
  <div class='insight'>
1192
+ Linear Regression won on <b>both accuracy and speed</b> - RΒ²=0.762 vs Random Forest's 0.716,
1193
  and a fraction of the training time (LR is a single matrix solve; RF trains 200 trees on
1194
  bootstrap samples). The reason LR wins here: once lag_7 (same weekday last week) is in the
1195
  features, the demand signal becomes mostly linear. Random Forest's complexity adds noise, not signal.
1196
+ Top predictors: <b>lag_7</b>, <b>lag_14</b>, <b>is_promo</b> - weekly rhythm and promotions
1197
  drive demand more than anything else.
1198
  </div>""", unsafe_allow_html=True)
1199
 
1200
  # ══════════════════════════════════════════════════════════════════════════════
1201
+ # TASK 6 - BUSINESS CASE
1202
  # ══════════════════════════════════════════════════════════════════════════════
1203
  with T6:
1204
  st.markdown("""
1205
  <div class='task-card'>
1206
+ <div class='task-icon' style='background:linear-gradient(135deg,#c2410c,#fb923c);box-shadow:0 6px 20px rgba(194,65,12,.4);'>
1207
  <svg width="24" height="24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
1208
+ <path d="M21.21 15.89A10 10 0 118 2.83"/>
1209
+ <path d="M22 12A10 10 0 0012 2v10z"/>
 
1210
  </svg>
1211
  </div>
1212
  <div>
 
1275
 
1276
  st.markdown(f"""
1277
  <div class='warn-box'>
1278
+ <b>Reminder:</b> these are estimates for illustration only - not measured values.
1279
  Current inputs: {fleet} vehicles, {daily} deliveries/day, {avg_km} km avg route,
1280
  {rt_save}% saving from A* routing, €{seg_rev}k rural revenue uplift assumed.
1281
  Change the sliders to model your own scenario.