CoolFace
Apppublic

Devina707/Building-Energy-Demand-Prediction

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
app.py1154 linesDownload Raw Back to root
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 &nbsp;·&nbsp;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 &nbsp;·&nbsp; Site ID 1 &nbsp;·&nbsp; 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 &nbsp;→&nbsp; OneHotEncoding &nbsp;→&nbsp; MinMaxScaling &nbsp;→&nbsp; 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