Add output folder tracking, report generator, sidebar download button
Browse files- app.py +190 -8
- requirements.txt +1 -0
app.py
CHANGED
|
@@ -18,9 +18,167 @@ os.makedirs("output", exist_ok=True)
|
|
| 18 |
import numpy as np
|
| 19 |
import pandas as pd
|
| 20 |
import streamlit as st
|
|
|
|
|
|
|
|
|
|
| 21 |
import plotly.graph_objects as go
|
| 22 |
from plotly.subplots import make_subplots
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
# ββ page config βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 25 |
st.set_page_config(page_title="EcoCart AI", layout="wide",
|
| 26 |
initial_sidebar_state="expanded")
|
|
@@ -94,15 +252,36 @@ with st.sidebar:
|
|
| 94 |
5. Read the *What does this tell us?* section
|
| 95 |
""")
|
| 96 |
st.divider()
|
| 97 |
-
|
| 98 |
-
st.
|
| 99 |
-
st.
|
| 100 |
-
st.
|
| 101 |
-
|
| 102 |
-
st.markdown("
|
| 103 |
-
st.markdown("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
st.divider()
|
| 105 |
-
st.caption("All outputs
|
| 106 |
|
| 107 |
# ββ header ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 108 |
st.markdown("<h2 style='margin:0 0 16px;color:#1e293b'>EcoCart AI System</h2>",
|
|
@@ -306,6 +485,7 @@ exists and applies a fix to make the results fair.
|
|
| 306 |
|
| 307 |
with st.spinner("Running task2_segmentation.py..."):
|
| 308 |
t2_output = _run_task2()
|
|
|
|
| 309 |
|
| 310 |
st.markdown("<div class='step-box'><span class='step-num'>2</span>"
|
| 311 |
"<b>Terminal output from task2_segmentation.py</b></div>",
|
|
@@ -376,6 +556,7 @@ how each algorithm explores the network step by step.
|
|
| 376 |
|
| 377 |
with st.spinner("Running task3_4_routing.py..."):
|
| 378 |
t3_output = _run_task3()
|
|
|
|
| 379 |
|
| 380 |
st.markdown("<div class='step-box'><span class='step-num'>2</span>"
|
| 381 |
"<b>Terminal output from task3_4_routing.py</b></div>",
|
|
@@ -664,6 +845,7 @@ on 140 unseen test days.
|
|
| 664 |
|
| 665 |
with st.spinner("Running task5_forecasting.py..."):
|
| 666 |
t5_output = _run_task5()
|
|
|
|
| 667 |
|
| 668 |
st.markdown("<div class='step-box'><span class='step-num'>2</span>"
|
| 669 |
"<b>Terminal output from task5_forecasting.py</b></div>",
|
|
|
|
| 18 |
import numpy as np
|
| 19 |
import pandas as pd
|
| 20 |
import streamlit as st
|
| 21 |
+
from docx import Document
|
| 22 |
+
from docx.shared import Inches, Pt, RGBColor
|
| 23 |
+
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
| 24 |
import plotly.graph_objects as go
|
| 25 |
from plotly.subplots import make_subplots
|
| 26 |
|
| 27 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 28 |
+
# REPORT GENERATOR
|
| 29 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 30 |
+
|
| 31 |
+
def _build_report(t2_text, t3_text, t5_text):
|
| 32 |
+
"""Build a Word report from the task outputs stored in session state."""
|
| 33 |
+
TNR = "Times New Roman"
|
| 34 |
+
doc = Document()
|
| 35 |
+
|
| 36 |
+
# default style
|
| 37 |
+
doc.styles["Normal"].font.name = TNR
|
| 38 |
+
doc.styles["Normal"].font.size = Pt(12)
|
| 39 |
+
doc.styles["Normal"].paragraph_format.space_after = Pt(8)
|
| 40 |
+
for lvl, sz in [(1,14),(2,13)]:
|
| 41 |
+
s = doc.styles[f"Heading {lvl}"]
|
| 42 |
+
s.font.name = TNR; s.font.bold = True; s.font.size = Pt(sz)
|
| 43 |
+
s.font.color.rgb = RGBColor(0,0,0)
|
| 44 |
+
s.paragraph_format.space_before = Pt(12)
|
| 45 |
+
s.paragraph_format.space_after = Pt(4)
|
| 46 |
+
|
| 47 |
+
def H(txt, lv=1):
|
| 48 |
+
p = doc.add_heading(txt, level=lv)
|
| 49 |
+
p.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
| 50 |
+
for r in p.runs: r.font.name=TNR; r.font.color.rgb=RGBColor(0,0,0)
|
| 51 |
+
|
| 52 |
+
def P(txt):
|
| 53 |
+
p = doc.add_paragraph()
|
| 54 |
+
p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
| 55 |
+
p.paragraph_format.space_after = Pt(8)
|
| 56 |
+
r = p.add_run(txt); r.font.name=TNR; r.font.size=Pt(12)
|
| 57 |
+
|
| 58 |
+
def CODE(txt):
|
| 59 |
+
p = doc.add_paragraph()
|
| 60 |
+
p.paragraph_format.space_after = Pt(6)
|
| 61 |
+
r = p.add_run(txt)
|
| 62 |
+
r.font.name = "Courier New"; r.font.size = Pt(9)
|
| 63 |
+
|
| 64 |
+
def IMG(path, caption="", width=5.5):
|
| 65 |
+
if os.path.exists(path):
|
| 66 |
+
doc.add_picture(path, width=Inches(width))
|
| 67 |
+
doc.paragraphs[-1].alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 68 |
+
if caption:
|
| 69 |
+
cp = doc.add_paragraph(caption)
|
| 70 |
+
cp.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 71 |
+
cp.paragraph_format.space_after = Pt(8)
|
| 72 |
+
for r in cp.runs: r.font.name=TNR; r.font.size=Pt(10); r.font.italic=True
|
| 73 |
+
|
| 74 |
+
def SP(): doc.add_paragraph("").paragraph_format.space_after = Pt(4)
|
| 75 |
+
|
| 76 |
+
# ββ cover βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 77 |
+
SP(); SP()
|
| 78 |
+
p = doc.add_paragraph()
|
| 79 |
+
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 80 |
+
r = p.add_run("EcoCart AI System")
|
| 81 |
+
r.font.name=TNR; r.font.size=Pt(24); r.font.bold=True
|
| 82 |
+
p.paragraph_format.space_after = Pt(8)
|
| 83 |
+
|
| 84 |
+
p2 = doc.add_paragraph()
|
| 85 |
+
p2.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 86 |
+
r2 = p2.add_run("Technical Report β TABA Section II")
|
| 87 |
+
r2.font.name=TNR; r2.font.size=Pt(14)
|
| 88 |
+
p2.paragraph_format.space_after = Pt(20)
|
| 89 |
+
|
| 90 |
+
for line in ["National College of Ireland",
|
| 91 |
+
"MSc Artificial Intelligence",
|
| 92 |
+
"Fundamentals of Artificial Intelligence", "May 2026"]:
|
| 93 |
+
lp = doc.add_paragraph(); lp.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 94 |
+
lr = lp.add_run(line); lr.font.name=TNR; lr.font.size=Pt(12)
|
| 95 |
+
lp.paragraph_format.space_after = Pt(4)
|
| 96 |
+
|
| 97 |
+
SP()
|
| 98 |
+
lnk = doc.add_paragraph(); lnk.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 99 |
+
lr2 = lnk.add_run("Live Demo: https://esvanth-ecocart-ai.streamlit.app")
|
| 100 |
+
lr2.font.name=TNR; lr2.font.size=Pt(11); lr2.font.bold=True
|
| 101 |
+
lr2.font.color.rgb = RGBColor(37,99,235)
|
| 102 |
+
doc.add_page_break()
|
| 103 |
+
|
| 104 |
+
# ββ task 2 ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 105 |
+
H("Task 2 β Customer Segmentation & Bias Mitigation")
|
| 106 |
+
P("Running task2_segmentation.py produced the following output:")
|
| 107 |
+
if t2_text:
|
| 108 |
+
CODE(t2_text)
|
| 109 |
+
SP()
|
| 110 |
+
IMG("output/bias_before_after.png",
|
| 111 |
+
"Figure 1: Customer clusters before and after bias mitigation")
|
| 112 |
+
SP()
|
| 113 |
+
IMG("output/disparate_impact.png",
|
| 114 |
+
"Figure 2: Disparate Impact and High Value rates before and after fix")
|
| 115 |
+
SP()
|
| 116 |
+
P("Before the fix: 0.0% of rural customers were in High Value (DI = 0.0 β biased). "
|
| 117 |
+
"After the fix: 57.3% of rural customers are in High Value (DI = 0.847 β fair, above 0.80 threshold).")
|
| 118 |
+
doc.add_page_break()
|
| 119 |
+
|
| 120 |
+
# ββ task 3 ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 121 |
+
H("Tasks 3 & 4 β Route Optimisation and Algorithm Comparison")
|
| 122 |
+
P("Running task3_4_routing.py produced the following output:")
|
| 123 |
+
if t3_text:
|
| 124 |
+
CODE(t3_text)
|
| 125 |
+
SP()
|
| 126 |
+
IMG("output/network_map.png",
|
| 127 |
+
"Figure 3: EcoCart 20-node delivery network")
|
| 128 |
+
SP()
|
| 129 |
+
IMG("output/algo_comparison.png",
|
| 130 |
+
"Figure 4: A* vs IDA* comparison across urban and rural routes")
|
| 131 |
+
SP()
|
| 132 |
+
IMG("output/green_vs_fast.png",
|
| 133 |
+
"Figure 5: Fastest route vs lowest COβ route comparison")
|
| 134 |
+
SP()
|
| 135 |
+
P("A* found the optimal path on every route with the fewest nodes expanded. "
|
| 136 |
+
"DFS was the only algorithm that did not find the shortest path. "
|
| 137 |
+
"Both A* and IDA* found identical optimal paths β A* is faster on small graphs, "
|
| 138 |
+
"IDA* uses less memory and is better suited for large-scale networks.")
|
| 139 |
+
doc.add_page_break()
|
| 140 |
+
|
| 141 |
+
# ββ task 5 ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 142 |
+
H("Task 5 β Demand Forecasting with Machine Learning")
|
| 143 |
+
P("Running task5_forecasting.py produced the following output:")
|
| 144 |
+
if t5_text:
|
| 145 |
+
CODE(t5_text)
|
| 146 |
+
SP()
|
| 147 |
+
IMG("output/forecast.png",
|
| 148 |
+
"Figure 6: Actual vs predicted daily sales on the 140-day test set")
|
| 149 |
+
SP()
|
| 150 |
+
IMG("output/residuals.png",
|
| 151 |
+
"Figure 7: Residuals for Linear Regression and Random Forest")
|
| 152 |
+
SP()
|
| 153 |
+
IMG("output/feature_importance.png",
|
| 154 |
+
"Figure 8: Random Forest feature importance β lag_7 is the strongest predictor")
|
| 155 |
+
SP()
|
| 156 |
+
P("Linear Regression: MAE=9.62, RMSE=12.38, RΒ²=0.762, MAPE=9.41%. "
|
| 157 |
+
"Random Forest: MAE=9.75, RMSE=13.50, RΒ²=0.716, MAPE=9.43%. "
|
| 158 |
+
"Linear Regression performed slightly better on this dataset. "
|
| 159 |
+
"The top predictors were lag_7 (same weekday last week), lag_14, and is_promo.")
|
| 160 |
+
doc.add_page_break()
|
| 161 |
+
|
| 162 |
+
# ββ references βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 163 |
+
H("References")
|
| 164 |
+
refs = [
|
| 165 |
+
"[1] S. Russell and P. Norvig, Artificial Intelligence: A Modern Approach, 4th ed. Hoboken, NJ: Pearson, 2020.",
|
| 166 |
+
"[2] F. Pedregosa et al., \"Scikit-learn: Machine Learning in Python,\" JMLR, vol. 12, pp. 2825-2830, 2011.",
|
| 167 |
+
"[3] M. Feldman et al., \"Certifying and Removing Disparate Impact,\" in Proc. ACM SIGKDD, 2015.",
|
| 168 |
+
"[4] P. E. Hart, N. J. Nilsson, and B. Raphael, \"A Formal Basis for the Heuristic Determination of Minimum Cost Paths,\" IEEE Trans. Syst. Sci. Cybern., vol. 4, no. 2, pp. 100-107, 1968.",
|
| 169 |
+
]
|
| 170 |
+
for ref in refs:
|
| 171 |
+
p = doc.add_paragraph(ref)
|
| 172 |
+
p.paragraph_format.space_after = Pt(5)
|
| 173 |
+
p.paragraph_format.left_indent = Inches(0.3)
|
| 174 |
+
p.paragraph_format.first_line_indent = Inches(-0.3)
|
| 175 |
+
for r in p.runs: r.font.name=TNR; r.font.size=Pt(11)
|
| 176 |
+
|
| 177 |
+
buf = io.BytesIO()
|
| 178 |
+
doc.save(buf)
|
| 179 |
+
buf.seek(0)
|
| 180 |
+
return buf
|
| 181 |
+
|
| 182 |
# ββ page config βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 183 |
st.set_page_config(page_title="EcoCart AI", layout="wide",
|
| 184 |
initial_sidebar_state="expanded")
|
|
|
|
| 252 |
5. Read the *What does this tell us?* section
|
| 253 |
""")
|
| 254 |
st.divider()
|
| 255 |
+
|
| 256 |
+
t2_done = st.session_state.get("t2_done", False)
|
| 257 |
+
t3_done = st.session_state.get("t3_done", False)
|
| 258 |
+
t5_done = st.session_state.get("t5_done", False)
|
| 259 |
+
|
| 260 |
+
st.markdown("**Task status:**")
|
| 261 |
+
st.markdown(f"{'β
' if t2_done else 'β¬'} Task 2 β Bias & Segmentation")
|
| 262 |
+
st.markdown(f"{'β
' if t3_done else 'β¬'} Task 3 β Route Algorithms")
|
| 263 |
+
st.markdown(f"{'β
' if t5_done else 'β¬'} Task 5 β Demand Forecasting")
|
| 264 |
+
st.divider()
|
| 265 |
+
|
| 266 |
+
st.markdown("**Download Report**")
|
| 267 |
+
if t2_done or t3_done or t5_done:
|
| 268 |
+
report_buf = _build_report(
|
| 269 |
+
st.session_state.get("t2_text",""),
|
| 270 |
+
st.session_state.get("t3_text",""),
|
| 271 |
+
st.session_state.get("t5_text",""),
|
| 272 |
+
)
|
| 273 |
+
st.download_button(
|
| 274 |
+
label="β¬ Download .docx report",
|
| 275 |
+
data=report_buf,
|
| 276 |
+
file_name="EcoCart_Report.docx",
|
| 277 |
+
mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
| 278 |
+
use_container_width=True,
|
| 279 |
+
)
|
| 280 |
+
st.caption("Report includes all outputs and charts from completed tasks.")
|
| 281 |
+
else:
|
| 282 |
+
st.caption("Run Tasks 2, 3 and 5 first β download button will appear here.")
|
| 283 |
st.divider()
|
| 284 |
+
st.caption("All outputs are from the actual task Python scripts.")
|
| 285 |
|
| 286 |
# ββ header ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 287 |
st.markdown("<h2 style='margin:0 0 16px;color:#1e293b'>EcoCart AI System</h2>",
|
|
|
|
| 485 |
|
| 486 |
with st.spinner("Running task2_segmentation.py..."):
|
| 487 |
t2_output = _run_task2()
|
| 488 |
+
st.session_state["t2_text"] = t2_output
|
| 489 |
|
| 490 |
st.markdown("<div class='step-box'><span class='step-num'>2</span>"
|
| 491 |
"<b>Terminal output from task2_segmentation.py</b></div>",
|
|
|
|
| 556 |
|
| 557 |
with st.spinner("Running task3_4_routing.py..."):
|
| 558 |
t3_output = _run_task3()
|
| 559 |
+
st.session_state["t3_text"] = t3_output
|
| 560 |
|
| 561 |
st.markdown("<div class='step-box'><span class='step-num'>2</span>"
|
| 562 |
"<b>Terminal output from task3_4_routing.py</b></div>",
|
|
|
|
| 845 |
|
| 846 |
with st.spinner("Running task5_forecasting.py..."):
|
| 847 |
t5_output = _run_task5()
|
| 848 |
+
st.session_state["t5_text"] = t5_output
|
| 849 |
|
| 850 |
st.markdown("<div class='step-box'><span class='step-num'>2</span>"
|
| 851 |
"<b>Terminal output from task5_forecasting.py</b></div>",
|
requirements.txt
CHANGED
|
@@ -5,3 +5,4 @@ plotly>=5.0.0
|
|
| 5 |
scikit-learn>=1.3.0
|
| 6 |
matplotlib>=3.7.0
|
| 7 |
networkx>=3.0
|
|
|
|
|
|
| 5 |
scikit-learn>=1.3.0
|
| 6 |
matplotlib>=3.7.0
|
| 7 |
networkx>=3.0
|
| 8 |
+
python-docx>=1.1.0
|