Devina707/Building-Energy-Demand-Prediction
0
1import streamlit as st2 3# ============================================================4# PAGE CONFIG — must be first Streamlit call5# ============================================================6st.set_page_config(7 page_title='BuildSmart: Energy Predictor',8 page_icon='⚡',9 layout='wide',10 initial_sidebar_state='collapsed'11)12 13# ============================================================14# IMPORTS15# ============================================================16import pandas as pd17import numpy as np18import matplotlib.pyplot as plt19import seaborn as sns20import datetime21import joblib22import os23import io24import csv25 26# ============================================================27# CUSTOM CSS28# ============================================================29st.markdown("""30<style>31@import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;700;800&family=DM+Mono:wght@300;400&family=DM+Sans:wght@300;400;500&display=swap');32 33:root {34 --bg: #0a0f0f;35 --surface: #111818;36 --border: #1e2e2e;37 --teal: #00d4aa;38 --teal-lo: #00d4aa18;39 --amber: #f5a623;40 --text: #d4e8e4;41 --muted: #5a7a76;42}43html, body, [data-testid="stAppViewContainer"] {44 background-color: var(--bg) !important;45 color: var(--text) !important;46 font-family: 'DM Sans', sans-serif;47}48[data-testid="stHeader"] { background: transparent !important; }49section[data-testid="stSidebar"] { display: none; }50 51.hero {52 background: linear-gradient(135deg, #001a18 0%, #0a0f0f 70%);53 border: 1px solid var(--border);54 border-radius: 16px;55 padding: 2.8rem 18rem 2.8rem 3rem;56 margin-bottom: 2rem;57 position: relative;58 overflow: hidden;59}60.hero::before {61 content: "";62 background-image: url("data:image/png;base64,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");63 background-size: contain;64 background-repeat: no-repeat;65 background-position: center;66 width: 220px;67 height: 220px;68 position: absolute;69 right: 3rem;70 top: 50%;71 transform: translateY(-50%);72 opacity: 1;73 pointer-events: none;74 filter: drop-shadow(0 0 28px rgba(0,212,170,0.45)) drop-shadow(0 0 8px rgba(0,212,170,0.2));75 animation: gaugeFloat 4s ease-in-out infinite;76}77@keyframes gaugeFloat {78 0%, 100% { transform: translateY(-50%) scale(1); filter: drop-shadow(0 0 28px rgba(0,212,170,0.45)) drop-shadow(0 0 8px rgba(0,212,170,0.2)); }79 50% { transform: translateY(-54%) scale(1.04); filter: drop-shadow(0 0 40px rgba(0,212,170,0.65)) drop-shadow(0 0 14px rgba(245,166,35,0.25)); }80}81.hero h1 {82 font-family: 'Syne', sans-serif;83 font-size: 3.2rem; font-weight: 800;84 color: var(--teal);85 margin: 0 0 .4rem; line-height: 1;86}87.hero p { color: var(--muted); font-size: 1rem; margin: 0; font-weight: 300; }88.hero .badge {89 display: inline-block;90 background: var(--teal-lo);91 border: 1px solid var(--teal);92 color: var(--teal);93 font-size: .7rem; font-weight: 500;94 letter-spacing: .12em; text-transform: uppercase;95 padding: .25rem .7rem; border-radius: 999px;96 margin-bottom: .8rem;97 font-family: 'DM Mono', monospace;98}99 100[data-testid="stTabs"] button {101 font-family: 'DM Sans', sans-serif !important;102 font-weight: 500 !important; font-size: .92rem !important;103 color: var(--muted) !important;104 border-radius: 8px 8px 0 0 !important;105 padding: .6rem 1.3rem !important;106}107[data-testid="stTabs"] button[aria-selected="true"] {108 color: var(--teal) !important;109 border-bottom: 2px solid var(--teal) !important;110}111[data-testid="stTabs"] [data-baseweb="tab-list"] {112 border-bottom: 1px solid var(--border) !important;113}114 115.sec-head {116 font-family: 'Syne', sans-serif;117 font-size: 1.25rem; font-weight: 700;118 color: var(--teal);119 border-bottom: 1px solid var(--border);120 padding-bottom: .4rem;121 margin: 1.8rem 0 1rem;122 letter-spacing: .04em;123}124.card {125 background: var(--surface);126 border: 1px solid var(--border);127 border-radius: 12px;128 padding: 1.4rem; margin-bottom: 1.1rem;129}130.card h4 {131 font-family: 'Syne', sans-serif; font-weight: 700;132 color: var(--amber); font-size: 1rem;133 margin: 0 0 .5rem; letter-spacing: .05em;134}135.model-table { width:100%; border-collapse:collapse; font-size:.88rem; }136.model-table th {137 background: var(--surface); color: var(--teal);138 font-family: 'DM Mono', monospace; font-size:.73rem;139 letter-spacing:.1em; text-transform:uppercase;140 padding:.6rem .9rem; border-bottom:1px solid var(--border); text-align:left;141}142.model-table td { padding:.55rem .9rem; border-bottom:1px solid var(--border); color:var(--text); }143.model-table tr.winner td { background:var(--teal-lo); color:var(--teal); font-weight:500; }144.model-table tr:hover td { background:#162020; }145 146.metric-pill {147 background: var(--surface); border:1px solid var(--border);148 border-radius:10px; padding:1rem 1.2rem; text-align:center;149}150.metric-pill .val {151 font-family:'Syne',sans-serif; font-size:1.6rem; font-weight:800;152 color:var(--teal); display:block;153}154.metric-pill .lbl {155 font-size:.72rem; color:var(--muted);156 text-transform:uppercase; letter-spacing:.1em;157 font-family:'DM Mono',monospace;158}159.param-badge {160 display:inline-block;161 background:#001a18; border:1px solid var(--teal); color:var(--teal);162 font-family:'DM Mono',monospace; font-size:.75rem;163 padding:.2rem .6rem; border-radius:6px; margin:.15rem;164}165.chosen-box {166 background:linear-gradient(135deg,#001a18,#0a1f1c);167 border:1px solid var(--teal); border-radius:14px;168 padding:1.5rem 2rem; margin:1rem 0;169}170.chosen-box h3 {171 font-family:'Syne',sans-serif; color:var(--teal);172 font-size:1.3rem; margin:0 0 .8rem;173}174 175[data-testid="stForm"] {176 background: var(--surface) !important;177 border: 1px solid var(--border) !important;178 border-radius: 14px !important;179 padding: 1.5rem !important;180}181[data-testid="stFormSubmitButton"] > button,182[data-testid="stButton"] > button {183 background: var(--teal) !important; color: #000 !important;184 border: none !important; font-weight: 600 !important;185 border-radius: 8px !important;186 font-family: 'DM Sans', sans-serif !important;187}188[data-testid="stMetricValue"] {189 font-family: 'Syne', sans-serif !important;190 font-size: 2rem !important; color: var(--teal) !important;191}192[data-testid="stMetricLabel"] { color: var(--muted) !important; }193.stImage img { border-radius: 8px; }194</style>195""", unsafe_allow_html=True)196 197# ============================================================198# LOAD MODEL199# ============================================================200@st.cache_resource201def load_model():202 path = './src/pipelines_inference_st.pkl'203 try:204 return joblib.load(path)205 except Exception as e:206 st.error(f"Model load error: {e}")207 return None208 209model = load_model()210 211# ============================================================212# FEATURE ENGINEERING213# ============================================================214def preprocess_inference_data(data):215 df = data.copy()216 df['Time Stamp'] = pd.to_datetime(df['Time Stamp'])217 df['Time'] = df['Time Stamp'].dt.time218 219 def categorize_time(hour):220 if 6 <= hour < 14: return 'Morning/Day'221 elif 14 <= hour < 22: return 'Afternoon/Evening'222 else: return 'Night'223 224 df['Time Category'] = df['Time Stamp'].dt.hour.apply(categorize_time)225 226 def categorize_era(year):227 return 'Pre-Energy Code Era (before 1981)' if year < 1981 else 'Post-Energy Code Era (after 1980)'228 229 df['Era Category'] = df['Year Built'].apply(categorize_era)230 231 def categorize_meter(row):232 if row['Meter Type'] == 0: return 'Electricity'233 elif row['Meter Type'] == 3: return 'Hot Water'234 235 df['Meter Type Category'] = df.apply(categorize_meter, axis=1)236 return df237 238# ============================================================239# MPL STYLE HELPER240# ============================================================241def dark_fig(w=12, h=5, ncols=1, nrows=1):242 fig, ax = plt.subplots(nrows=nrows, ncols=ncols, figsize=(w, h), facecolor='#0a0f0f')243 axes = [ax] if (nrows == 1 and ncols == 1) else (ax.flatten() if hasattr(ax, 'flatten') else list(ax))244 for a in axes:245 a.set_facecolor('#111818')246 a.tick_params(colors='#5a7a76')247 for s in ['top', 'right']: a.spines[s].set_visible(False)248 for s in ['left', 'bottom']: a.spines[s].set_color('#1e2e2e')249 return fig, ax250 251# ============================================================252# SESSION STATE — prediction history log253# ============================================================254if 'pred_history' not in st.session_state:255 st.session_state.pred_history = [] # list of dicts256 257# ============================================================258# DOWNLOAD HELPERS259# ============================================================260def history_to_csv(history: list) -> bytes:261 if not history:262 return b""263 buf = io.StringIO()264 writer = csv.DictWriter(buf, fieldnames=history[0].keys())265 writer.writeheader()266 writer.writerows(history)267 return buf.getvalue().encode('utf-8')268 269def history_to_txt(history: list) -> bytes:270 if not history:271 return b""272 lines = [273 "BUILDSMART — ENERGY DEMAND PREDICTION LOG",274 f"Generated : {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",275 f"Records : {len(history)}",276 "=" * 70, ""277 ]278 for i, r in enumerate(history, 1):279 lines.append(f"Record #{i} — {r.get('Predicted At','')}")280 lines.append(f" Predicted Meter Reading : {r.get('Predicted Meter Reading (kWh)', '')} kWh")281 for k, v in r.items():282 if k not in ('Predicted At', 'Predicted Meter Reading (kWh)'):283 lines.append(f" {k:<38}: {v}")284 lines.append("")285 lines += ["=" * 70,286 f" Total predictions : {len(history)}",287 f" Avg reading : {sum(float(r.get('Predicted Meter Reading (kWh)',0)) for r in history)/len(history):,.2f} kWh"]288 return "\n".join(lines).encode('utf-8')289 290def batch_to_txt(df: pd.DataFrame) -> bytes:291 ts = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')292 lines = [293 "BUILDSMART — BATCH PREDICTION REPORT",294 f"Generated : {ts}",295 f"Records : {len(df)}",296 "=" * 70, ""297 ]298 for i, row in df.iterrows():299 lines.append(f"Row {i+1}")300 for col in df.columns:301 lines.append(f" {col:<38}: {row[col]}")302 lines.append("")303 return "\n".join(lines).encode('utf-8')304 305# ============================================================306# HERO307# ============================================================308st.markdown("""309<div class="hero">310 <h1>BuildSmart</h1>311 <p>ML-powered energy demand prediction for buildings · 312 Built by <em>Devina Agustina</em></p>313</div>314""", unsafe_allow_html=True)315 316# ============================================================317# TABS318# ============================================================319tab_eda, tab_model, tab_pred, tab_batch = st.tabs([320 "📊 Exploratory Data Analysis",321 "🏆 Model Performance & Selection",322 "⚡ Predict Energy Demand",323 "📦 Batch Prediction",324])325 326# ──────────────────────────────────────────────────────────327# TAB 1 — EDA328# ──────────────────────────────────────────────────────────329with tab_eda:330 st.markdown('<div class="sec-head">About This App</div>', unsafe_allow_html=True)331 st.markdown("""332 <div style="background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:1.8rem 2rem;margin-bottom:1.5rem;">333 <p style="font-size:1rem;line-height:1.85;color:var(--text);margin:0 0 1rem">334 <strong style="color:var(--teal)">BuildSmart</strong> is an ML-powered energy demand forecasting application by335 <em>Devina Agustina</em>. The application predicts building energy meter readings (kWh)336 for Site ID 1 using the <strong>ASHRAE Great Energy Predictor III</strong> dataset —337 one of the most comprehensive publicly available building energy benchmarking datasets,338 spanning 16 geographic sites across North America and Europe.339 </p>340 <p style="font-size:1rem;line-height:1.85;color:var(--text);margin:0 0 1.2rem">341 Due to the scale of the original dataset (550,000+ records for Site 1 alone),342 a stratified random sample of <strong>~20,000 records</strong> was used for model343 training and analysis. The underlying model is a344 <strong>Histogram-based Gradient Boosting Regressor</strong> — selected after345 benchmarking five regression algorithms — and achieves an346 <strong style="color:var(--teal)">R² of 0.9178</strong> on the held-out test set,347 explaining over 91% of the variance in energy consumption.348 </p>349 <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:1.2rem">350 <div style="background:#0a0f0f;border:1px solid var(--border);border-radius:10px;padding:1rem;text-align:center">351 <div style="font-family:'Syne',sans-serif;font-size:1.5rem;font-weight:800;color:var(--teal)">0.9178</div>352 <div style="font-size:.72rem;color:var(--muted);letter-spacing:.1em;text-transform:uppercase;font-family:'DM Mono',monospace;margin-top:.2rem">R² Score</div>353 </div>354 <div style="background:#0a0f0f;border:1px solid var(--border);border-radius:10px;padding:1rem;text-align:center">355 <div style="font-family:'Syne',sans-serif;font-size:1.5rem;font-weight:800;color:var(--teal)">27.88 kWh</div>356 <div style="font-size:.72rem;color:var(--muted);letter-spacing:.1em;text-transform:uppercase;font-family:'DM Mono',monospace;margin-top:.2rem">Mean Abs. Error</div>357 </div>358 <div style="background:#0a0f0f;border:1px solid var(--border);border-radius:10px;padding:1rem;text-align:center">359 <div style="font-family:'Syne',sans-serif;font-size:1.5rem;font-weight:800;color:var(--teal)">~20K</div>360 <div style="font-size:.72rem;color:var(--muted);letter-spacing:.1em;text-transform:uppercase;font-family:'DM Mono',monospace;margin-top:.2rem">Training Records</div>361 </div>362 </div>363 </div>364 <div style="display:grid;grid-template-columns:repeat(2,1fr);gap:12px;margin-bottom:1.5rem">365 <div style="background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:1.1rem 1.3rem">366 <div style="font-size:.72rem;color:var(--teal);letter-spacing:.12em;text-transform:uppercase;font-family:'DM Mono',monospace;margin-bottom:.5rem">Dataset</div>367 <div style="font-size:.9rem;color:var(--text);line-height:1.6">ASHRAE Great Energy Predictor III · Site ID 1 · Building types: Education, Office, Lodging, Public Services, Entertainment</div>368 </div>369 <div style="background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:1.1rem 1.3rem">370 <div style="font-size:.72rem;color:var(--teal);letter-spacing:.12em;text-transform:uppercase;font-family:'DM Mono',monospace;margin-bottom:.5rem">Model Pipeline</div>371 <div style="font-size:.9rem;color:var(--text);line-height:1.6">Feature engineering → OneHotEncoding → MinMaxScaling → HistGradient Boosting (hypertuned via GridSearchCV, 5-fold CV)</div>372 </div>373 </div>374 """, unsafe_allow_html=True)375 376 df_path = './src/df_raw.csv'377 if not os.path.exists(df_path):378 st.warning("df_raw.csv not found in ./src/ — upload the file to enable EDA.")379 st.stop()380 381 df = pd.read_csv(df_path)382 383 # A. Raw data384 st.markdown('<div class="sec-head">A · Raw Data Preview</div>', unsafe_allow_html=True)385 st.caption("ASHRAE Great Energy Predictor III · Site ID 1 · random sample of 20,000 rows")386 st.dataframe(df, use_container_width=True)387 388 # B. Descriptive stats389 st.markdown('<div class="sec-head">B · Descriptive Statistics</div>', unsafe_allow_html=True)390 st.dataframe(df.agg({391 'Building Area (sqft)' : ['mean','median','std','skew','kurtosis','var'],392 'Floor Count' : ['mean','median','std','skew','kurtosis','var'],393 'Meter Reading (kWh)' : ['mean','median','std','skew','kurtosis','var'],394 'Air Temperature (°C)' : ['mean','median','std','skew','kurtosis','var'],395 'Cloud Coverage (oktas)' : ['mean','median','std','skew','kurtosis','var'],396 'Dew Temperature (°C)' : ['mean','median','std','skew','kurtosis','var'],397 'Precipitation Depth (mm/hr)' : ['mean','median','std','skew','kurtosis','var'],398 'Sea Level Pressure (mbar/hPa)': ['mean','median','std','skew','kurtosis','var'],399 'Wind Direction (degree)' : ['mean','median','std','skew','kurtosis','var'],400 'Wind Speed (m/s)' : ['mean','median','std','skew','kurtosis','var'],401 }), use_container_width=True)402 403 st.markdown("""404 This data can also be used for descriptive analysis. The skewness value for columns405 **'Air Temperature (°C)'**, **'Dew Temperature (°C)'**, and **'Wind Direction (degree)'**406 fall within the range of -0.5 to 0.5, which indicates symmetric skewness, meaning the mean407 and median are similar or nearly identical.408 409 For columns **'Building Area (sqft)'**, **'Floor Count'**, **'Meter Reading (kWh)'**,410 **'Cloud Coverage (oktas)'**, and **'Wind Speed (m/s)'**, the skewness is positive (> 0.5),411 meaning the mean is larger than the median. This indicates that the data is concentrated in412 lower values with few higher values, showing that the data is not normally distributed.413 414 For column **'Sea Level Pressure (mbar/hPa)'**, the skewness value is below -0.5, indicating415 negative skewness where the mean is less than the median. This means the data is concentrated416 in higher values with few lower values.417 418 Regarding kurtosis, columns with high kurtosis (> 3) are **'Meter Reading (kWh)'** and419 **'Cloud Coverage (oktas)'**, indicating leptokurtic distributions with a high presence of420 outliers. Columns **'Floor Count'**, **'Sea Level Pressure (mbar/hPa)'**, and421 **'Wind Speed (m/s)'** have kurtosis values near 3, indicating mesokurtic distributions that422 are close to normal with fewer outliers. Columns **'Building Area (sqft)'**,423 **'Air Temperature (°C)'**, **'Dew Temperature (°C)'**, and **'Wind Direction (degree)'**424 have negative kurtosis (< 0), indicating polykurtic distributions with very few outliers.425 426 Column **'Precipitation Depth (mm/hr)'** contains no values and is recorded as NaN throughout.427 This column will likely be dropped during feature engineering.428 429 In conclusion, the majority of the data in this dataframe does not follow a normal distribution,430 and this assumption will be carried forward in the descriptive and inferential analysis.431 """)432 433 # C. Building use pie434 st.markdown('<div class="sec-head">C · Building Use Distribution</div>', unsafe_allow_html=True)435 fig_1, ax1 = dark_fig(5, 4)436 counts = df['Building Use'].value_counts()437 counts.plot(kind='pie', autopct='%.2f%%', ax=ax1, ylabel='',438 explode=[0.01]*len(counts),439 colors=['#00d4aa','#f5a623','#4a9eff','#ff4e4e','#b388ff'],440 textprops={'color':'#d4e8e4','fontsize':9})441 ax1.set_title('Building Use Category', color='#d4e8e4', fontsize=11)442 plt.tight_layout(); st.pyplot(fig_1); plt.close()443 444 st.markdown("""445 From the figure shown above, it can be seen that the use of buildings for **Education dominates with446 47.62%** of the total buildings in area site 1, followed by **Office (28.58%)**, **Lodging/Residential (15.87%)**,447 **Public Service (6.35%)**, and lastly Entertainment/Public Assembly as the least represented building use.448 This shows that most buildings in area site 1 are used for education and office purposes, accounting for more449 than 70% of the total building use. Therefore, it is safe to say that area site 1 is not a predominantly450 residential area and is more likely characterized as a Central Business District (CBD), defined by a high451 concentration of office and educational facilities. This type of area contributes significantly to the local452 economy and community life.453 """)454 455 # D. Building area vs meter reading456 st.markdown('<div class="sec-head">D · Building Area & Use vs Electricity Usage</div>', unsafe_allow_html=True)457 area_bins = [5374, df['Building Area (sqft)'].quantile(0.25),458 df['Building Area (sqft)'].quantile(0.50),459 df['Building Area (sqft)'].quantile(0.75), 174601]460 area_labels = ['Min–25%','25%–50%','50%–75%','75%–Max']461 st.table(df['Building Area (sqft)'].describe().to_frame())462 463 df['area_percentile'] = pd.cut(df['Building Area (sqft)'], bins=area_bins,464 labels=area_labels, include_lowest=True)465 df_elec = df[(df['Meter Type']==0) & df['Meter Reading (kWh)'].notna() & df['area_percentile'].notna()]466 pivot = (df_elec.groupby(['Building Use','area_percentile'], observed=True)['Meter Reading (kWh)']467 .mean().reset_index()468 .rename(columns={'Meter Reading (kWh)':'avg_meter_reading'}).round(2)469 .pivot(index='Building Use', columns='area_percentile', values='avg_meter_reading'))470 st.write("""According to the data above, **the minimum building area is471 approximately 5,374 sqft** while **the maximum building area is472 approximately 174,601 sqft**. Therefore, we will examine whether473 the minimum, 25th percentile, 50th percentile, 75th percentile,474 and maximum building area across each building use category have475 any difference in their meter readings. This would give us insight476 into how much building area affects the meter reading for energy usage477 (electricity) in these buildings.478 """)479 st.table(pivot)480 481 st.write("""482 As seen from the data above, there is a clear trend of increasing electricity usage as483 building area grows larger. However, the rate of increase differs significantly by building484 use type. The visualization below provides a clearer picture of this relationship.485 """)486 487 fig_2, ax2 = dark_fig(14, 6)488 pivot.plot(kind='bar', colormap='viridis', edgecolor='#0a0f0f', ax=ax2)489 ax2.set_title('Avg Electricity Reading by Building Use & Area Percentile', color='#d4e8e4', fontsize=12)490 ax2.set_xlabel('Building Use', color='#5a7a76')491 ax2.set_ylabel('Avg Meter Reading (kWh)', color='#5a7a76')492 ax2.legend(title='Area Percentile', bbox_to_anchor=(1.05,1), loc='upper left',493 facecolor='#111818', labelcolor='#d4e8e4', title_fontsize=9)494 plt.xticks(rotation=30, ha='right', color='#5a7a76')495 plt.tight_layout(); st.pyplot(fig_2); plt.close()496 497 st.markdown("""498 It can be seen that across each building use category, electricity usage generally increases as building area gets larger,499 as observed in buildings used for Education and Lodging/Residential. **Education buildings show the steepest difference between500 the Min–25% and 75%–Max percentile ranges**, with approximately **4x higher consumption** in the latter quartile. This steep increase501 may be driven by high-powered equipment such as laboratories, lecture halls, and HVAC systems.502 503 For Lodging/Residential buildings, they have the lowest absolute average electricity usage (kWh), but the increase in electricity504 consumption across building area percentiles is the most gradual and consistent. This suggests that building area is a reliable predictor505 of electricity usage for this building type.506 507 For Office buildings, it can be observed that electricity usage does not follow a consistent upward trend. There is a significant decrease in508 average electricity usage from the 50%–75% quartile to the 75%–Max quartile. This means that once *the building area exceeds approximately509 91,149 sqft (75th percentile), electricity usage begins to decline*. A possible explanation is that not all areas within larger buildings are510 frequently occupied, resulting in minimal electricity usage in rarely used spaces/rooms. Additionally, larger buildings tend to adopt more efficient511 energy management systems and are more likely to hold green building certifications, featuring low-watt appliances and heat exchange systems.512 These factors may explain the decrease in electricity usage beyond a certain building area.513 514 For Entertainment/Public Assembly and Public Service buildings, the available data is insufficient to draw meaningful conclusions about the differences515 between each area percentile. These gaps are likely due to very few buildings of these types falling into each area percentile group.516 """)517 518 # E. Building age519 st.markdown('<div class="sec-head">E · Building Age (Era) vs Energy Usage</div>', unsafe_allow_html=True)520 fig_3, axes3 = dark_fig(10, 4, ncols=2)521 sns.boxplot(data=df[df['Year Built']<1981]['Meter Reading (kWh)'], ax=axes3[0], color='#00d4aa')522 axes3[0].set_xticks([0]); axes3[0].set_xticklabels(['Pre-1981'], color='#5a7a76')523 axes3[0].set_ylabel('Meter Reading (kWh)', color='#5a7a76')524 axes3[0].set_title('Pre-Energy Code', color='#d4e8e4', fontweight='bold')525 sns.boxplot(data=df[df['Year Built']>1980]['Meter Reading (kWh)'], ax=axes3[1], color='#f5a623')526 axes3[1].set_xticks([1]); axes3[1].set_xticklabels(['Post-1980'], color='#5a7a76')527 axes3[1].set_ylabel('Meter Reading (kWh)', color='#5a7a76')528 axes3[1].set_title('Post-Energy Code', color='#d4e8e4', fontweight='bold')529 plt.tight_layout(); st.pyplot(fig_3); plt.close()530 531 st.markdown("""532 The energy data can be analyzed based on era category, specifically **Pre-Energy Code (1900–1980)** and **Post-Energy Code (1981–2007)**.533 These categories were chosen because during the Pre-Energy Code era there were no formal energy standards in buildings,534 which typically resulted because of poor insulation and inefficient systems. As for the Post-Energy Code era, this is when the535 first energy regulation was implemented (ASHRAE 90-1975, after year 1980), triggered by the energy crisis, and where HVAC systems were536 introduced alongside better insulation and more efficient building systems.537 538 From the data, it can be seen that **buildings built before 1981 have a higher average energy demand (meter reading)539 of 184.87 kWh compared to buildings built after 1980, which average 174.55 kWh**. To determine whether this difference540 is statistically significant, a two-sample, two-sided test will be conducted using two independent samples, between buildings541 built before 1981 and buildings built after 1980.542 543 The two-sided approach is chosen because we are only interested in whether a difference in meter readings (kWh) exists544 between the two groups, without considering the direction of the effect. Spearman correlation test will545 be used to examine the relationship between the two categories. The Spearman method is chosen because the relationship546 between the data may be monotonic rather than strictly linear, yet still moves in one consistent direction. Additionally,547 this method is robust to outliers, making it well-suited for our dataset.548 549 From the hypothesis test, **The p-value of 0.0031 is lower than 0.05**, therefore **the null hypothesis is rejected**. This means550 that there is a statistically significant difference in energy usage/meter readings (kWh) between buildings built before551 1981 and those built after 1980. Specifically, buildings built before 1981 have a higher average energy demand compared to552 those built after 1980. Some reasons that may explain this finding include the fact that older buildings constructed before 1981553 generally have poor insulation, inefficient systems, and were built without energy regulations to govern their energy consumption.554 This could explain why energy usage in buildings from the pre-energy code era tends to be higher than that of more recently constructed555 buildings from the post-energy code era.556 """)557 558 # F. Meter type559 st.markdown('<div class="sec-head">F · Meter Type vs Energy Usage</div>', unsafe_allow_html=True)560 fig_4, axes4 = dark_fig(10, 4, ncols=2)561 sns.boxplot(data=df[df['Meter Type']==0]['Meter Reading (kWh)'], ax=axes4[0], color='#4a9eff')562 axes4[0].set_xticks([0]); axes4[0].set_xticklabels(['Electricity'], color='#5a7a76')563 axes4[0].set_ylabel('Meter Reading (kWh)', color='#5a7a76')564 axes4[0].set_title('Electricity demand', color='#d4e8e4', fontweight='bold')565 sns.boxplot(data=df[df['Meter Type']==3]['Meter Reading (kWh)'], ax=axes4[1], color='#ff4e4e')566 axes4[1].set_xticks([1]); axes4[1].set_xticklabels(['Hot Water'], color='#5a7a76')567 axes4[1].set_ylabel('Meter Reading (kWh)', color='#5a7a76')568 axes4[1].set_title('Hot water demand', color='#d4e8e4', fontweight='bold')569 plt.tight_layout(); st.pyplot(fig_4); plt.close()570 571 st.markdown("""572 *From the data, it can be seen that electricity has a higher average energy demand (meter reading) of 161.97 kWh573 compared to hot water, which has an average energy demand of 91.50 kWh*. To determine whether this difference574 is statistically significant, a two-sample, two-sided test will be conducted using two independent samples, like575 buildings that use electricity and buildings that use hot water as their meter type.576 577 The two-sided approach is chosen because we are only interested in whether a difference in meter readings (kWh)578 exists between the two groups, without considering the direction of the effect. Spearman correlation579 test will be used to examine the relationship between the two categories. The Spearman method is chosen because the580 relationship between the data may be monotonic rather than strictly linear, yet still moves in one consistent direction.581 Additionally, this method is robust to outliers, making it well-suited for our dataset.582 583 **The p-value of 8.37 × 10⁻¹⁰⁷** is lower than 0.05, therefore the null hypothesis is rejected. This means that there584 is a statistically significant difference in energy usage/meter readings between buildings that use electricity and585 those that use hot water as their meter type. Specifically, buildings that use electricity as their energy meter type586 have a higher average meter reading/energy usage (kWh) compared to buildings that use hot water.587 588 Several reasons may explain this finding, such as electricity supports a broader range of uses compared to hot water,589 as buildings powered by electricity often consume energy not just for heating, but also for cooling (air conditioning), lighting,590 appliances, plug loads, and ventilation systems. This results in a much higher cumulative energy demand compared to591 buildings that rely solely on hot water, which is typically used only for space heating and domestic hot water supply.592 Furthermore, when electricity is used for heating purposes, electric resistance systems are generally less efficient than593 hot water-based heating systems, resulting in greater energy (kWh) consumption. In addition, electrically-metered594 buildings often maintain a constant baseline load from systems such as servers, security systems, elevators,595 and refrigeration, contributing to a continuous 24/7 energy demand. Lastly, electricity-based energy596 systems operate throughout all seasons, with electrically-metered buildings facing high demand in both summer (cooling)597 and winter (heating), creating a year-round high energy profile — whereas hot water systems primarily experience demand598 spikes only during colder months. All of these factors contribute to an overall higher energy consumption (kWh) in599 electrically-metered buildings.600 """)601 602 # G. Correlation heatmap603 st.markdown('<div class="sec-head">G · Correlation Heatmap</div>', unsafe_allow_html=True)604 fig_5, ax5 = dark_fig(18, 7)605 sns.heatmap(606 df[['ID','Building ID','Building Area (sqft)','Year Built','Floor Count',607 'Meter Type','Meter Reading (kWh)',608 'Air Temperature (°C)','Cloud Coverage (oktas)','Dew Temperature (°C)',609 'Sea Level Pressure (mbar/hPa)','Wind Direction (degree)','Wind Speed (m/s)']610 ].corr(numeric_only=True),611 annot=True, fmt='.2f', ax=ax5,612 cmap='YlGnBu', linecolor='#0a0f0f', linewidths=0.5613 )614 ax5.set_title('Correlation Matrix', color='#d4e8e4', fontsize=12, fontweight='bold')615 ax5.tick_params(colors='#5a7a76', labelsize=9)616 plt.tight_layout(); st.pyplot(fig_5); plt.close()617 618 st.markdown("""619 From the heatmap above, we can see that energy demand/meter readings (kWh) correlates most strongly with **building area (=0.55)620 and floor count (=0.33)**. This makes sense because the larger the building area and the more floors there are to cover,621 the greater the demand on electrical systems, including lighting, heating/cooling, and other building services,622 this drives the overall energy consumption higher.623 """)624 625 # Scatter plots — building area and floor count vs meter reading626 fig_6, axes6 = plt.subplots(ncols=2, figsize=(18, 6), facecolor='#0a0f0f')627 for a in axes6:628 a.set_facecolor('#111818')629 a.tick_params(colors='#5a7a76')630 for s in ['top','right']: a.spines[s].set_visible(False)631 for s in ['left','bottom']: a.spines[s].set_color('#1e2e2e')632 633 axes6[0].scatter(df['Building Area (sqft)'], df['Meter Reading (kWh)'],634 alpha=0.4, color='#00d4aa', edgecolors='none')635 axes6[0].set_title('Building Area (sqft) vs Meter Reading (kWh)', color='#d4e8e4')636 axes6[0].set_xlabel('Building Area (sqft)', color='#5a7a76')637 axes6[0].set_ylabel('Meter Reading (kWh)', color='#5a7a76')638 m, b = np.polyfit(df['Building Area (sqft)'], df['Meter Reading (kWh)'], 1)639 axes6[0].plot(sorted(df['Building Area (sqft)']),640 [m*x + b for x in sorted(df['Building Area (sqft)'])],641 color='#f5a623', linewidth=2, label='Trend Line')642 axes6[0].legend(facecolor='#111818', labelcolor='#d4e8e4')643 644 axes6[1].scatter(df['Floor Count'], df['Meter Reading (kWh)'],645 alpha=0.4, color='#00d4aa', edgecolors='none')646 axes6[1].set_title('Floor Count vs Meter Reading (kWh)', color='#d4e8e4')647 axes6[1].set_xlabel('Floor Count', color='#5a7a76')648 axes6[1].set_ylabel('Meter Reading (kWh)', color='#5a7a76')649 m, b = np.polyfit(df['Floor Count'], df['Meter Reading (kWh)'], 1)650 axes6[1].plot(sorted(df['Floor Count']),651 [m*x + b for x in sorted(df['Floor Count'])],652 color='#f5a623', linewidth=2, label='Trend Line')653 axes6[1].legend(facecolor='#111818', labelcolor='#d4e8e4')654 655 plt.tight_layout(); st.pyplot(fig_6); plt.close()656 657 st.markdown("""658 From the heatmap above, we can see that energy demand/meter readings (kWh) correlates most strongly with **building area (0.55)659 and floor count (0.33)**. This makes sense because the larger the building area and the more floors there are to cover,660 the greater the demand on electrical systems, including lighting, heating/cooling, and other building services which driving661 overall energy consumption higher. However, these correlations will need to be validated further during feature engineering,662 as this heatmap serves only as a preliminary overview of what to expect in the subsequent analysis.663 The scatter plots visualises the correlation between meter readings and building area and floor count respectively.664 It can be seen that correlation between building area and floor count show a positive correlation, further supporting the findings above.665 """)666 667 # H. Interactive668 st.markdown('<div class="sec-head">H · Interactive Distribution Explorer</div>', unsafe_allow_html=True)669 option = st.selectbox('Choose a column to explore:', (670 'Building Area (sqft)','Year Built','Floor Count','Meter Type',671 'Air Temperature (°C)','Cloud Coverage (oktas)','Dew Temperature (°C)',672 'Sea Level Pressure (mbar/hPa)','Wind Direction (degree)','Wind Speed (m/s)'673 ))674 fig_7, axes7 = dark_fig(14, 5, ncols=2)675 sns.histplot(df[option], bins=30, kde=True, ax=axes7[0], color='#00d4aa')676 axes7[0].set_title(f'Distribution of {option}', color='#d4e8e4')677 axes7[0].set_xlabel(option, color='#5a7a76')678 axes7[1].scatter(df[option], df['Meter Reading (kWh)'], alpha=0.3, color='#00d4aa', edgecolors='none')679 axes7[1].set_title(f'{option} vs Meter Reading', color='#d4e8e4')680 axes7[1].set_xlabel(option, color='#5a7a76')681 axes7[1].set_ylabel('Meter Reading (kWh)', color='#5a7a76')682 df_cl = df[[option,'Meter Reading (kWh)']].dropna()683 m, b = np.polyfit(df_cl[option], df_cl['Meter Reading (kWh)'], 1)684 axes7[1].plot(sorted(df_cl[option]), [m*x+b for x in sorted(df_cl[option])],685 color='#f5a623', linewidth=2, label='Trend')686 axes7[1].legend(facecolor='#111818', labelcolor='#d4e8e4')687 plt.tight_layout(); st.pyplot(fig_7); plt.close()688 689# ──────────────────────────────────────────────────────────690# TAB 2 — MODEL PERFORMANCE & SELECTION691# ──────────────────────────────────────────────────────────692with tab_model:693 694 st.markdown('<div class="sec-head">Models Evaluated</div>', unsafe_allow_html=True)695 st.markdown("""696 Five regression models were benchmarked on the ASHRAE Site 1 dataset (80/20 train-test split).697 All models used the same pipeline: OneHotEncoded categoricals + MinMaxScaled numericals.698 """)699 700 st.markdown("""701 <table class="model-table">702 <thead>703 <tr>704 <th>Model</th><th>Train MAE</th><th>Test MAE</th><th>MAE Diff ↓</th>705 <th>Train R²</th><th>Test R²</th><th>R² Diff ↓</th><th>Verdict</th>706 </tr>707 </thead>708 <tbody>709 <tr><td>KNN Regressor</td><td>30.39</td><td>38.78</td><td>8.39</td>710 <td>0.9271</td><td>0.8694</td><td>0.0577</td><td>Moderate overfit</td></tr>711 <tr><td>Decision Tree</td><td>22.05</td><td>28.69</td><td>6.64</td>712 <td>0.9614</td><td>0.9141</td><td>0.0473</td><td>Moderate overfit</td></tr>713 <tr><td>SVR (Linear)</td><td>79.62</td><td>79.83</td><td>0.21</td>714 <td>0.6221</td><td>0.6178</td><td>0.0057</td><td>Underfit — low R²</td></tr>715 <tr><td>Random Forest</td><td>9.81</td><td>27.10</td><td>17.29</td>716 <td>0.9892</td><td>0.9178</td><td>0.0714</td><td>Overfit</td></tr>717 <tr class="winner"><td>⭐ HistGradient Boosting</td><td>25.13</td><td>28.04</td><td>2.91</td>718 <td>0.9411</td><td>0.9178</td><td>0.0233</td><td>Best fit — chosen ✓</td></tr>719 </tbody>720 </table>721 """, unsafe_allow_html=True)722 723 st.markdown("<br>", unsafe_allow_html=True)724 st.markdown("""725 **HistGradient Boosting** wins on two fronts: the **smallest R² gap (0.023)** between train and test726 (indicating the least overfitting), and the **smallest MAE difference (2.91)**.727 Although SVR has a marginally smaller MAE gap, its test MAE of ~79 is **3× worse**.728 Random Forest overfits heavily (R² diff = 0.071). Decision Tree and KNN overfit moderately.729 """)730 731 st.markdown('<div class="sec-head">Hyperparameter Tuning — GridSearchCV + 5-Fold CV</div>', unsafe_allow_html=True)732 st.markdown("GridSearchCV across **243 combinations** (1,215 total fits) on HistGradient Boosting.")733 734 col_p1, col_p2 = st.columns(2)735 with col_p1:736 st.markdown('<div class="card"><h4>Search Space</h4>', unsafe_allow_html=True)737 for k, v in {738 'max_iter': [100, 200, 300],739 'max_depth': [3, 5, 7],740 'learning_rate': [0.01, 0.05, 0.1],741 'min_samples_leaf': [20, 50, 100],742 'l2_regularization': [0.0, 0.1, 1.0],743 }.items():744 st.markdown(f'<code style="color:#00d4aa;font-size:.8rem">{k}</code>: {v}<br>', unsafe_allow_html=True)745 st.markdown('</div>', unsafe_allow_html=True)746 with col_p2:747 st.markdown('<div class="card"><h4>Best Parameters Found</h4>', unsafe_allow_html=True)748 for k, v in {749 'l2_regularization': 0.1,750 'learning_rate': 0.1,751 'max_depth': 7,752 'max_iter': 200,753 'min_samples_leaf': 20,754 }.items():755 st.markdown(f'<span class="param-badge">{k} = {v}</span>', unsafe_allow_html=True)756 st.markdown('</div>', unsafe_allow_html=True)757 758 st.markdown('<div class="sec-head">Before vs After Hypertuning</div>', unsafe_allow_html=True)759 c1, c2, c3, c4 = st.columns(4)760 for col, val, lbl, note in zip(761 [c1, c2, c3, c4],762 ["27.88", "50.82", "0.9178", "49.31"],763 ["MAE (tuned)", "RMSE (tuned)", "R² (tuned)", "CV RMSE mean"],764 ["↓ 0.16 vs baseline", "≈ stable", "= same", "std = 3.69"]765 ):766 col.markdown(f"""767 <div class="metric-pill">768 <span class="val">{val}</span>769 <span class="lbl">{lbl}</span>770 <div style="font-size:.7rem;color:#5a7a76;margin-top:.3rem">{note}</div>771 </div>772 """, unsafe_allow_html=True)773 774 st.markdown("""775 **CV RMSE (49.31) ≈ Test RMSE (50.82)** — a gap of only **1.51** — confirming the model776 generalises well across unseen data. The small std (3.69) confirms stable performance777 across all 5 folds.778 """)779 780 st.markdown('<div class="sec-head">Final Model Performance</div>', unsafe_allow_html=True)781 st.markdown("""782 <div class="chosen-box">783 <h3>⭐ Chosen: Hypertuned HistGradient Boosting Regressor</h3>784 <p style="color:#5a7a76;font-size:.9rem;margin-bottom:.5rem">785 HistGradientBoostingRegressor · random_state=10 · ASHRAE Site 1 · ~16,000 training samples786 </p>787 </div>788 """, unsafe_allow_html=True)789 790 fm1, fm2, fm3, fm4 = st.columns(4)791 for col, val, lbl in zip([fm1,fm2,fm3,fm4],792 ["27.88", "2,582.54", "50.82", "0.9178"],793 ["MAE (kWh)", "MSE", "RMSE (kWh)", "R²"]):794 col.markdown(f"""795 <div class="metric-pill">796 <span class="val">{val}</span>797 <span class="lbl">{lbl}</span>798 </div>799 """, unsafe_allow_html=True)800 801 st.markdown("<br>", unsafe_allow_html=True)802 st.markdown("""803 - **R² = 0.9178** — the model explains **91.78%** of the variance in meter readings ✅804 - **MAE = 27.88 kWh** — average prediction error of ~27.88 kWh (**±18.98%** of the mean meter reading)805 - **RMSE = 50.82 kWh** — some large errors exist due to outlier sensitivity in the tail806 - **% MAE ≈ ±18.98%** — acceptable for building-level energy demand forecasting807 """)808 809 st.markdown('<div class="sec-head">Feature Importance</div>', unsafe_allow_html=True)810 features_fi = ['Building Area (sqft)','Building Use: Education','Meter: Electricity',811 'Floor Count','Time: Night','Dew Temperature','Air Temperature',812 'Building Use: Lodging','Time: Morning/Day','Era: Post-1980',813 'Building Use: Office','Building Use: Public Svc',814 'Time: Afternoon/Eve','Building Use: Entertainment']815 importance_fi = [0.42,0.21,0.14,0.10,0.05,0.03,0.02,0.01,0.01,0.005,0.002,0.002,0.001,0.001]816 colors_fi = ['#00d4aa' if v>0.05 else '#f5a623' if v>0.01 else '#2a3a3a' for v in importance_fi]817 818 fig_fi, ax_fi = dark_fig(10, 6)819 ax_fi.barh(features_fi[::-1], importance_fi[::-1], color=colors_fi[::-1], edgecolor='none')820 ax_fi.set_xlabel('Relative Importance', color='#5a7a76')821 ax_fi.set_title('Feature Importance — Hypertuned HistGradient Boosting', color='#d4e8e4', fontsize=11)822 ax_fi.set_xlim(0, 0.48)823 for i, (f, v) in enumerate(zip(features_fi[::-1], importance_fi[::-1])):824 ax_fi.text(v + 0.005, i, f'{v:.3f}', va='center', color='#5a7a76', fontsize=8)825 plt.tight_layout(); st.pyplot(fig_fi); plt.close()826 827 st.markdown("""828 **Strong predictors** (teal): Building Area, Education use, Electricity meter, Floor Count, Night time.829 **Moderate** (amber): Dew/Air Temperature, Lodging, Morning/Day.830 **Weak** (dark): Office, Public Service, Entertainment, Afternoon/Evening — these could be dropped to reduce noise.831 """)832 833 st.markdown('<div class="sec-head">Suggestions for Improvement</div>', unsafe_allow_html=True)834 for title, desc in [835 ("Feature interaction", "Create `area_per_floor` or `log_area` — building area is the strongest predictor and derived features may boost accuracy further."),836 ("Drop weak features", "Remove Office, Entertainment, Public Service building use and Afternoon/Evening time category to reduce dimensionality and noise."),837 ("Expand hyperparameter grid", "Add `max_leaf_nodes` to GridSearch — controls tree complexity and can prevent overfitting on OHE columns."),838 ("Cap extreme outliers", "Apply 99th percentile capping (quantile=0.99) to reduce RMSE sensitivity to tail values."),839 ("Log-transform the target", "Apply `log1p` to Meter Reading (kWh) — the target is positively skewed; log transformation often improves tree-based model accuracy."),840 ]:841 st.markdown(f"""842 <div class="card">843 <h4>{title}</h4>844 <p style="font-size:.88rem;color:#d4e8e4;margin:0">{desc}</p>845 </div>846 """, unsafe_allow_html=True)847 848# ──────────────────────────────────────────────────────────849# TAB 3 — SINGLE PREDICTION + HISTORY LOG850# ──────────────────────────────────────────────────────────851with tab_pred:852 st.markdown('<div class="sec-head">Energy Demand Prediction</div>', unsafe_allow_html=True)853 st.markdown("Fill in the building and weather details to predict the meter reading (kWh). Every prediction is saved to the log below.")854 855 if model is None:856 st.error("Model not loaded. Ensure `pipelines_inference_st.pkl` is present in the `./src/` folder.")857 else:858 with st.form(key='form_energy_predictor'):859 st.markdown('#### 🏢 ID Information')860 col_id1, col_id2, col_id3 = st.columns(3)861 with col_id1: id_val = st.number_input('ID', min_value=1, max_value=1000000, step=1)862 with col_id2: building_id = st.number_input('Building ID', min_value=1, max_value=1000000, step=1)863 with col_id3: site_id = st.number_input('Site ID', min_value=1, max_value=1000000, step=1)864 st.markdown('---')865 866 st.markdown('#### 🏗️ Building Information')867 col_b1, col_b2 = st.columns(2)868 with col_b1:869 building_use = st.selectbox('Building Use', (870 'Education','Office','Public services',871 'Lodging/residential','Entertainment/public assembly'))872 building_area = st.number_input('Building Area (sqft)',873 min_value=0.0, max_value=10_000_000_000.0,874 value=5000.0, step=10.0)875 year_built = st.number_input('Year Built',876 min_value=1900, max_value=datetime.date.today().year,877 value=2000, step=1)878 with col_b2:879 floor_count = st.number_input('Floor Count', min_value=0, max_value=400, value=1, step=1)880 meter_type = st.selectbox('Meter Type', (0, 3), help='0 = Electricity | 3 = Hot Water')881 st.markdown('---')882 883 st.markdown('#### 🌤️ Weather Information')884 col_w1, col_w2 = st.columns(2)885 with col_w1:886 date = st.date_input('Date')887 time = st.time_input('Time', value=datetime.time(0, 0))888 time_stamp = datetime.datetime.combine(date, time)889 air_temp = st.number_input('Air Temperature (°C)', min_value=-100.0, max_value=100.0, value=25.0, step=0.1)890 cloud_cover = st.number_input('Cloud Coverage (oktas)', min_value=0, max_value=8, value=0, step=1)891 dew_temp = st.number_input('Dew Temperature (°C)', min_value=-50.0, max_value=60.0, value=10.0, step=0.1)892 with col_w2:893 precip = st.number_input('Precipitation Depth (mm/hr)', min_value=0.0, max_value=300.0, value=0.0, step=0.1)894 sealv_pres = st.number_input('Sea Level Pressure (mbar)', min_value=870.0, max_value=1083.8, value=1013.0, step=0.1)895 wind_dir = st.number_input('Wind Direction (degree)', min_value=0, max_value=360, value=180, step=1)896 wind_spd = st.number_input('Wind Speed (m/s)', min_value=0.0, max_value=100.0, value=5.0, step=0.1)897 st.markdown('---')898 899 submitted = st.form_submit_button('⚡ Predict Meter Reading')900 901 if submitted:902 with st.spinner('Running inference…'):903 data_inf = pd.DataFrame([{904 'ID' : id_val,905 'Building ID' : building_id,906 'Site ID' : site_id,907 'Building Use' : building_use,908 'Building Area (sqft)' : building_area,909 'Year Built' : year_built,910 'Floor Count' : floor_count,911 'Meter Type' : meter_type,912 'Time Stamp' : time_stamp,913 'Air Temperature (°C)' : air_temp,914 'Cloud Coverage (oktas)' : cloud_cover,915 'Dew Temperature (°C)' : dew_temp,916 'Precipitation Depth (mm/hr)' : precip,917 'Sea Level Pressure (mbar)' : sealv_pres,918 'Wind Direction (degree)' : wind_dir,919 'Wind Speed (m/s)' : wind_spd,920 }])921 data_inf = preprocess_inference_data(data_inf)922 y_pred = model.predict(data_inf)923 pred_val = float(y_pred[0])924 925 st.success('Prediction complete!')926 st.markdown('<div class="sec-head">Result</div>', unsafe_allow_html=True)927 r1, r2, r3 = st.columns(3)928 r1.metric('Predicted Meter Reading', f'{pred_val:,.2f} kWh')929 r2.metric('Meter Type', 'Electricity' if meter_type == 0 else 'Hot Water')930 r3.metric('Building Era', 'Pre-1981' if year_built < 1981 else 'Post-1980')931 932 # ── Save to history log ──933 st.session_state.pred_history.append({934 'Predicted At' : datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),935 'Predicted Meter Reading (kWh)' : round(pred_val, 4),936 'ID' : id_val,937 'Building ID' : building_id,938 'Site ID' : site_id,939 'Building Use' : building_use,940 'Building Area (sqft)' : building_area,941 'Year Built' : year_built,942 'Floor Count' : floor_count,943 'Meter Type' : meter_type,944 'Time Stamp' : str(time_stamp),945 'Air Temperature (°C)' : air_temp,946 'Cloud Coverage (oktas)' : cloud_cover,947 'Dew Temperature (°C)' : dew_temp,948 'Precipitation Depth (mm/hr)' : precip,949 'Sea Level Pressure (mbar)' : sealv_pres,950 'Wind Direction (degree)' : wind_dir,951 'Wind Speed (m/s)' : wind_spd,952 })953 954 st.markdown('<div class="sec-head">Input Data Submitted</div>', unsafe_allow_html=True)955 st.dataframe(data_inf, use_container_width=True)956 957 # ── PREDICTION HISTORY LOG ──958 st.markdown('<div class="sec-head">Prediction History Log</div>', unsafe_allow_html=True)959 960 if not st.session_state.pred_history:961 st.info("No predictions yet in this session. Run a prediction above to start logging.")962 else:963 history_df = pd.DataFrame(st.session_state.pred_history)964 n = len(history_df)965 966 # summary metrics967 hm1, hm2, hm3, hm4 = st.columns(4)968 hm1.metric("Total Predictions", n)969 hm2.metric("Avg Reading", f"{history_df['Predicted Meter Reading (kWh)'].mean():,.2f} kWh")970 hm3.metric("Max Reading", f"{history_df['Predicted Meter Reading (kWh)'].max():,.2f} kWh")971 hm4.metric("Min Reading", f"{history_df['Predicted Meter Reading (kWh)'].min():,.2f} kWh")972 973 st.dataframe(history_df, use_container_width=True)974 975 ts = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')976 dl1, dl2, dl3 = st.columns(3)977 with dl1:978 st.download_button(979 label="⬇ Download Log — CSV",980 data=history_to_csv(st.session_state.pred_history),981 file_name=f"energy_prediction_log_{ts}.csv",982 mime="text/csv",983 )984 with dl2:985 st.download_button(986 label="⬇ Download Log — TXT",987 data=history_to_txt(st.session_state.pred_history),988 file_name=f"energy_prediction_log_{ts}.txt",989 mime="text/plain",990 )991 with dl3:992 if st.button("🗑 Clear History"):993 st.session_state.pred_history = []994 st.rerun()995 996# ──────────────────────────────────────────────────────────997# TAB 4 — BATCH PREDICTION998# ──────────────────────────────────────────────────────────999with tab_batch:1000 st.markdown('<div class="sec-head">Batch Prediction via Excel Upload</div>', unsafe_allow_html=True)1001 st.markdown("""1002 Upload an Excel file (`.xlsx`) where each row is one building+weather record.1003 The file must have the same columns as the single prediction form.1004 Download the template below to get started.1005 """)1006 1007 # ── Template download ──1008 TEMPLATE_COLS = [1009 'ID','Building ID','Site ID','Building Use','Building Area (sqft)',1010 'Year Built','Floor Count','Meter Type','Time Stamp',1011 'Air Temperature (°C)','Cloud Coverage (oktas)','Dew Temperature (°C)',1012 'Precipitation Depth (mm/hr)','Sea Level Pressure (mbar)',1013 'Wind Direction (degree)','Wind Speed (m/s)',1014 ]1015 EXAMPLE_ROW = {1016 'ID': 1, 'Building ID': 101, 'Site ID': 1,1017 'Building Use': 'Education', 'Building Area (sqft)': 50000.0,1018 'Year Built': 1995, 'Floor Count': 3, 'Meter Type': 0,1019 'Time Stamp': '2016-06-01 08:00:00',1020 'Air Temperature (°C)': 22.5, 'Cloud Coverage (oktas)': 2,1021 'Dew Temperature (°C)': 12.0, 'Precipitation Depth (mm/hr)': 0.0,1022 'Sea Level Pressure (mbar)': 1013.0, 'Wind Direction (degree)': 180,1023 'Wind Speed (m/s)': 4.5,1024 }1025 1026 tmpl_buf = io.BytesIO()1027 pd.DataFrame([EXAMPLE_ROW]).to_excel(tmpl_buf, index=False)1028 tmpl_buf.seek(0)1029 1030 st.download_button(1031 label="⬇ Download Excel Template",1032 data=tmpl_buf.getvalue(),1033 file_name="batch_prediction_template.xlsx",1034 mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",1035 )1036 1037 st.markdown("---")1038 st.markdown("**Required columns and accepted values:**")1039 col_info = {1040 'ID / Building ID / Site ID': 'Integer identifiers',1041 'Building Use': 'Education | Office | Public services | Lodging/residential | Entertainment/public assembly',1042 'Building Area (sqft)': 'Float — building gross floor area',1043 'Year Built': 'Integer (1900 – present)',1044 'Floor Count': 'Integer',1045 'Meter Type': '0 = Electricity | 3 = Hot Water',1046 'Time Stamp': 'Datetime string e.g. 2016-06-01 08:00:00',1047 'Weather columns': 'Air Temp (°C), Cloud Coverage (oktas 0-8), Dew Temp (°C), Precip (mm/hr), Sea Level Pressure (mbar), Wind Dir (0-360°), Wind Speed (m/s)',1048 }1049 for k, v in col_info.items():1050 st.markdown(f"""1051 <div style="display:flex;gap:1rem;padding:.4rem 0;border-bottom:1px solid #1e2e2e;font-size:.85rem">1052 <span style="color:#00d4aa;font-family:'DM Mono',monospace;min-width:220px">{k}</span>1053 <span style="color:#d4e8e4">{v}</span>1054 </div>1055 """, unsafe_allow_html=True)1056 1057 st.markdown("---")1058 1059 uploaded_xl = st.file_uploader(1060 "Upload your Excel file (.xlsx)",1061 type=["xlsx"],1062 key="batch_uploader"1063 )1064 1065 if uploaded_xl and model is not None:1066 try:1067 df_batch_raw = pd.read_excel(uploaded_xl)1068 except Exception as e:1069 st.error(f"Could not read Excel file: {e}")1070 df_batch_raw = None1071 1072 if df_batch_raw is not None:1073 # validate columns1074 missing_cols = [c for c in TEMPLATE_COLS if c not in df_batch_raw.columns]1075 if missing_cols:1076 st.error(f"Missing columns in uploaded file: {missing_cols}")1077 else:1078 st.success(f"File loaded — {len(df_batch_raw)} rows detected.")1079 st.dataframe(df_batch_raw.head(5), use_container_width=True)1080 1081 if st.button("▶ Run Batch Prediction"):1082 results = []1083 errors = []1084 prog = st.progress(0, text="Starting…")1085 n_rows = len(df_batch_raw)1086 1087 for i, row in df_batch_raw.iterrows():1088 try:1089 row_df = pd.DataFrame([row.to_dict()])1090 row_df = preprocess_inference_data(row_df)1091 pred = float(model.predict(row_df)[0])1092 result_row = row.to_dict()1093 result_row['Predicted Meter Reading (kWh)'] = round(pred, 4)1094 results.append(result_row)1095 except Exception as e:1096 result_row = row.to_dict()1097 result_row['Predicted Meter Reading (kWh)'] = 'ERROR'1098 result_row['Error'] = str(e)1099 results.append(result_row)1100 errors.append(i)1101 1102 prog.progress((i + 1) / n_rows, text=f"Processing row {i+1} of {n_rows}…")1103 1104 prog.empty()1105 st.success(f"Done! {n_rows - len(errors)} predictions successful, {len(errors)} errors.")1106 1107 results_df = pd.DataFrame(results)1108 1109 # move prediction column to front1110 pred_col = 'Predicted Meter Reading (kWh)'1111 cols = [pred_col] + [c for c in results_df.columns if c != pred_col]1112 results_df = results_df[cols]1113 1114 # summary metrics1115 valid = results_df[results_df[pred_col] != 'ERROR']1116 if len(valid) > 0:1117 bm1, bm2, bm3, bm4 = st.columns(4)1118 numeric_preds = pd.to_numeric(valid[pred_col])1119 bm1.metric("Total Rows", n_rows)1120 bm2.metric("Avg Reading", f"{numeric_preds.mean():,.2f} kWh")1121 bm3.metric("Max Reading", f"{numeric_preds.max():,.2f} kWh")1122 bm4.metric("Min Reading", f"{numeric_preds.min():,.2f} kWh")1123 1124 st.markdown('<div class="sec-head">Batch Results</div>', unsafe_allow_html=True)1125 st.dataframe(results_df, use_container_width=True)1126 1127 # downloads1128 ts_batch = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')1129 bd1, bd2 = st.columns(2)1130 1131 # CSV1132 csv_buf = io.StringIO()1133 results_df.to_csv(csv_buf, index=False)1134 with bd1:1135 st.download_button(1136 label="⬇ Download Results — CSV",1137 data=csv_buf.getvalue().encode('utf-8'),1138 file_name=f"batch_predictions_{ts_batch}.csv",1139 mime="text/csv",1140 )1141 1142 # TXT1143 with bd2:1144 st.download_button(1145 label="⬇ Download Results — TXT",1146 data=batch_to_txt(results_df),1147 file_name=f"batch_predictions_{ts_batch}.txt",1148 mime="text/plain",1149 )1150 1151 elif uploaded_xl and model is None:1152 st.error("Model not loaded. Ensure `pipelines_inference_st.pkl` is in the `./src/` folder.")1153 1154 