lauren-cw's picture
Upload app.py
71ce824 verified
raw
history blame
14.5 kB
# -*- coding: utf-8 -*-
"""DASS心理模型(Q12).ipynb
Automatically generated by Colab.
Original file is located at
https://colab.research.google.com/drive/19ATyW5Lb692QV2Gk2I0rlsbeSQNwEDnQ
建立環境
"""
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import gradio as gr
import sklearn
import pickle
import joblib
import time
import os
import json
import gspread
from sklearn.model_selection import train_test_split, RandomizedSearchCV
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, confusion_matrix, f1_score, accuracy_score, precision_score, balanced_accuracy_score
from sklearn.ensemble import RandomForestClassifier
from lightgbm import LGBMClassifier
from AutoPreprocess import AutoPreprocess
from google.oauth2.service_account import Credentials
from datetime import datetime, timezone, timedelta
"""Gradio 使用者介面"""
# 載入模型
import pickle
model_path = os.path.abspath("DASS_model.bin")
with open(model_path, "rb") as f:
model = pickle.load(f)
model
"""定義歷史紀錄功能"""
def update_history(current_result_1, current_result_2, history_list):
"""
current_result_1 & 2: 來自 predict_risk 的兩個回傳值 (HTML 字串)
history_list: 來自 gr.State 的現有紀錄列表
"""
# 獲取當前時間,格式為:2023-10-27 14:30:05
now = datetime.now(tw_timezone).strftime("%Y-%m-%d %H:%M:%S")
# 組合這次的結果 (假設你想存這兩個 outputs 的組合)
new_entry = f"""
<div style="border-bottom: 2px solid #eee; padding-bottom: 20px; margin-bottom: 20px;">
<div style="font-size: 18px; color: #666; margin-bottom: 10px; font-weight: bold;">
🕒 測驗時間:{now}
</div>
<div style="
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
gap: 20px;
border-bottom: 1px dashed #ccc;
padding-bottom: 15px;
margin-bottom: 15px;
width: 100%;">
<div style="flex: 1;">{current_result_1}</div>
<div style="flex: 1;">{current_result_2}</div>
</div>
</div>
"""
# 將新紀錄放在最前面 (置頂)
history_list.insert(0, new_entry)
# 組合所有歷史紀錄,並整體縮小 80%
# 使用 zoom: 0.8 或 transform 達到字體與版面同時縮小的效果
combined_html = f"""
<div style="zoom: 0.8; -moz-transform: scale(0.8); -moz-transform-origin: 0 0;">
{"".join(history_list)}
</div>
"""
return combined_html, history_list
"""定義儲存測試資料的功能"""
import os
import json
from datetime import datetime, timezone, timedelta
import gspread
from google.oauth2.service_account import Credentials
# 設定台灣時區
tw_timezone = timezone(timedelta(hours=8))
def save_to_google_sheets(inputs, a_score, d_score, s_score, t_score, score):
# 1. 設定 Google Sheets 存取權限
scope = ['https://www.googleapis.com/auth/spreadsheets',
'https://www.googleapis.com/auth/drive']
# 設定Secret Variables(藏金鑰)
google_json = os.environ.get("DASS_JSON")
info = json.loads(google_json)
creds = Credentials.from_service_account_info(info, scopes=scope)
client = gspread.authorize(creds)
# 2. 開啟指定名稱的試算表 (確保已分享權限給 service account)
sheet = client.open("DASS使用者測試資料").sheet1 # 存於檔案的第一張工作表
# 1. 拆分資料:前 3 個是基本資料,後面剩下的 (*rest) 是 12 題答案
user_info = inputs[:3] # 取得前三個:姓名, 年齡, 性別
q_answers = inputs[3:] # 取得剩下的 12 題
now = datetime.now(tw_timezone).strftime("%Y-%m-%d %H:%M:%S")
# 2. 準備要儲存的資料字典
row_to_add = [
now, # 欄位 A: 測試時間
user_info[0], # 欄位 B: 性別
user_info[1], # 欄位 C: 年齡
user_info[2], # 欄位 D: 家庭人數
a_score, # 欄位 E: 焦慮分數
d_score, # 欄位 F: 憂鬱分數
s_score, # 欄位 G: 壓力分數
t_score, # 欄位 H: 總體分數
score # 欄位 I: 整體程度 (標籤)
]
row_to_add.extend(q_answers) # 加入 Q1-Q12 (J欄以後)
# 4. 追加到試算表最後一行
sheet.append_row(row_to_add)
"""定義重新測驗功能"""
# 清空函數:回傳與輸入組件數量相同的 None (15個:gen, age, family + 12個問題)
def clear_all():
# 15個輸入(gen, age, family, q1~q12) + 2個即時結果
return [None] * 15 + ["", ""]
"""定義主要測試功能"""
def predict_risk(gen, age, family, q1, q2, q3, q4, q5, q6, q7, q8, q9, q10, q11, q12):
inputs = [gen, age, family, q1, q2, q3, q4, q5, q6, q7, q8, q9, q10, q11, q12]
# 檢查是否有任何一個選項是 None (未按)
if any(v is None or v == "" for v in inputs):
# Return error message to a dedicated output component
return "", "", "<div style=\"color: red; font-weight: bold;\">⚠️測驗載入有誤:請確保每一題都已填答或查看填答格式是否正確。</div>"
# Clear any previous error message if inputs are valid
error_message = ""
# 1. 跑進度條 (需確保函式參數有 progress=gr.Progress())
progress = gr.Progress()
progress(0, desc="模型計算中...")
# 2. 將 12 個輸入整理成模型認得的 DataFrame
# 欄位名稱必須與訓練時完全相同
cols = ["gender", "age", "familysize", "Q2A", "Q4A", "Q19A", "Q20A", "Q28A", "Q21A", "Q26A", "Q37A", "Q42A", "Q11A", "Q12A", "Q27A"]
input_df = pd.DataFrame([[gen, age, family, q1, q2, q3, q4, q5, q6, q7, q8, q9, q10, q11, q12]], columns=cols)
progress(0.5, desc="正在分析數據...")
time.sleep(0.5) # 模擬運算時間
# 3. 使用模型 model 進行預測
score = model.predict(input_df)[0]
progress(1.0, desc="計算完成!")
# 4. 定義風險標籤
if score == 0:
label = "低度風險"
color = "#91cd92" # 綠色
elif score == 1:
label = "中度風險"
color = "#f59e0b" # 橘色
elif score == 2:
label = "高度風險"
color = "#ef4444" # 紅色
else:
label = "計算結果有誤,請重新測試。"
# 定義類別分數條
a_score = (q1 + q2 + q3 + q4 + q5)
d_score = (q6 + q7 + q8 + q9)
s_score = (q10 + q11 + q12)
t_score = a_score + d_score + s_score
max_val = 36
def make_bar(label, score, max_val, color):
percent = (score / max_val) * 100
return f"""
<div style="margin-bottom: 10px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
<span style="font-weight: bold;">{label}</span>
</div>
<div style="background-color: #e0e0e0; border-radius: 10px; height: 12px; width: 100%;">
<div style="background-color: {color}; width: {percent}%; height: 100%; border-radius: 10px;"></div>
</div>
</div>
"""
# 5. 準備回傳內容
# 總分與風險標籤
result_score = f"""
<div style="text-align: center; font-family: sans-serif;">
<h2 style="color: #313230;">您的預測結果為</h2>
<h1 style="font-size: 60px; color: {color}; margin: 0;">
{label}
</h1>
<h1 style="font-size: 20px; color: #bbbbc2; margin: 0;">
{t_score}/36
</h1>
</div>
"""
# 類別分數條
label_html = f"""
<div style="padding: 20px; background: white; border-radius: 10px; border: 1px solid #ddd;">
<h2 style="color: #313230;margin-top: 0; margin-bottom: 15px;">各面向之比重</h2>
{make_bar("焦慮 (Anxiety)", a_score, max_val, "#fccb42")}
{make_bar("憂鬱 (Depression)", d_score, max_val, "#6dc8fe")}
{make_bar("壓力 (Stress)", s_score, max_val, "#fb6d6d")}
</div>
"""
# 儲存測試資料
save_to_google_sheets(inputs, a_score, d_score, s_score, t_score, score)
progress(1.0, desc="完成")
return result_score, label_html, error_message
# 設定主題色
theme = gr.themes.Default(
primary_hue="amber",
secondary_hue="amber",
).set(
body_background_fill="#fffbeb"
)
# 線上主題調色器
# gr.themes.builder()
# 介面編排
#按鈕及面板格式設定
custom_css = """
#my_green_btn {
background-color: #91cd92 !important;
color: white !important;
border: none;
}
#my_green_btn:hover {
background-color: #72a473 !important; /* 滑鼠懸停時變深 */
}
#my_white_btn {
background-color: #ffffff !important;
color: black !important;
border: 1px solid #e4e4e7;
}
#my_white_btn:hover {
background-color: #e4e4e7 !important;
color: black !important; /* 滑鼠懸停時變深 */
}
.my-custom-panel {
background-color: #fffef8 !important;
border: 2px solid #e4e4e7 !important;
padding: 20px;
border-radius: 15px;
}
#history_panel .label-wrap span {
font-weight: bold !important;
}
"""
with gr.Blocks(theme=theme, css=custom_css) as demo:
# 建立 Session 狀態 (開啟瀏覽器時初始化為空列表)
history_state = gr.State([])
# 標題及說明
gr.Markdown("")
gr.HTML(f"""
<div style="text-align: center; font-family: sans-serif;">
<h2 style="font-size: 32px; color: #313230; margin: 0;">🌿心理健康風險程度測試📝</h2>
</div>
""")
gr.HTML(f"""
<div style="text-align: center; font-family: sans-serif;">
<h2 style="font-size: 18px; color: #313230; margin: 0;">歡迎來到心理健康風險程度測試環境!<br>
本測驗將透過12題問答,替您在5分鐘內簡單計算出潛在的心理健康風險程度。<br>
請輕鬆填答,無須思慮過度,測驗愉快!</h2>
</div>
""")
with gr.Column(variant="panel", elem_classes="my-custom-panel"):
# 輸入區塊1(人口靜態欄位)
gr.Markdown("## Step 1. 請輸入基本資訊")
with gr.Row():
with gr.Column():
name = gr.Textbox(label="暱稱")
gen = gr.Dropdown(choices=["男", "女", "其他"],
label="性別",
value=[])
with gr.Column():
age = gr.Number(label="年齡 (僅限填寫數字)", value ="")
family = gr.Number(label="家庭人數 (僅限填寫數字)", value ="")
# 輸入區塊2(測驗題)
gr.Markdown("")
gr.Markdown("## Step 2. 請依自身狀態選擇符合的答案")
q1 = gr.Radio([("從不", 0), ("偶爾", 1), ("經常", 2), ("總是", 3)],
label="Q1.我感覺到口乾舌燥。")
q2 = gr.Radio([("從不", 0), ("偶爾", 1), ("經常", 2), ("總是", 3)],
label="Q2.我感到呼吸困難(例如:在沒有體力勞動的情況下,呼吸過度急促或喘不過氣)。")
q3 = gr.Radio([("從不", 0), ("偶爾", 1), ("經常", 2), ("總是", 3)],
label="Q3.在氣溫不高或沒有體力勞動的情況下,我明顯地流汗(例如:手汗)。")
q4 = gr.Radio([("從不", 0), ("偶爾", 1), ("經常", 2), ("總是", 3)],
label="Q4.我無緣無故地感到害怕。")
q5 = gr.Radio([("從不", 0), ("偶爾", 1), ("經常", 2), ("總是", 3)],
label="Q5.我覺得自己接近恐慌發作的邊緣。")
q6 = gr.Radio([("從不", 0), ("偶爾", 1), ("經常", 2), ("總是", 3)],
label="Q6.我覺得生命沒什麼意義/價值。")
q7 = gr.Radio([("從不", 0), ("偶爾", 1), ("經常", 2), ("總是", 3)],
label="Q7.我感到垂頭喪氣、情緒低落。")
q8 = gr.Radio([("從不", 0), ("偶爾", 1), ("經常", 2), ("總是", 3)],
label="Q8.我覺得未來毫無希望。")
q9 = gr.Radio([("從不", 0), ("偶爾", 1), ("經常", 2), ("總是", 3)],
label="Q9.我發現自己很難打起精神主動去做事。")
q10 = gr.Radio([("從不", 0), ("偶爾", 1), ("經常", 2), ("總是", 3)],
label="Q10.我發現自己很容易變得心煩意亂。")
q11 = gr.Radio([("從不", 0), ("偶爾", 1), ("經常", 2), ("總是", 3)],
label="Q11.我覺得自己消耗了大量的神經能量(處於高度緊繃狀態)。")
q12 = gr.Radio([("從不", 0), ("偶爾", 1), ("經常", 2), ("總是", 3)],
label="Q12.我發現自己非常易怒(容易焦躁)。")
# 確認送出按鈕
sub_button = gr.Button("確認送出", elem_id="my_green_btn")
# 重新測驗按鈕
with gr.Row():
btn_reset = gr.Button("重新測驗", elem_id="my_white_btn")
# 輸出測試結果
with gr.Row():
out_html = gr.HTML()
out_label = gr.HTML()
# --- 新增:歷史紀錄呈現區域 ---
with gr.Accordion("查看歷史紀錄", open=False, elem_id="history_panel"):
history_display = gr.HTML(value="目前尚無測驗紀錄")
# 按鈕設定
# 1. 確認送出
sub_button.click(fn=predict_risk,
inputs= [gen, age, family, q1, q2, q3, q4, q5, q6, q7, q8, q9, q10, q11, q12],
outputs= [out_html, out_label]
).then(
fn=update_history,
inputs=[out_html, out_label, history_state],
outputs=[history_display, history_state])
# 2. 重新測驗 (清空所有輸入與輸出)
# 注意:outputs 必須包含所有輸入的組件
btn_reset.click(
fn=lambda: [None]*15 + ["", ""],
inputs=None,
outputs=[gen, age, family, q1, q2, q3, q4, q5, q6, q7, q8, q9, q10, q11, q12, out_html , out_label]
)
gr.Markdown("## 免責聲明")
gr.Markdown("""本測驗結果僅供參考,非屬正規醫療檢驗範疇。
若對於自身狀況有任何疑慮,敬請尋求正規專業醫療協助!♡第四組關心您♡""")
demo.launch(share=True)
# 如需免費永久托管,需在終端機模式執行「gradio deploy」部署到 Hugging Face Spaces。