#!/usr/bin/env python # coding: utf-8 # 建立環境 # In[50]: # get_ipython().system('pip install gradio') # In[51]: # get_ipython().system('pip install gspread google-auth') # In[52]: 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 使用者介面 # In[53]: # 載入模型 import pickle model_path = os.path.abspath("DASS_model.bin") with open(model_path, "rb") as f: model = pickle.load(f) model # 定義儲存測試資料的功能 # In[54]: 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. 追加到試算表最後一行 def to_py(v): return v.item() if hasattr(v, "item") else v row_to_add = [to_py(x) for x in row_to_add] sheet.append_row(row_to_add) def save_to_google_sheets_full42(inputs, dep_score, anx_score, str_score, overall, answers42): scope = ['https://www.googleapis.com/auth/spreadsheets', 'https://www.googleapis.com/auth/drive'] 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) sh = client.open("DASS使用者測試資料") ws_title = "DASS42_完整版" try: sheet = sh.worksheet(ws_title) except gspread.exceptions.WorksheetNotFound: sheet = sh.add_worksheet(title=ws_title, rows=2000, cols=60) header = ( ["測試時間", "性別", "年齡", "家庭人數", "焦慮分數", "憂鬱分數", "壓力分數", "總體分數", "整體程度"] + [f"Q{i}" for i in range(1, 43)] ) if sheet.acell("A1").value in (None, ""): sheet.update("A1", [header]) # ===== 整理資料 ===== now = datetime.now(tw_timezone).strftime("%Y-%m-%d %H:%M:%S") # inputs = [name, gen, age, family] + answers _, gen, age, family = inputs[0], inputs[1], inputs[2], inputs[3] # 若 family 是 15人以上 → 存成 16 if family == "15人以上": family = 16 t_score = dep_score + anx_score + str_score row_to_add = [ now, gen, age, family, anx_score, dep_score, str_score, t_score, overall ] row_to_add.extend(list(answers42)) def to_py(v): return v.item() if hasattr(v, "item") else v row_to_add = [to_py(x) for x in row_to_add] sheet.append_row(row_to_add) # 定義歷史紀錄功能 # In[56]: 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"""
🕒 測驗時間:{now}
{current_result_1}
{current_result_2}
""" # 將新紀錄放在最前面 (置頂) history_list.insert(0, new_entry) # 組合所有歷史紀錄,並整體縮小 80% # 使用 zoom: 0.8 或 transform 達到字體與版面同時縮小的效果 combined_html = f"""
{"".join(history_list)}
""" return combined_html, history_list # 定義重新測驗功能 # In[57]: # 清空函數:回傳與輸入組件數量相同的 None (15個:gen, age, family + 12個問題) def clear_all(): # 15個輸入(gen, age, family, q1~q12) + 2個即時結果 + 1個歷史面板 return [None] * 15 + ["", ""] # 排名設定 def make_rank_list(a_score, d_score, s_score): items = [ ("焦慮 (Anxiety)", a_score), ("憂鬱 (Depression)", d_score), ("壓力 (Stress)", s_score), ] items_sorted = sorted(items, key=lambda x: x[1], reverse=True) medal = ["🥇", "🥈", "🥉"] rows = "" for i, (name, _) in enumerate(items_sorted): rows += f"""
{medal[i]} {name}
第 {i+1} 名
""" return f"""

各面向排名

{rows}
""" # 定義主要測試功能 # In[58]: # DASS-42 官方計分 dep_nums = [3,5,10,13,16,17,21,24,26,31,34,37,38,42] anx_nums = [2,4,7,9,15,19,20,23,25,28,30,36,40,41] str_nums = [1,6,8,11,12,14,18,22,27,29,32,33,35,39] def qcols(nums): return [f"Q{n}A" for n in nums] def level_dep(score): # DASS-42 Depression if score <= 9: return 0 if score <= 13: return 1 if score <= 20: return 2 if score <= 27: return 3 return 4 def level_anx(score): # DASS-42 Anxiety if score <= 7: return 0 if score <= 9: return 1 if score <= 14: return 2 if score <= 19: return 3 return 4 def level_str(score): # DASS-42 Stress if score <= 14: return 0 if score <= 18: return 1 if score <= 25: return 2 if score <= 33: return 3 return 4 LEVEL_NAME = {0:"正常", 1:"輕度", 2:"中度", 3:"重度", 4:"極重度"} def overall_severity_v2(d_level, a_level, s_level): total = d_level + a_level + s_level if (d_level == 4) or (a_level == 4) or (s_level == 4): return 2 if total >= 9: return 2 if (d_level == 3) or (a_level == 3) or (s_level == 3): return 1 if total <= 5: return 0 else: return 1 OVERALL_NAME = { 0:"低度風險", 1:"中度風險", 2:"高度風險"} OVERALL_COLOR = { 0: "#91cd92", # 綠 1: "#f59e0b", # 橘 2: "#ef4444", # 紅 } def predict_dass42_full(name, gen, age, family, *answers): """ 完整版:42題官方計分(0-3) 回傳:只顯示 Overall(v2)(大字置中)+ 右側排名卡(保留) 並寫入 Google Sheet(DASS42_完整版) """ inputs = [name, gen, age, family] + list(answers) if any(v is None or v == "" for v in inputs): raise gr.Error("⚠️請確保基本資料與 42 題都已填答。") # 建立 Q1A~Q42A row = {f"Q{i+1}A": int(answers[i]) for i in range(42)} # 官方三向度分數 dep_score = sum(row[c] for c in qcols(dep_nums)) anx_score = sum(row[c] for c in qcols(anx_nums)) str_score = sum(row[c] for c in qcols(str_nums)) # 分級 dep_level = level_dep(dep_score) anx_level = level_anx(anx_score) str_level = level_str(str_score) # Overall v2 overall = overall_severity_v2(dep_level, anx_level, str_level) # 寫入 Google Sheet(42 題) # answers42 就是 42 題答案本身 save_to_google_sheets_full42( inputs=[name, gen, age, family], dep_score=dep_score, anx_score=anx_score, str_score=str_score, overall=overall, answers42=answers ) # 顏色 + 顯示文字 color = OVERALL_COLOR.get(overall, "#999999") text = OVERALL_NAME.get(overall, str(overall)) # 左側大字結果(不含下方說明) result_html = f"""
您的預測結果為
{text}
""" # 右邊排名卡 rank_html = make_rank_list(anx_score, dep_score, str_score) return result_html, rank_html # 定義預測功能 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] if any(v is None or v == "" for v in inputs): raise gr.Error("⚠️測驗載入有誤:請確保每一題都已填答或查看填答格式是否正確。") progress = gr.Progress() progress(0, desc="模型計算中...") # age 可能是 int / str,都統一轉 int age = int(age) # family 可能是 int / str / "15人以上" if family == "15人以上": family = 16 else: family = int(family) 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) score = model.predict(input_df)[0] if score == 0: label = "低度風險" color = "#91cd92" elif score == 1: label = "中度風險" color = "#f59e0b" elif score == 2: label = "高度風險" color = "#ef4444" else: label = "計算結果有誤,請重新測試。" color = "#999999" 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 result_score = f"""

您的預測結果為

{label}

""" label_html = make_rank_list(a_score, d_score, s_score) save_to_google_sheets(inputs, a_score, d_score, s_score, t_score, score) progress(1.0, desc="完成") return result_score, label_html # In[59]: # 設定主題色 theme = gr.themes.Default( primary_hue="amber", secondary_hue="amber", ).set( body_background_fill="#fffbeb" ) # In[60]: # 線上主題調色器 # gr.themes.builder() # In[61]: # 介面編排 #按鈕及面板格式設定 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; } """ # UI age_choices = list(range(1, 100)) # 1~99歲 family_choices = list(range(1, 16)) + ["15人以上"] with gr.Blocks() as demo: # Tab1 歷史 history_state = gr.State([]) gr.Markdown("") gr.HTML("""

🌿心理健康風險程度測試📝

""") with gr.Tabs(): # ========================================================= # TAB 1:簡易版(12題|模型) # ========================================================= with gr.TabItem("簡易版 12題"): gr.HTML("""

歡迎來到心理健康風險程度測試環境!
本測驗將透過12題問答,替您在5分鐘內簡單計算出潛在的心理健康風險程度。
請輕鬆填答,無須思慮過度,測驗愉快!

""") with gr.Column(variant="panel", elem_classes="my-custom-panel"): gr.Markdown("## Step 1. 請輸入基本資訊") with gr.Row(): with gr.Column(): name = gr.Textbox(label="暱稱") gen = gr.Dropdown(choices=["男", "女", "其他"], label="性別") with gr.Column(): age = gr.Dropdown( choices=age_choices, label="年齡", value=None ) family = gr.Dropdown( choices=family_choices, label="家庭人數", value=None ) 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") 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="目前尚無測驗紀錄") 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], ) 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("""本測驗結果僅供參考,非屬正規醫療檢驗範疇。 若對於自身狀況有任何疑慮,敬請尋求正規專業醫療協助!♡第四組關心您♡""") # ========================================================= # TAB 2:完整版(42題|官方計分) # ========================================================= with gr.TabItem("完整版 42題"): # Tab2 歷史 full_history_state = gr.State([]) gr.HTML("""

歡迎來到 DASS-42 完整版心理測驗!
本測驗將依 42 題官方計分規則,計算您的心理狀態並輸出風險程度。
請依過去一週的感受作答,測驗愉快!

""") gr.Markdown("## Step 1. 請輸入基本資訊") with gr.Row(): with gr.Column(): full_name = gr.Textbox(label="暱稱") full_gen = gr.Dropdown(choices=["男", "女", "其他"], label="性別") with gr.Column(): full_age = gr.Dropdown( choices=age_choices, label="年齡", value=None ) full_family = gr.Dropdown( choices=family_choices, label="家庭人數", value=None ) gr.Markdown("## Step 2. DASS-42(0~3)") dass42_questions = [ "1. 我發覺自己會因為瑣事而沮喪", "2. 我察覺到自己口乾舌燥", "3. 我似乎無法經驗任何正向的感受", "4. 我經歷過呼吸困難(例如:在沒有耗費體力的情況下仍呼吸過快、呼吸困難)", "5. 我似乎就是提不起勁", "6. 我傾向對情境過度反應", "7. 我有顫抖的感覺(例如:雙腿無力)", "8. 我發覺難以放鬆", "9. 我發覺我處於讓自己頗焦慮的情況,而當它們結束時我感到紓解", "10. 我覺得我沒有什麼可以期待的", "11. 我發覺自己頗容易變得沮喪", "12. 我覺得我相當費心勞神", "13. 我感到傷心且憂鬱", "14. 我發覺自己在任何被延誤的情況中(例如:等電梯、等紅綠燈、一直等待)會變得不耐煩", "15. 我有頭暈無力的感覺", "16. 我覺得我幾乎對任何事情已失去興趣", "17. 我覺得我作為一個人沒有多少價值", "18. 我覺得我頗敏感", "19. 在沒有高溫或耗費體力的情況下,我仍明顯出汗(例如:冒手汗)", "20. 我會無緣無故地感到害怕", "21. 我覺得人生沒有價值", "22. 我發覺難以平靜下來", "23. 我有吞嚥的困難", "24. 我似乎無法從所做的事情中獲得任何樂趣", "25. 在沒有耗費體力的情況下,我仍覺察到我心臟的活動(例如:感覺心率增加、心跳漏拍)", "26. 我感到沮喪而消沉", "27. 我發覺自己煩躁易怒", "28. 我覺得自己快要陷入恐慌", "29. 我發覺當某些事物讓我沮喪後,我難以平靜下來", "30. 我怕自己會被某些瑣碎但不熟悉的任務給難倒", "31. 我無法對任何事物變得熱衷", "32. 我發覺難以容忍自己正在做的事情被打擾", "33. 我處於神經緊繃的狀態", "34. 我覺得我頗沒有價值", "35. 我不能容忍任何阻礙我繼續做事的事物", "36. 我感到懼怕", "37. 我看不到未來可以期盼的事物", "38. 我覺得人生是沒有意義的", "39. 我發覺自己變得激動不安", "40. 我對於可能會讓我恐慌或是出糗的情境感到擔憂", "41. 我有過顫抖的經驗(例如:手部)", "42. 我發覺難以產生動力去做事", ] full_q_widgets = [] for q_text in dass42_questions: full_q_widgets.append( gr.Radio([("從不", 0), ("偶爾", 1), ("經常", 2), ("總是", 3)], label=q_text) ) full_submit = gr.Button("確認送出(計算完整版分數)", elem_id="my_green_btn") with gr.Row(): full_out_html = gr.HTML() full_out_rank = gr.HTML() with gr.Accordion("查看歷史紀錄(完整版)", open=False, elem_id="history_panel"): full_history_display = gr.HTML(value="目前尚無測驗紀錄") full_submit.click( fn=predict_dass42_full, inputs=[full_name, full_gen, full_age, full_family] + full_q_widgets, outputs=[full_out_html, full_out_rank], ).then( fn=update_history, inputs=[full_out_html, full_out_rank, full_history_state], outputs=[full_history_display, full_history_state], ) gr.Markdown("## 免責聲明") gr.Markdown("""本測驗結果僅供參考,非屬正規醫療檢驗範疇。 若對於自身狀況有任何疑慮,敬請尋求正規專業醫療協助!♡第四組關心您♡""") demo.launch(theme=theme, css=custom_css)