bymurphywang commited on
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 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
+ =======================================================================================