Spaces:
Sleeping
資料完整性檢查與可重複性技術設定
Browse files【資料完整性檢查】
差異性檢定(t 檢定/ANOVA)在任一組樣本數 N<2 時,scipy 會算出 p=NaN,
但原邏輯的 `NaN < 0.05` 比較恆為 False,會 fallthrough 到「ns」,讓
「資料不足、根本無法檢定」跟「已檢定、無顯著差異」變成同一個值,讀者與
LLM 都無法分辨。「個體間比較」功能允許分組粒度細到每檔案一組,N=1 是
實際會發生的情境,不是假設案例。
- 新增 INSUFFICIENT_DATA_LABEL 常數,p=NaN 時明確標成獨立類別,不計算
效果量(不用 cohens_d()/eta_squared() 對 N<2 的 0.0 保底值),不計入
顯著/不顯著的統計比例
- 同步修正 sig_df 篩選、docx 報告表格(紅字改灰字斜體、None 值防呆)、
送給 LLM 的 JSON(新增 untestable_indicators/資料完整性提醒欄位)、
畫面即時說明文字,四處都要排除新標籤,避免被誤算進「顯著指標」
- system prompt 補規則:看到「資料完整性提醒」的指標只能寫「資料不足,
無法檢定」,不能寫成「無顯著差異」
【可重複性技術設定】
- temperature 預設從 0.5 改成 0
- LLM1_MODEL/LLM2_MODEL 從環境變數可覆寫改成寫死常數,執行期無法覆寫
(對應 docs/開發優化建議.md 3.3/3.4 節記錄過的真實案例:測試環境曾
被覆寫成 openrouter/free 自動路由別名,同代號兩次呼叫導向不同模型)
- 新增 PROMPT_VERSION 常數,隨 system prompt 異動手動遞增,印在 docx/
txt/畫面三處報告輸出的標頭,讓每份報告可回溯當時用的模型與提示詞版本
- .env.example/使用手冊.md/.docx 同步移除已失效的 LLM_MODEL 覆寫說明
以上均以隔離測試腳本驗證(N=1/N=3/混合案例、LLM 資料層輸出、docx 實際
產出內容),未觸及既有正常流程之行為。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- .env.example +3 -3
- app_agent.py +123 -22
- 使用手冊.docx +2 -2
- 使用手冊.md +4 -2
|
@@ -2,6 +2,6 @@
|
|
| 2 |
LLM1_API_KEY=
|
| 3 |
LLM2_API_KEY=
|
| 4 |
|
| 5 |
-
#
|
| 6 |
-
#
|
| 7 |
-
# LLM2_MODEL
|
|
|
|
| 2 |
LLM1_API_KEY=
|
| 3 |
LLM2_API_KEY=
|
| 4 |
|
| 5 |
+
# 2026-08-22:模型名稱不再支援用環境變數覆寫(可重複性設計,避免研究結果
|
| 6 |
+
# 隨執行環境的模型覆寫設定而變動)。要換模型請直接改
|
| 7 |
+
# app_agent.py 的 LLM1_MODEL/LLM2_MODEL 常數。
|
|
@@ -127,14 +127,45 @@ def start_session_cleanup_scheduler() -> None:
|
|
| 127 |
threading.Thread(target=_loop, daemon=True, name="session-cleanup").start()
|
| 128 |
|
| 129 |
# ── LLM 設定 ────────────────────────────────────────────────────────────────
|
|
|
|
| 130 |
LLM1_API_KEY = os.environ.get("LLM1_API_KEY", "")
|
| 131 |
-
LLM1_MODEL = os.environ.get("LLM1_MODEL", "google/gemma-4-31b-it")
|
| 132 |
-
|
| 133 |
LLM2_API_KEY = os.environ.get("LLM2_API_KEY", "")
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
|
| 136 |
ALPHA = 0.05
|
| 137 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
# ── POS 集合(與 Prog02 相同)───────────────────────────────────────────────
|
| 139 |
Vset = ['VC','VH','VE','VJ','VA','V2','VK','VL','VD','VCL','VHC','VB','VF','VI','VAC','VG']
|
| 140 |
Nset = ['Na','Nb','Nc','Ncd','Nd','Nf','Nh','Nv']
|
|
@@ -397,6 +428,10 @@ SYSTEM_PROMPT_BASE = """# 角色定義
|
|
| 397 |
(M=32.5),1989 年組別最低(M=18.2)。」
|
| 398 |
區分:達統計顯著的差異(標明 p 值、效果量、最高與最低組別及平均數)、未達顯著但數值差距
|
| 399 |
較大的趨勢(同樣要指出方向)、無明顯差異的指標。
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
2. 監督式學習(隨機森林):說明 feature_importance_top20 裡重要性最高的 3-5 項指標與其重要性數值。
|
| 401 |
同時陳述「隨機森林分類準確率」的數字,並原樣附上 JSON 裡「隨機森林準確率之樣本外預測意義」
|
| 402 |
這欄給的完整句子(那句話已經是可以直接使用的中文敘述,不必再改寫或省略)。
|
|
@@ -476,8 +511,13 @@ _load_resources()
|
|
| 476 |
# SECTION 3: LLM API Wrappers
|
| 477 |
# ============================================================================
|
| 478 |
|
| 479 |
-
def _call_openrouter(api_key, model, system_prompt, user_prompt, max_tokens=16000, temperature=0
|
| 480 |
-
"""通用 OpenRouter API 呼叫(同步)。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 481 |
url = "https://openrouter.ai/api/v1/chat/completions"
|
| 482 |
headers = {
|
| 483 |
"Authorization": f"Bearer {api_key}",
|
|
@@ -1043,25 +1083,35 @@ def run_statistical_analysis(csv_path: Path, output_dir: Path, group_labels: dic
|
|
| 1043 |
row[f"{g}_SD"] = round(float(np.std(gdata[gi], ddof=1)), 4) if len(gdata[gi])>1 else 0.0
|
| 1044 |
if n_groups == 2:
|
| 1045 |
stat, p = stats.ttest_ind(gdata[0], gdata[1], equal_var=False)
|
| 1046 |
-
es = cohens_d(gdata[0], gdata[1])
|
| 1047 |
es_name = "Cohen's d"; method = "t-test"
|
| 1048 |
else:
|
| 1049 |
stat, p = stats.f_oneway(*gdata)
|
| 1050 |
-
es = eta_squared(gdata)
|
| 1051 |
es_name = "η²"; method = "ANOVA"
|
| 1052 |
row["方法"] = method
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1053 |
row["統計量"] = round(float(stat), 4) if not np.isnan(stat) else None
|
| 1054 |
-
row["p值"] = round(float(p),
|
| 1055 |
-
row["顯著"] = ("***" if p is not None and p<0.001 else
|
| 1056 |
-
"**" if p is not None and p<0.01 else
|
| 1057 |
-
"*" if p is not None and p<0.05 else "ns")
|
| 1058 |
row["效果量名稱"] = es_name
|
| 1059 |
-
|
| 1060 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1061 |
diff_results.append(row)
|
| 1062 |
|
| 1063 |
diff_df = pd.DataFrame(diff_results)
|
| 1064 |
-
sig_df = diff_df[diff_df['顯著']
|
|
|
|
| 1065 |
diff_path = output_dir / "02_difference_analysis.csv"
|
| 1066 |
diff_df.to_csv(str(diff_path), index=False, encoding='utf-8-sig')
|
| 1067 |
|
|
@@ -1291,7 +1341,9 @@ def run_statistical_analysis(csv_path: Path, output_dir: Path, group_labels: dic
|
|
| 1291 |
# 修過的 Type1.1/rf_accuracy_is_meaningful 是同一種「技術變數名稱外
|
| 1292 |
# 洩漏」問題,改成不點名欄位、只講語意。
|
| 1293 |
doc.add_paragraph(f'分組依據:語料分組(共 {n_groups} 組)。採用 {method_name},顯著水準 α = {ALPHA}。')
|
| 1294 |
-
|
|
|
|
|
|
|
| 1295 |
# 2026-08-15(業主要求):表格欄位裡的 N/M/SD 是統計慣用縮寫,第一次
|
| 1296 |
# 看到報告的人不一定認得,加一句說明放在表格正上方。
|
| 1297 |
note_p = doc.add_paragraph('表格中 N=樣本數、M=平均數、SD=標準差。')
|
|
@@ -1318,7 +1370,10 @@ def run_statistical_analysis(csv_path: Path, output_dir: Path, group_labels: dic
|
|
| 1318 |
vals.extend([str(row['方法']),
|
| 1319 |
f"{row['統計量']:.4f}" if row['統計量'] is not None else "-",
|
| 1320 |
f"{row['p值']:.4f}" if row['p值'] is not None else "-",
|
| 1321 |
-
str(row['顯著']),
|
|
|
|
|
|
|
|
|
|
| 1322 |
for j, val in enumerate(vals):
|
| 1323 |
cell = diff_tbl.rows[i].cells[j]
|
| 1324 |
cell.text = val
|
|
@@ -1326,8 +1381,15 @@ def run_statistical_analysis(csv_path: Path, output_dir: Path, group_labels: dic
|
|
| 1326 |
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 1327 |
for r in p.runs:
|
| 1328 |
r.font.size = Pt(7)
|
| 1329 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1330 |
r.font.color.rgb = RGBColor(0xCC, 0x00, 0x00)
|
|
|
|
|
|
|
|
|
|
| 1331 |
doc.add_paragraph('')
|
| 1332 |
if box_path and box_path.exists():
|
| 1333 |
doc.add_heading('顯著指標箱形圖', level=2)
|
|
@@ -1373,6 +1435,7 @@ def run_statistical_analysis(csv_path: Path, output_dir: Path, group_labels: dic
|
|
| 1373 |
"cluster_csv_path": cluster_csv_path,
|
| 1374 |
"report_path": report_path,
|
| 1375 |
"sig_count": len(sig_df),
|
|
|
|
| 1376 |
"total_indicators": len(diff_df),
|
| 1377 |
"accuracy": accuracy,
|
| 1378 |
"do_eval": do_eval,
|
|
@@ -1446,10 +1509,29 @@ def load_analysis_data_for_llm(csv_path: Path, output_dir: Path, ml_summary: dic
|
|
| 1446 |
else:
|
| 1447 |
data[key] = d.to_dict(orient='records')
|
| 1448 |
if key == "difference_analysis":
|
| 1449 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1450 |
data["significant_indicators"] = sig.to_dict(orient='records')
|
| 1451 |
data["significant_count"] = len(sig)
|
| 1452 |
data["total_indicators"] = len(d)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1453 |
else:
|
| 1454 |
data[key] = []
|
| 1455 |
if "significant_count" not in data:
|
|
@@ -1535,7 +1617,12 @@ def run_report_writing(scenario_key: str, user_metadata: str, user_focus: str,
|
|
| 1535 |
# 標題後標註使用的 LLM 模型(藍色)
|
| 1536 |
model_p = doc.add_paragraph()
|
| 1537 |
model_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 1538 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1539 |
mr.font.size = Pt(10)
|
| 1540 |
mr.font.color.rgb = RGBColor(0x0E, 0x73, 0xB8)
|
| 1541 |
doc.add_paragraph(f'研究情���:{scenario_info["title"]}',
|
|
@@ -1561,6 +1648,10 @@ def run_report_writing(scenario_key: str, user_metadata: str, user_focus: str,
|
|
| 1561 |
with open(str(txt_path), 'w', encoding='utf-8') as f:
|
| 1562 |
f.write(f"研究情境:{scenario_info['title']}\n")
|
| 1563 |
f.write(f"時間:{now_tw().strftime('%Y-%m-%d %H:%M:%S')}(台灣時間)\n")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1564 |
# 2026-07-27:原本用 "="*60 當純文字分隔線,但緊接在上兩行文字正下方,
|
| 1565 |
# 符合 Markdown Setext 標題語法(文字正上方接一整行 = 會被解讀成 h1
|
| 1566 |
# 底線),導致這兩行 metadata 被渲染成巨大無樣式標題。改成前面留空行
|
|
@@ -3619,10 +3710,19 @@ def create_app():
|
|
| 3619 |
|
| 3620 |
if diff_path.exists():
|
| 3621 |
diff = pd.read_csv(str(diff_path))
|
| 3622 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3623 |
# sig=0 時不會產生箱形圖(見下方 box_path 判斷),若只留白使用者
|
| 3624 |
# 容易誤以為系統壞掉,這裡改在標題文字附加空狀態說明
|
| 3625 |
empty_note = "" if len(sig) > 0 else "\n\n本次分析未發現統計顯著的指標差異,因此未產生箱形圖。"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3626 |
# 2026-08-14(業主要求):標題句前面加一句「以[檢定方法]比較
|
| 3627 |
# [分組]文本」,方法與分組名稱都從 diff 本身反推——分組代碼
|
| 3628 |
# 是「{組別}_N」這組欄名的前綴(run_statistical_analysis 產出
|
|
@@ -3649,7 +3749,7 @@ def create_app():
|
|
| 3649 |
# 的做法一致。
|
| 3650 |
R[6] = gr.update(
|
| 3651 |
value=(f"### 差異性分析\n{prefix}共 **{len(diff)}** 個指標,"
|
| 3652 |
-
f"其中 **{len(sig)}** 個達顯著差異(p < {ALPHA})。{empty_note}\n\n"
|
| 3653 |
f"下方表格中 N=樣本數、M=平均數、SD=標準差。"),
|
| 3654 |
visible=True,
|
| 3655 |
)
|
|
@@ -3722,7 +3822,8 @@ def create_app():
|
|
| 3722 |
with open(str(txt_files[0]), 'r', encoding='utf-8') as f:
|
| 3723 |
rpt_text = f.read()
|
| 3724 |
model_note = (f"<span style='color:var(--c-accent-strong);font-weight:600'>"
|
| 3725 |
-
f"本報告由主 Agent(LLM1)模型 {LLM1_MODEL} 生成
|
|
|
|
| 3726 |
R[18] = f"### LLM 輔助分析 {model_note}\n\n{rpt_text}"
|
| 3727 |
except Exception:
|
| 3728 |
R[18] = "### LLM 輔助分析\n\n(讀取報告時發生錯誤)"
|
|
|
|
| 127 |
threading.Thread(target=_loop, daemon=True, name="session-cleanup").start()
|
| 128 |
|
| 129 |
# ── LLM 設定 ────────────────────────────────────────────────────────────────
|
| 130 |
+
# API 金鑰維持環境變數(本來就該是每個部署環境各自設定的機密資訊,不進版控)。
|
| 131 |
LLM1_API_KEY = os.environ.get("LLM1_API_KEY", "")
|
|
|
|
|
|
|
| 132 |
LLM2_API_KEY = os.environ.get("LLM2_API_KEY", "")
|
| 133 |
+
|
| 134 |
+
# 2026-08-22(業主要求:為研究結果的「可重複性」鎖定模型版本):模型代號
|
| 135 |
+
# 原本是 `os.environ.get("LLM1_MODEL", "google/gemma-4-31b-it")`——可以被
|
| 136 |
+
# 環境變數在執行期任意覆寫,且不會留下任何紀錄。docs/開發優化建議.md 第
|
| 137 |
+
# 3.3/3.4 節就記錄過一次真實案例:測試環境曾被覆寫成 `openrouter/free`
|
| 138 |
+
# 這種自動路由別名,同一個代號兩次呼叫被導去兩個不同的實際模型,耗時與
|
| 139 |
+
# 輸出品質都不穩定——這正是「模型版本沒鎖定」會實際發生的風險,不是假設。
|
| 140 |
+
# 改成固定常數:只能透過修改這裡的程式碼(進而走 git commit 留下紀錄)
|
| 141 |
+
# 才能更換模型,執行期無法覆寫,「這次分析用的是哪個模型」才會是一個
|
| 142 |
+
# 可以回頭稽核、不會被悄悄改掉的事實。
|
| 143 |
+
LLM1_MODEL = "google/gemma-4-31b-it"
|
| 144 |
+
LLM2_MODEL = "google/gemma-4-26b-a4b-it"
|
| 145 |
+
|
| 146 |
+
# 2026-08-22(同上,可重複性設計之二:提示詞版本控制):SYSTEM_PROMPT_BASE
|
| 147 |
+
# 與各 SCENARIO_MODULE_* 本身已經隨這個檔案進 git 版控,但報告產出當下
|
| 148 |
+
# 沒有留下任何「這份報告是哪一版提示詞寫的」的紀錄——事後拿到一份舊報告,
|
| 149 |
+
# 沒辦法回推當時的提示詞內容跟現在是否一致。這裡加一個顯式版本號,每次
|
| 150 |
+
# 異動 SYSTEM_PROMPT_BASE 或任一 SCENARIO_MODULE_* 的內容時手動遞增,並且
|
| 151 |
+
# 直接印在每份產出的報告上(見 run_report_writing()),讓版本資訊跟著
|
| 152 |
+
# 報告本身走,不用回頭 git blame 才查得到。
|
| 153 |
+
PROMPT_VERSION = "v1"
|
| 154 |
|
| 155 |
ALPHA = 0.05
|
| 156 |
|
| 157 |
+
# 2026-08-22(業主提出「數據完整性檢查」的必要性,追查後確認是真實存在的
|
| 158 |
+
# 問題):差異性檢定(t 檢定/ANOVA)在任一組樣本數 N<2 時,scipy 會算出
|
| 159 |
+
# p=NaN(沒有組內變異,這個檢定工具本來就不適用),但 `NaN < 0.05` 這類比較
|
| 160 |
+
# 在 Python 裡恆為 False,若不特別攔截,會一路 fallthrough 到「無顯著差異」
|
| 161 |
+
# 的判斷分支,跟「有算出結果、但真的沒有顯著差異」變成同一個值,讀者與 LLM
|
| 162 |
+
# 都無法分辨「資料不足、根本沒辦法檢定」跟「檢定過、沒有差異」這兩種本質
|
| 163 |
+
# 不同的情況。「個體間比較」功能(見 _extract_individual_grouping)允許
|
| 164 |
+
# 把分組粒度細到每個檔案自己一組,N=1 per group 是實際會發生的情境,不是
|
| 165 |
+
# 假設的邊角案例。這裡定義一個獨立標籤,任何用到「顯著」欄位的地方都要用
|
| 166 |
+
# 這個常數判斷,不要漏掉、也不要各自硬寫字串。
|
| 167 |
+
INSUFFICIENT_DATA_LABEL = "資料不足"
|
| 168 |
+
|
| 169 |
# ── POS 集合(與 Prog02 相同)───────────────────────────────────────────────
|
| 170 |
Vset = ['VC','VH','VE','VJ','VA','V2','VK','VL','VD','VCL','VHC','VB','VF','VI','VAC','VG']
|
| 171 |
Nset = ['Na','Nb','Nc','Ncd','Nd','Nf','Nh','Nv']
|
|
|
|
| 428 |
(M=32.5),1989 年組別最低(M=18.2)。」
|
| 429 |
區分:達統計顯著的差異(標明 p 值、效果量、最高與最低組別及平均數)、未達顯著但數值差距
|
| 430 |
較大的趨勢(同樣要指出方向)、無明顯差異的指標。
|
| 431 |
+
【資料完整性】若 JSON 附有「資料完整性提醒」欄位(表示部分指標因組別樣本數不足、根本無法
|
| 432 |
+
進行統計檢定),這些指標一律只能陳述為「資料不足,無法檢定」,絕對不可以寫成「無顯著差異」,
|
| 433 |
+
也不可以把它們計入「達統計顯著」或「無明顯差異」的統計比例——這是兩種完全不同的結論,混為
|
| 434 |
+
一談會誤導讀者以為系統已經檢定過、確認沒有差異。
|
| 435 |
2. 監督式學習(隨機森林):說明 feature_importance_top20 裡重要性最高的 3-5 項指標與其重要性數值。
|
| 436 |
同時陳述「隨機森林分類準確率」的數字,並原樣附上 JSON 裡「隨機森林準確率之樣本外預測意義」
|
| 437 |
這欄給的完整句子(那句話已經是可以直接使用的中文敘述,不必再改寫或省略)。
|
|
|
|
| 511 |
# SECTION 3: LLM API Wrappers
|
| 512 |
# ============================================================================
|
| 513 |
|
| 514 |
+
def _call_openrouter(api_key, model, system_prompt, user_prompt, max_tokens=16000, temperature=0):
|
| 515 |
+
"""通用 OpenRouter API 呼叫(同步)。
|
| 516 |
+
2026-08-22(可重複性設計):temperature 預設從 0.5 改成 0,降低同一組輸入
|
| 517 |
+
在不同次呼叫間的用字差異。注意這不代表輸出保證逐字相同——OpenRouter
|
| 518 |
+
後端本身的浮點運算、批次處理等因素仍可能造成極小差異,temperature=0
|
| 519 |
+
只是把「取樣造成的隨機性」這個變因降到最低,不是完全消除所有變因。
|
| 520 |
+
"""
|
| 521 |
url = "https://openrouter.ai/api/v1/chat/completions"
|
| 522 |
headers = {
|
| 523 |
"Authorization": f"Bearer {api_key}",
|
|
|
|
| 1083 |
row[f"{g}_SD"] = round(float(np.std(gdata[gi], ddof=1)), 4) if len(gdata[gi])>1 else 0.0
|
| 1084 |
if n_groups == 2:
|
| 1085 |
stat, p = stats.ttest_ind(gdata[0], gdata[1], equal_var=False)
|
|
|
|
| 1086 |
es_name = "Cohen's d"; method = "t-test"
|
| 1087 |
else:
|
| 1088 |
stat, p = stats.f_oneway(*gdata)
|
|
|
|
| 1089 |
es_name = "η²"; method = "ANOVA"
|
| 1090 |
row["方法"] = method
|
| 1091 |
+
# 見上方 INSUFFICIENT_DATA_LABEL 定義處的說明:p=NaN 代表這個指標在
|
| 1092 |
+
# 目前的分組下根本無法檢定(通常是某組 N<2),不是「檢定過、沒有
|
| 1093 |
+
# 顯著差異」。這裡明確分流,不落入下面的顯著性判斷,效果量也不計算
|
| 1094 |
+
# ——cohens_d()/eta_squared() 各自對 N<2 有 0.0 的保底值,那是給
|
| 1095 |
+
# 「算得出來、真的沒差異」用的,跟「根本沒算出來」不該共用同一個
|
| 1096 |
+
# 0.0,會誤導成「效果量趨近於零」。
|
| 1097 |
+
is_untestable = bool(np.isnan(p))
|
| 1098 |
row["統計量"] = round(float(stat), 4) if not np.isnan(stat) else None
|
| 1099 |
+
row["p值"] = round(float(p), 4) if not is_untestable else None
|
|
|
|
|
|
|
|
|
|
| 1100 |
row["效果量名稱"] = es_name
|
| 1101 |
+
if is_untestable:
|
| 1102 |
+
row["顯著"] = INSUFFICIENT_DATA_LABEL
|
| 1103 |
+
row["效果量"] = None
|
| 1104 |
+
row["效果大小"] = None
|
| 1105 |
+
else:
|
| 1106 |
+
es = cohens_d(gdata[0], gdata[1]) if n_groups == 2 else eta_squared(gdata)
|
| 1107 |
+
row["顯著"] = ("***" if p<0.001 else "**" if p<0.01 else "*" if p<0.05 else "ns")
|
| 1108 |
+
row["效果量"] = round(float(es), 4)
|
| 1109 |
+
row["效果大小"] = effect_label(es, method)
|
| 1110 |
diff_results.append(row)
|
| 1111 |
|
| 1112 |
diff_df = pd.DataFrame(diff_results)
|
| 1113 |
+
sig_df = diff_df[~diff_df['顯著'].isin(["ns", INSUFFICIENT_DATA_LABEL])]
|
| 1114 |
+
untestable_df = diff_df[diff_df['顯著'] == INSUFFICIENT_DATA_LABEL]
|
| 1115 |
diff_path = output_dir / "02_difference_analysis.csv"
|
| 1116 |
diff_df.to_csv(str(diff_path), index=False, encoding='utf-8-sig')
|
| 1117 |
|
|
|
|
| 1341 |
# 修過的 Type1.1/rf_accuracy_is_meaningful 是同一種「技術變數名稱外
|
| 1342 |
# 洩漏」問題,改成不點名欄位、只講語意。
|
| 1343 |
doc.add_paragraph(f'分組依據:語料分組(共 {n_groups} 組)。採用 {method_name},顯著水準 α = {ALPHA}。')
|
| 1344 |
+
_untestable_note = (f"另有 {len(untestable_df)} 個指標因組別樣本數不足(N<2),"
|
| 1345 |
+
f"無法進行統計檢定,不計入上述比例。" if len(untestable_df) > 0 else "")
|
| 1346 |
+
doc.add_paragraph(f'共 {len(diff_df)} 個指標,其中 {len(sig_df)} 個達顯著差異(p < {ALPHA})。{_untestable_note}')
|
| 1347 |
# 2026-08-15(業主要求):表格欄位裡的 N/M/SD 是統計慣用縮寫,第一次
|
| 1348 |
# 看到報告的人不一定認得,加一句說明放在表格正上方。
|
| 1349 |
note_p = doc.add_paragraph('表格中 N=樣本數、M=平均數、SD=標準差。')
|
|
|
|
| 1370 |
vals.extend([str(row['方法']),
|
| 1371 |
f"{row['統計量']:.4f}" if row['統計量'] is not None else "-",
|
| 1372 |
f"{row['p值']:.4f}" if row['p值'] is not None else "-",
|
| 1373 |
+
str(row['顯著']),
|
| 1374 |
+
f"{row['效果量']:.4f}" if row['效果量'] is not None else "-",
|
| 1375 |
+
str(row['效果大小']) if row['效果大小'] is not None else "-"])
|
| 1376 |
+
_is_untestable_row = row['顯著'] == INSUFFICIENT_DATA_LABEL
|
| 1377 |
for j, val in enumerate(vals):
|
| 1378 |
cell = diff_tbl.rows[i].cells[j]
|
| 1379 |
cell.text = val
|
|
|
|
| 1381 |
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 1382 |
for r in p.runs:
|
| 1383 |
r.font.size = Pt(7)
|
| 1384 |
+
# 顯著(紅字)跟資料不足(灰字)是兩種不同性質的標記,
|
| 1385 |
+
# 不能共用紅字——紅字在這份報告的視覺語言裡代表「這是
|
| 1386 |
+
# 值得注意的發現」,「資料不足」剛好相反,是「這項不能
|
| 1387 |
+
# 拿來當發現看」,用紅字反而會讓人誤以為它也是顯著結果。
|
| 1388 |
+
if row['顯著'] not in ("ns", INSUFFICIENT_DATA_LABEL) and j == len(vals)-4:
|
| 1389 |
r.font.color.rgb = RGBColor(0xCC, 0x00, 0x00)
|
| 1390 |
+
elif _is_untestable_row:
|
| 1391 |
+
r.font.color.rgb = RGBColor(0x88, 0x88, 0x88)
|
| 1392 |
+
r.italic = True
|
| 1393 |
doc.add_paragraph('')
|
| 1394 |
if box_path and box_path.exists():
|
| 1395 |
doc.add_heading('顯著指標箱形圖', level=2)
|
|
|
|
| 1435 |
"cluster_csv_path": cluster_csv_path,
|
| 1436 |
"report_path": report_path,
|
| 1437 |
"sig_count": len(sig_df),
|
| 1438 |
+
"untestable_count": len(untestable_df),
|
| 1439 |
"total_indicators": len(diff_df),
|
| 1440 |
"accuracy": accuracy,
|
| 1441 |
"do_eval": do_eval,
|
|
|
|
| 1509 |
else:
|
| 1510 |
data[key] = d.to_dict(orient='records')
|
| 1511 |
if key == "difference_analysis":
|
| 1512 |
+
if '顯著' in d.columns:
|
| 1513 |
+
sig = d[~d['顯著'].isin(['ns', INSUFFICIENT_DATA_LABEL])]
|
| 1514 |
+
untestable = d[d['顯著'] == INSUFFICIENT_DATA_LABEL]
|
| 1515 |
+
else:
|
| 1516 |
+
sig = pd.DataFrame(); untestable = pd.DataFrame()
|
| 1517 |
data["significant_indicators"] = sig.to_dict(orient='records')
|
| 1518 |
data["significant_count"] = len(sig)
|
| 1519 |
data["total_indicators"] = len(d)
|
| 1520 |
+
# 2026-08-22:跟資料層預先中文化同一個原則(見
|
| 1521 |
+
# rf_accuracy_is_meaningful/cluster Type1 的前例)——與其在
|
| 1522 |
+
# system prompt 裡要求 LLM「自己判斷哪些指標資料不足」,
|
| 1523 |
+
# 這裡直接把清單跟現成的中文說明準備好,LLM 只要照抄,不用
|
| 1524 |
+
# 自己從一堆 JSON 欄位裡推斷「顯著=資料不足」代表什麼。
|
| 1525 |
+
data["untestable_indicators"] = untestable.to_dict(orient='records')
|
| 1526 |
+
data["untestable_count"] = len(untestable)
|
| 1527 |
+
data["資料完整性提醒"] = (
|
| 1528 |
+
f"以上 difference_analysis 中有 {len(untestable)} 項指標的「顯著」欄位"
|
| 1529 |
+
f"標示為「{INSUFFICIENT_DATA_LABEL}」,代表該指標在某一組別的樣本數少於 2"
|
| 1530 |
+
"(無法計算組內變異),統計檢定本身就無法執行,p 值與效果量均為 null。"
|
| 1531 |
+
"這些指標必須明確陳述為「資料不足,無法檢定」,絕對不可以寫成「無顯著差異」"
|
| 1532 |
+
"或納入「達統計顯著」/「無明顯差異」的分類統計,兩者是完全不同的結論。"
|
| 1533 |
+
if len(untestable) > 0 else ""
|
| 1534 |
+
)
|
| 1535 |
else:
|
| 1536 |
data[key] = []
|
| 1537 |
if "significant_count" not in data:
|
|
|
|
| 1617 |
# 標題後標註使用的 LLM 模型(藍色)
|
| 1618 |
model_p = doc.add_paragraph()
|
| 1619 |
model_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 1620 |
+
# 2026-08-22(可重複性設計):模型名稱後面補上提示詞版本號,讓每份報告
|
| 1621 |
+
# 都留下「當時是哪一版 system prompt 產出的」這個事後可稽核的紀錄,不用
|
| 1622 |
+
# 回頭 git blame 才查得到——PROMPT_VERSION 定義處、修改時機見該常數註解。
|
| 1623 |
+
mr = model_p.add_run(
|
| 1624 |
+
f'本報告由主 Agent(LLM1)模型 {LLM1_MODEL} 生成(提示詞版本 {PROMPT_VERSION})'
|
| 1625 |
+
)
|
| 1626 |
mr.font.size = Pt(10)
|
| 1627 |
mr.font.color.rgb = RGBColor(0x0E, 0x73, 0xB8)
|
| 1628 |
doc.add_paragraph(f'研究情���:{scenario_info["title"]}',
|
|
|
|
| 1648 |
with open(str(txt_path), 'w', encoding='utf-8') as f:
|
| 1649 |
f.write(f"研究情境:{scenario_info['title']}\n")
|
| 1650 |
f.write(f"時間:{now_tw().strftime('%Y-%m-%d %H:%M:%S')}(台灣時間)\n")
|
| 1651 |
+
# 2026-08-22(可重複性設計):docx 版標題下方本來就有模型名稱標註,
|
| 1652 |
+
# txt 版之前完全沒寫,這裡補上、順便帶提示詞版本號,兩種格式的
|
| 1653 |
+
# 稽核資訊要一致,不能只有 docx 有。
|
| 1654 |
+
f.write(f"模型:主 Agent(LLM1){LLM1_MODEL}(提示詞版本 {PROMPT_VERSION})\n")
|
| 1655 |
# 2026-07-27:原本用 "="*60 當純文字分隔線,但緊接在上兩行文字正下方,
|
| 1656 |
# 符合 Markdown Setext 標題語法(文字正上方接一整行 = 會被解讀成 h1
|
| 1657 |
# 底線),導致這兩行 metadata 被渲染成巨大無樣式標題。改成前面留空行
|
|
|
|
| 3710 |
|
| 3711 |
if diff_path.exists():
|
| 3712 |
diff = pd.read_csv(str(diff_path))
|
| 3713 |
+
if '顯著' in diff.columns:
|
| 3714 |
+
sig = diff[~diff['顯著'].isin(['ns', INSUFFICIENT_DATA_LABEL])]
|
| 3715 |
+
untestable_n = int((diff['顯著'] == INSUFFICIENT_DATA_LABEL).sum())
|
| 3716 |
+
else:
|
| 3717 |
+
sig = pd.DataFrame(); untestable_n = 0
|
| 3718 |
# sig=0 時不會產生箱形圖(見下方 box_path 判斷),若只留白使用者
|
| 3719 |
# 容易誤以為系統壞掉,這裡改在標題文字附加空狀態說明
|
| 3720 |
empty_note = "" if len(sig) > 0 else "\n\n本次分析未發現統計顯著的指標差異,因此未產生箱形圖。"
|
| 3721 |
+
untestable_note = (
|
| 3722 |
+
f"\n\n⚠️ 另有 **{untestable_n}** 個指標因組別樣本數不足(N<2),"
|
| 3723 |
+
"無法進行統計檢定,不計入上述比例。"
|
| 3724 |
+
if untestable_n > 0 else ""
|
| 3725 |
+
)
|
| 3726 |
# 2026-08-14(業主要求):標題句前面加一句「以[檢定方法]比較
|
| 3727 |
# [分組]文本」,方法與分組名稱都從 diff 本身反推——分組代碼
|
| 3728 |
# 是「{組別}_N」這組欄名的前綴(run_statistical_analysis 產出
|
|
|
|
| 3749 |
# 的做法一致。
|
| 3750 |
R[6] = gr.update(
|
| 3751 |
value=(f"### 差異性分析\n{prefix}共 **{len(diff)}** 個指標,"
|
| 3752 |
+
f"其中 **{len(sig)}** 個達顯著差異(p < {ALPHA})。{empty_note}{untestable_note}\n\n"
|
| 3753 |
f"下方表格中 N=樣本數、M=平均數、SD=標準差。"),
|
| 3754 |
visible=True,
|
| 3755 |
)
|
|
|
|
| 3822 |
with open(str(txt_files[0]), 'r', encoding='utf-8') as f:
|
| 3823 |
rpt_text = f.read()
|
| 3824 |
model_note = (f"<span style='color:var(--c-accent-strong);font-weight:600'>"
|
| 3825 |
+
f"本報告由主 Agent(LLM1)模型 {LLM1_MODEL} 生成"
|
| 3826 |
+
f"(提示詞版本 {PROMPT_VERSION})</span>")
|
| 3827 |
R[18] = f"### LLM 輔助分析 {model_note}\n\n{rpt_text}"
|
| 3828 |
except Exception:
|
| 3829 |
R[18] = "### LLM 輔助分析\n\n(讀取報告時發生錯誤)"
|
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a25202eddd2838b2eda7cb6dbdee97f6b7997484b74249f3e8ffda944bfee197
|
| 3 |
+
size 44651
|
|
@@ -47,11 +47,13 @@ LLM 透過 OpenRouter 呼叫,於專案目錄的 `.env` 檔設定以下變數
|
|
| 47 |
|
| 48 |
```
|
| 49 |
LLM1_API_KEY 你的 OpenRouter 金鑰(主 Agent)
|
| 50 |
-
LLM1_MODEL google/gemma-4-31b-it(預設,可不填)
|
| 51 |
LLM2_API_KEY 你的 OpenRouter 金鑰(工作程序 Agent)
|
| 52 |
-
LLM2_MODEL google/gemma-4-26b-a4b-it(預設,可不填)
|
| 53 |
```
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
### 啟動系統
|
| 56 |
|
| 57 |
於專案目錄執行:
|
|
|
|
| 47 |
|
| 48 |
```
|
| 49 |
LLM1_API_KEY 你的 OpenRouter 金鑰(主 Agent)
|
|
|
|
| 50 |
LLM2_API_KEY 你的 OpenRouter 金鑰(工作程序 Agent)
|
|
|
|
| 51 |
```
|
| 52 |
|
| 53 |
+
> 模型名稱(主 Agent:`google/gemma-4-31b-it`;工作程序 Agent:`google/gemma-4-26b-a4b-it`)
|
| 54 |
+
> 為可重複性設計,已固定寫在程式碼裡,不再支援用環境變數覆寫;要換模型需直接修改
|
| 55 |
+
> `app_agent.py` 的 `LLM1_MODEL`/`LLM2_MODEL` 常數。
|
| 56 |
+
|
| 57 |
### 啟動系統
|
| 58 |
|
| 59 |
於專案目錄執行:
|