Commit ·
cc37807
1
Parent(s): 63eb2a5
Add statistical models: chi-square tests and fatal-severity logistic regression
Browse files- Chi-square independence tests (rain / time period / first-party vehicle
group vs fatal outcome) with Cramer's V effect sizes
- Accident-level logistic regression on fatality: heavy vehicles OR 6.7,
early morning OR 2.7, male OR 2.4, +10km/h speed limit OR 1.3 (all p<0.05);
rain not significant for severity
- Outputs to 分析結果/模型結果/ with rare-event caveat in model summary
- src/taipei_traffic/models.py +154 -0
- 分析結果/模型結果/卡方檢定結果.csv +4 -0
- 分析結果/模型結果/邏輯迴歸_勝算比.csv +14 -0
- 分析結果/模型結果/邏輯迴歸_模型摘要.txt +31 -0
src/taipei_traffic/models.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""統計檢定與事故嚴重度模型。
|
| 2 |
+
|
| 3 |
+
1. 卡方獨立性檢定:雨天/時段/第一當事人車種大類 × 是否為死亡事故
|
| 4 |
+
2. 邏輯迴歸:以事故層級預測「是否為死亡事故」(24小時內或2-30日內有人死亡)
|
| 5 |
+
|
| 6 |
+
死亡事故僅 72 件(佔 0.32%),屬罕見事件:迴歸結果著重方向與相對風險
|
| 7 |
+
(勝算比),不宜直接作為機率預測器使用。
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
import pandas as pd
|
| 14 |
+
import statsmodels.api as sm
|
| 15 |
+
from scipy import stats as scipy_stats
|
| 16 |
+
|
| 17 |
+
from .config import DB_PATH, MODEL_DIR
|
| 18 |
+
from .db import connect
|
| 19 |
+
|
| 20 |
+
_MODEL_QUERY = """
|
| 21 |
+
SELECT a.is_fatal, a.is_rain, a.time_period, a.weekday, a.speed_limit,
|
| 22 |
+
p.age, p.gender_code, p.vehicle_category
|
| 23 |
+
FROM accidents a
|
| 24 |
+
JOIN parties p ON p.accident_id = a.accident_id AND p.party_seq = 1
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
# 死亡事故稀少,車種合併為大類以避免完全分離
|
| 28 |
+
_VEHICLE_GROUPS = {
|
| 29 |
+
"機車": "機車",
|
| 30 |
+
"小客車": "小客車",
|
| 31 |
+
"小貨車": "小貨車",
|
| 32 |
+
"大客車": "大型車",
|
| 33 |
+
"大貨車": "大型車",
|
| 34 |
+
"聯結車": "大型車",
|
| 35 |
+
"曳引車": "大型車",
|
| 36 |
+
"慢車": "慢車",
|
| 37 |
+
"人(行人/乘客)": "其他",
|
| 38 |
+
"其他車": "其他",
|
| 39 |
+
"特種車": "其他",
|
| 40 |
+
"軍車": "其他",
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
_VEHICLE_REF = "小客車"
|
| 44 |
+
_PERIOD_REF = "下午 (12:00-17:59)"
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def load_model_data(db_path: Path = DB_PATH) -> pd.DataFrame:
|
| 48 |
+
with connect(db_path) as conn:
|
| 49 |
+
df = pd.read_sql_query(_MODEL_QUERY, conn)
|
| 50 |
+
df["vehicle_group"] = df["vehicle_category"].map(_VEHICLE_GROUPS)
|
| 51 |
+
df["is_weekend"] = (df["weekday"] >= 5).astype(int)
|
| 52 |
+
return df
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _cramers_v(table: np.ndarray, chi2: float) -> float:
|
| 56 |
+
n = table.sum()
|
| 57 |
+
k = min(table.shape) - 1
|
| 58 |
+
return float(np.sqrt(chi2 / (n * k))) if k > 0 else float("nan")
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def chi_square_tests(df: pd.DataFrame) -> pd.DataFrame:
|
| 62 |
+
"""對是否死亡事故做三組卡方獨立性檢定。"""
|
| 63 |
+
tests = {
|
| 64 |
+
"雨天 × 是否死亡事故": df["is_rain"],
|
| 65 |
+
"時段 × 是否死亡事故": df["time_period"],
|
| 66 |
+
"第一當事人車種大類 × 是否死亡事故": df["vehicle_group"],
|
| 67 |
+
}
|
| 68 |
+
rows = []
|
| 69 |
+
for name, factor in tests.items():
|
| 70 |
+
table = pd.crosstab(factor, df["is_fatal"]).to_numpy()
|
| 71 |
+
chi2, p, dof, _ = scipy_stats.chi2_contingency(table)
|
| 72 |
+
rows.append(
|
| 73 |
+
{
|
| 74 |
+
"檢定": name,
|
| 75 |
+
"卡方值": round(chi2, 3),
|
| 76 |
+
"自由度": dof,
|
| 77 |
+
"p值": round(p, 5),
|
| 78 |
+
"Cramér's V": round(_cramers_v(table, chi2), 4),
|
| 79 |
+
"顯著(α=0.05)": "是" if p < 0.05 else "否",
|
| 80 |
+
}
|
| 81 |
+
)
|
| 82 |
+
return pd.DataFrame(rows)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _design_matrix(df: pd.DataFrame) -> tuple[pd.DataFrame, pd.Series]:
|
| 86 |
+
d = df[
|
| 87 |
+
df["gender_code"].isin([1, 2])
|
| 88 |
+
& df["age"].notna()
|
| 89 |
+
& df["speed_limit"].notna()
|
| 90 |
+
& df["vehicle_group"].notna()
|
| 91 |
+
].copy()
|
| 92 |
+
|
| 93 |
+
X = pd.DataFrame(index=d.index)
|
| 94 |
+
X["雨天"] = d["is_rain"]
|
| 95 |
+
X["週末"] = d["is_weekend"]
|
| 96 |
+
X["年齡(每+10歲)"] = d["age"] / 10
|
| 97 |
+
X["男性"] = (d["gender_code"] == 1).astype(int)
|
| 98 |
+
X["速限(每+10km/h)"] = d["speed_limit"] / 10
|
| 99 |
+
|
| 100 |
+
period_dummies = pd.get_dummies(d["time_period"], prefix="時段").astype(int)
|
| 101 |
+
period_dummies = period_dummies.drop(columns=f"時段_{_PERIOD_REF}")
|
| 102 |
+
X = pd.concat([X, period_dummies], axis=1)
|
| 103 |
+
|
| 104 |
+
vehicle_dummies = pd.get_dummies(d["vehicle_group"], prefix="車種").astype(int)
|
| 105 |
+
vehicle_dummies = vehicle_dummies.drop(columns=f"車種_{_VEHICLE_REF}")
|
| 106 |
+
X = pd.concat([X, vehicle_dummies], axis=1)
|
| 107 |
+
|
| 108 |
+
X = sm.add_constant(X)
|
| 109 |
+
return X, d["is_fatal"]
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def severity_logit(df: pd.DataFrame):
|
| 113 |
+
"""配適死亡事故邏輯迴歸,回傳 (fit結果, 勝算比表)。"""
|
| 114 |
+
X, y = _design_matrix(df)
|
| 115 |
+
fit = sm.Logit(y, X.astype(float)).fit(disp=False)
|
| 116 |
+
|
| 117 |
+
conf = fit.conf_int()
|
| 118 |
+
odds = pd.DataFrame(
|
| 119 |
+
{
|
| 120 |
+
"變項": fit.params.index,
|
| 121 |
+
"係數": fit.params.round(4).values,
|
| 122 |
+
"勝算比(OR)": np.exp(fit.params).round(3).values,
|
| 123 |
+
"OR 95%CI下界": np.exp(conf[0]).round(3).values,
|
| 124 |
+
"OR 95%CI上界": np.exp(conf[1]).round(3).values,
|
| 125 |
+
"p值": fit.pvalues.round(5).values,
|
| 126 |
+
}
|
| 127 |
+
)
|
| 128 |
+
odds = odds[odds["變項"] != "const"].reset_index(drop=True)
|
| 129 |
+
return fit, odds
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def run_all(db_path: Path = DB_PATH, out_dir: Path = MODEL_DIR) -> None:
|
| 133 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 134 |
+
df = load_model_data(db_path)
|
| 135 |
+
|
| 136 |
+
chi = chi_square_tests(df)
|
| 137 |
+
chi.to_csv(out_dir / "卡方檢定結果.csv", index=False, encoding="utf-8-sig")
|
| 138 |
+
print("✓ 卡方檢定結果.csv")
|
| 139 |
+
print(chi.to_string(index=False))
|
| 140 |
+
|
| 141 |
+
fit, odds = severity_logit(df)
|
| 142 |
+
odds.to_csv(out_dir / "邏輯迴歸_勝算比.csv", index=False, encoding="utf-8-sig")
|
| 143 |
+
print("\n✓ 邏輯迴歸_勝算比.csv")
|
| 144 |
+
print(odds.to_string(index=False))
|
| 145 |
+
|
| 146 |
+
summary_path = out_dir / "邏��迴歸_模型摘要.txt"
|
| 147 |
+
n_events = int(df["is_fatal"].sum())
|
| 148 |
+
caveat = (
|
| 149 |
+
"註:死亡事故為罕見事件({} 件 / {} 件,約 {:.2%}),本模型用於解讀風險\n"
|
| 150 |
+
"因子方向與相對強度(勝算比),非機率預測器。參考組別:時段=下午、\n"
|
| 151 |
+
"車種=小客車。\n\n"
|
| 152 |
+
).format(n_events, len(df), n_events / len(df))
|
| 153 |
+
summary_path.write_text(caveat + str(fit.summary()), encoding="utf-8")
|
| 154 |
+
print(f"✓ {summary_path.name}(McFadden pseudo R² = {fit.prsquared:.3f})")
|
分析結果/模型結果/卡方檢定結果.csv
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
檢定,卡方值,自由度,p值,Cramér's V,顯著(α=0.05)
|
| 2 |
+
雨天 × 是否死亡事故,0.615,1,0.43284,0.0052,否
|
| 3 |
+
時段 × 是否死亡事故,9.194,3,0.02682,0.0203,是
|
| 4 |
+
第一當事人車種大類 × 是否死亡事故,71.658,5,0.0,0.0567,是
|
分析結果/模型結果/邏輯迴歸_勝算比.csv
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
變項,係數,勝算比(OR),OR 95%CI下界,OR 95%CI上界,p值
|
| 2 |
+
雨天,-0.327,0.721,0.344,1.513,0.38723
|
| 3 |
+
週末,0.0099,1.01,0.575,1.775,0.97268
|
| 4 |
+
年齡(每+10歲),0.1913,1.211,1.047,1.401,0.01006
|
| 5 |
+
男性,0.8547,2.351,1.096,5.041,0.02812
|
| 6 |
+
速限(每+10km/h),0.2888,1.335,1.01,1.763,0.0421
|
| 7 |
+
時段_上午 (06:00-11:59),0.3189,1.376,0.794,2.384,0.25571
|
| 8 |
+
時段_夜晚 (18:00-23:59),-0.1838,0.832,0.409,1.694,0.61225
|
| 9 |
+
時段_清晨 (00:00-05:59),0.9892,2.689,1.13,6.396,0.02527
|
| 10 |
+
車種_其他,1.8253,6.205,2.067,18.627,0.00114
|
| 11 |
+
車種_大型車,1.8983,6.674,3.192,13.957,0.0
|
| 12 |
+
車種_小貨車,1.1825,3.263,1.434,7.42,0.00479
|
| 13 |
+
車種_慢車,0.3315,1.393,0.321,6.049,0.65812
|
| 14 |
+
車種_機車,0.1141,1.121,0.622,2.02,0.70397
|
分析結果/模型結果/邏輯迴歸_模型摘要.txt
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
註:死亡事故為罕見事件(72 件 / 22368 件,約 0.32%),本模型用於解讀風險
|
| 2 |
+
因子方向與相對強度(勝算比),非機率預測器。參考組別:時段=下午、
|
| 3 |
+
車種=小客車。
|
| 4 |
+
|
| 5 |
+
Logit Regression Results
|
| 6 |
+
==============================================================================
|
| 7 |
+
Dep. Variable: is_fatal No. Observations: 22071
|
| 8 |
+
Model: Logit Df Residuals: 22057
|
| 9 |
+
Method: MLE Df Model: 13
|
| 10 |
+
Date: Sat, 11 Jul 2026 Pseudo R-squ.: 0.06586
|
| 11 |
+
Time: 20:44:03 Log-Likelihood: -452.22
|
| 12 |
+
converged: True LL-Null: -484.11
|
| 13 |
+
Covariance Type: nonrobust LLR p-value: 1.103e-08
|
| 14 |
+
=======================================================================================
|
| 15 |
+
coef std err z P>|z| [0.025 0.975]
|
| 16 |
+
---------------------------------------------------------------------------------------
|
| 17 |
+
const -9.0942 0.902 -10.085 0.000 -10.862 -7.327
|
| 18 |
+
雨天 -0.3270 0.378 -0.865 0.387 -1.068 0.414
|
| 19 |
+
週末 0.0099 0.288 0.034 0.973 -0.554 0.574
|
| 20 |
+
年齡(每+10歲) 0.1913 0.074 2.574 0.010 0.046 0.337
|
| 21 |
+
男性 0.8547 0.389 2.196 0.028 0.092 1.618
|
| 22 |
+
速限(每+10km/h) 0.2888 0.142 2.033 0.042 0.010 0.567
|
| 23 |
+
時段_上午 (06:00-11:59) 0.3189 0.281 1.137 0.256 -0.231 0.869
|
| 24 |
+
時段_夜晚 (18:00-23:59) -0.1838 0.363 -0.507 0.612 -0.895 0.527
|
| 25 |
+
時段_清晨 (00:00-05:59) 0.9892 0.442 2.237 0.025 0.123 1.856
|
| 26 |
+
車種_其他 1.8253 0.561 3.254 0.001 0.726 2.925
|
| 27 |
+
車種_大型車 1.8983 0.376 5.044 0.000 1.161 2.636
|
| 28 |
+
車種_小貨車 1.1825 0.419 2.821 0.005 0.361 2.004
|
| 29 |
+
車種_慢車 0.3315 0.749 0.443 0.658 -1.137 1.800
|
| 30 |
+
車種_機車 0.1141 0.300 0.380 0.704 -0.475 0.703
|
| 31 |
+
=======================================================================================
|