MEG529/BuildingEnergy
0
1# -*- coding: utf-8 -*-2"""3建築能耗指標 (EPI) 預測系統 — Hugging Face Spaces 部署版4"""5 6import pandas as pd7import numpy as np8import os9from collections import OrderedDict10import gradio as gr11 12from sklearn.linear_model import LinearRegression13from sklearn.ensemble import RandomForestRegressor14from lightgbm import LGBMRegressor15from catboost import CatBoostRegressor16import xgboost as xgb17 18# ==========================================19# 區塊 1: 載入資料20# ==========================================21path_bdg = '.'22 23electricity = pd.read_csv(os.path.join(path_bdg, '模擬結果.csv'), encoding='big5')24builddata = pd.read_csv(os.path.join(path_bdg, '外殼參數.csv'), encoding='big5')25HVACdata = pd.read_csv(os.path.join(path_bdg, '空調參數.csv'), encoding='big5')26 27electricity = electricity.iloc[:, 1:]28builddata = builddata.iloc[:, 1:]29HVACdata = HVACdata.iloc[:, 1:]30 31allsingle_meter = pd.concat([electricity, builddata, HVACdata], axis=1)32 33# ==========================================34# 區塊 2: 特徵篩選(相關係數 > 0.1)35# ==========================================36target_columns = electricity.columns.tolist()37feature_columns = [col for col in allsingle_meter.columns if col not in target_columns]38corr_matrix = allsingle_meter.corr()39corr_subset = corr_matrix.loc[feature_columns, target_columns]40 41res_df = corr_subset.unstack().reset_index()42res_df.columns = ['目標變數', '特徵名稱', '相關係數']43res_df = res_df[res_df['相關係數'].abs() > 0.1].sort_values(by='目標變數')44 45target_features_map = res_df.groupby('目標變數')['特徵名稱'].apply(list).to_dict()46all_unique_features = sorted(list(set().union(*target_features_map.values())))47 48# ==========================================49# 區塊 3: 特徵顯示設定(標籤、單位、分組、選項名稱)50# ==========================================51 52# 原始欄位名 -> (顯示名稱, 單位)53FEATURE_LABEL_MAP = {54 '樓板面積': ('樓板面積', 'm²'),55 '地點': ('地點', ''),56 '方位': ('方位', '度'),57 '樓層數': ('樓層數', ''),58 '體型係數(V/F)': ('體型係數(V/F)', 'm³/m²'),59 '樓高': ('樓高', 'm'),60 '窗牆比 (WWR)': ('窗牆比 (WWR)', ''),61 '遮陽比': ('遮陽比', ''),62 'SHGC': ('SHGC', ''),63 '窗戶U值': ('窗戶U值', 'W/m²·K'),64 '外牆U值': ('外牆U值', 'W/m²·K'),65 '屋頂U值': ('屋頂U值', 'W/m²·K'),66 '設備密度': ('設備密度', 'W/m²'),67 '照明密度': ('照明密度', 'W/m²'),68 '人員密度': ('人員密度', 'person/m²'),69 '活動量': ('活動量', 'met'),70 '間隙風': ('間隙風', 'L/s-m²'),71 '室外新風量': ('室外新風量', 'L/s·m²'),72 '冷房設定值': ('冷房設定值', '°C'),73 '主機類型': ('主機類型', ''),74 '主機 COP': ('主機 COP', 'kW/kW'),75 '風機效率ATF': ('風機效率ATF', 'W·s/L'),76 '冰水泵輸送效率WTF': ('冰水泵輸送效率WTF', 'W·s/L'),77 '區域泵輸送效率WTF': ('區域泵輸送效率WTF', 'W·s/L'),78 '冷卻水泵輸送效率WTF': ('冷卻水泵輸送效率WTF', 'W·s/L'),79 '冷卻水泵輸送效率WTF.1': ('冷卻水塔效率', 'L/s·kW'),80 '安全係數': ('安全係數', ''),81 '空氣側變風量系統': ('空氣側變風量系統', ''),82 '冰水VWV': ('冰水VWV', ''),83 '全熱交換器': ('全熱交換器', ''),84 'CO2濃度控制': ('CO2濃度控制', ''),85 '冷卻水塔濕球溫度接近控制': ('冷卻水塔濕球溫度接近控制', ''),86 '冷凍水溫度重設': ('冷凍水溫度重設', ''),87 '主機變頻': ('主機變頻', ''),88}89 90# 原始代碼(數字) -> 顯示名稱(依實際資料中的唯一值設定)91FEATURE_CHOICES_MAP = {92 '地點': {1: '臺北', 2: '臺中', 3: '高雄'},93 '主機類型': {1: '離心式', 3: '往復式'},94 '主機變頻': {0: '無', 1: '主機變頻'},95 '空氣側變風量系統': {0: '無', 1: '空氣側變風量系統'},96 '冰水VWV': {0: '無',97 1: '使用三通閥冰水系統',98 2: '一次變頻冰水系統',99 3: '一次定頻/二次變頻冰水系統(含二次以上)'},100 '全熱交換器': {0: '無',101 1: '無外氣旁通全熱交換器系統',102 2: '外氣旁通全熱交換器系統'},103 'CO2濃度控制': {0: '無', 1: 'CO2濃度控制'},104 '冷卻水塔濕球溫度接近控制': {0: '無',105 1: '定頻+出水溫度控制',106 2: '定頻+濕球溫度控制',107 3: '出水溫度變頻控制',108 4: '濕球溫度及水溫變頻控制'},109 '冷凍水溫度重設': {0: '無', 1: '冷凍水溫度重設'},110}111 112# 分組定義(按截圖順序)113FEATURE_GROUPS = [114 ('建築幾何', ['樓板面積', '地點', '方位', '樓層數', '體型係數(V/F)', '樓高', '窗牆比 (WWR)', '遮陽比']),115 ('外殼', ['SHGC', '窗戶U值', '外牆U值', '屋頂U值']),116 ('內部負載', ['設備密度', '照明密度', '人員密度', '活動量', '間隙風']),117 ('操作', ['室外新風量', '冷房設定值']),118 ('HVAC 系統',['主機類型', '主機 COP', '風機效率ATF', '冰水泵輸送效率WTF',119 '區域泵輸送效率WTF', '冷卻水泵輸送效率WTF', '冷卻水泵輸送效率WTF.1']),120 ('節能措施', ['安全係數', '空氣側變風量系統', '冰水VWV', '全熱交換器',121 'CO2濃度控制', '冷卻水塔濕球溫度接近控制', '冷凍水溫度重設', '主機變頻']),122]123 124def get_label(feature):125 display, unit = FEATURE_LABEL_MAP.get(feature, (feature, ''))126 return f"{display}(單位: {unit})" if unit else display127 128# ==========================================129# 區塊 4: 訓練模型(啟動時執行一次)130# ==========================================131selected_model_config = {132 'EPIG': 'CatBoost',133 'EPIL': 'LinearRegression',134 'EPIS': 'CatBoost',135 'EPICG': 'LinearRegression',136 'EPIAT': 'LGBM',137 'EPIWT': 'CatBoost',138}139 140def get_model_instance(model_key):141 if model_key == 'LinearRegression': return LinearRegression()142 if model_key == 'RandomForest': return RandomForestRegressor(random_state=42)143 if model_key == 'LGBM': return LGBMRegressor(random_state=42, verbose=-1)144 if model_key == 'XGBoost': return xgb.XGBRegressor(random_state=42)145 if model_key == 'CatBoost': return CatBoostRegressor(random_state=42, verbose=0, allow_writing_files=False)146 raise ValueError(f"未知模型: {model_key}")147 148print("正在訓練模型,請稍候...")149trained_models = {}150for target, model_key in selected_model_config.items():151 features = target_features_map[target]152 model = get_model_instance(model_key)153 model.fit(allsingle_meter.iloc[:500][features], electricity.iloc[:500][target])154 trained_models[target] = model155 print(f" ✅ {target} ({model_key}) 訓練完成")156print("所有模型訓練完成!")157 158# ==========================================159# 區塊 5: 顯示名稱 -> 原始代碼(預測時反查)160# ==========================================161feature_label_to_code = {}162for feat, mapping in FEATURE_CHOICES_MAP.items():163 feature_label_to_code[feat] = {v: k for k, v in mapping.items()}164 165# 依分組收集有進入 all_unique_features 的特徵,保留順序166grouped_features = []167seen = set()168for group, feats in FEATURE_GROUPS:169 for f in feats:170 if f in all_unique_features and f not in seen:171 grouped_features.append((group, f))172 seen.add(f)173for f in all_unique_features:174 if f not in seen:175 grouped_features.append(('其他', f))176 177# ==========================================178# 區塊 6: 預測函數179# ==========================================180def predict(*args):181 user_inputs = {}182 for idx, (group, feature) in enumerate(grouped_features):183 raw = args[idx]184 if feature in feature_label_to_code:185 user_inputs[feature] = feature_label_to_code[feature].get(raw, raw)186 else:187 try:188 user_inputs[feature] = float(raw)189 except Exception:190 user_inputs[feature] = raw191 192 # 下標設定:EPI名稱 -> (前綴, 下標文字)193 EPI_DISPLAY = {194 "EPIG": ("EPI", "G"),195 "EPIL": ("EPI", "L"),196 "EPIS": ("EPI", "S"),197 "EPICG": ("EPI", "CG"),198 "EPIAT": ("EPI", "AT"),199 "EPIWT": ("EPI", "WT"),200 }201 EPI_UNITS = {202 "EPIG": "kWh/m²·yr",203 "EPIL": "kWh/m²·yr",204 "EPIS": "kWh/kWh",205 "EPICG": "kWh/kWh",206 "EPIAT": "kWh/kWh",207 "EPIWT": "kWh/kWh",208 }209 rows = []210 for target, model in trained_models.items():211 prefix, sub = EPI_DISPLAY.get(target, (target, ""))212 unit = EPI_UNITS.get(target, "")213 label_html = f"{prefix}<sub>{sub}</sub>" if sub else prefix214 try:215 needed = target_features_map[target]216 X_df = pd.DataFrame([{f: user_inputs[f] for f in needed}])217 pred_val = model.predict(X_df)[0]218 rows.append(219 f"<div class='epi-row'>"220 f"<span class='epi-name'>📊 {label_html}</span>"221 f"<span class='epi-model'>({selected_model_config[target]})</span>"222 f"<span class='epi-value'>{pred_val:.2f} <span class='epi-unit'>{unit}</span></span>"223 f"</div>"224 )225 except Exception as e:226 rows.append(f"<div class='epi-row epi-error'>❌ {label_html}:預測失敗({e})</div>")227 return "<div class='epi-results'>" + "".join(rows) + "</div>"228 229 230# ==========================================231# 區塊 7: 自訂 CSS232# ==========================================233custom_css = """234body { font-family: 'Noto Sans TC', sans-serif; font-size: 1.15em; }235#title-md h2 {236 text-align: center;237 color: #2e7d32;238 font-size: 1.9em;239 font-weight: 700;240 padding: 8px 0 2px 0;241}242.section-header p { color: #1b5e20; font-weight: 700; font-size: 1.2em; margin: 2px 0 4px 0; }243.group-box {244 border: 1.5px solid #a5d6a7;245 border-radius: 10px;246 padding: 10px 16px 4px 16px;247 margin-bottom: 8px;248 background: #f1f8e9;249}250.group-box label { font-size: 1.1em !important; }251.group-box input, .group-box select { font-size: 1.1em !important; }252.gradio-slider { margin-bottom: 4px !important; }253.gradio-dropdown { margin-bottom: 4px !important; }254.predict-btn { background-color: #2e7d32 !important; color: white !important; font-size: 1.15em !important; }255.result-area textarea { font-size: 1.15em !important; line-height: 1.5 !important; }256.epi-results { padding: 8px 4px; }257.epi-row { display: flex; align-items: baseline; gap: 10px; padding: 6px 8px; border-bottom: 1px solid #c8e6c9; font-size: 1.1em; }258.epi-row:last-child { border-bottom: none; }259.epi-name { font-weight: 700; color: #1b5e20; min-width: 90px; }260.epi-model { color: #777; font-size: 0.9em; min-width: 130px; }261.epi-value { font-size: 1.15em; font-weight: 600; color: #2e7d32; }262.epi-unit { font-size: 0.85em; color: #555; font-weight: 400; }263.epi-error { color: #c62828; }264footer { display: none !important; }265"""266 267# ==========================================268# 區塊 8: 組裝 Gradio Blocks269# ==========================================270groups_dict = OrderedDict()271for group, feature in grouped_features:272 groups_dict.setdefault(group, []).append(feature)273 274with gr.Blocks(css=custom_css, title="建築能效快速評估平台-FCU系統") as demo:275 gr.Markdown("## 建築能效快速評估平台-FCU系統", elem_id="title-md")276 gr.Markdown("### 輸入參數", elem_classes=["section-header"])277 278 all_comp_ordered = []279 280 for group_name, feats in groups_dict.items():281 with gr.Group(elem_classes=["group-box"]):282 gr.Markdown(f"**{group_name}**")283 pairs = [feats[i:i+2] for i in range(0, len(feats), 2)]284 for pair in pairs:285 with gr.Row():286 for feature in pair:287 label = get_label(feature)288 data = allsingle_meter[feature]289 mapping = FEATURE_CHOICES_MAP.get(feature)290 291 if mapping:292 labels_list = list(mapping.values())293 comp = gr.Dropdown(choices=labels_list,294 value=labels_list[0],295 label=label)296 elif pd.api.types.is_string_dtype(data) or data.nunique() <= 10:297 choices = [str(x) for x in sorted(data.unique().tolist())]298 comp = gr.Dropdown(choices=choices,299 value=choices[0],300 label=label)301 else:302 mn = float(data.min())303 mx = float(data.max())304 dp = max((len(str(float(v)).rstrip("0").split(".")[1])305 if "." in str(float(v)) else 0)306 for v in data.unique())307 stp = 1 if dp == 0 else round(10 ** -dp, dp)308 mv = round(float(data.mean()), dp)309 comp = gr.Slider(minimum=mn, maximum=mx,310 step=stp, value=mv, label=label)311 all_comp_ordered.append(comp)312 313 gr.Markdown("---")314 gr.Markdown("### 模型預測結果")315 predict_btn = gr.Button("🔍 進行預測", elem_classes=["predict-btn"])316 result_box = gr.HTML(value="<div class='epi-results' style='color:#888;padding:8px;'>請輸入參數後按下「進行預測」</div>",317 elem_classes=["result-area"])318 319 predict_btn.click(fn=predict, inputs=all_comp_ordered, outputs=result_box)320 321demo.launch()322 