diff --git a/bot_listener.py b/bot_listener.py index 61b3431a..16206db7 100644 --- a/bot_listener.py +++ b/bot_listener.py @@ -161,6 +161,48 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None): if hist_mae > sigma: sigma = hist_mae + # === Shock Score: 气象突变软评分 (0~1) === + # 用近 4 条 METAR 的风向/云量/气压变化评估环境稳定性 + # 越高 = 越不稳定 = σ 放宽 + shock_score = 0.0 + recent_obs = metar.get("recent_obs", []) + if len(recent_obs) >= 2: + oldest = recent_obs[-1] # 最早 + newest = recent_obs[0] # 最新 + + # ① 风向变化项 (0~0.4) + # 角度差 × 风速放大系数(风速 > 10kt 时权重高,弱风忽略噪声) + wdir_old = oldest.get("wdir") + wdir_new = newest.get("wdir") + wspd_new = newest.get("wspd") or 0 + if wdir_old is not None and wdir_new is not None: + angle_diff = abs(wdir_new - wdir_old) + if angle_diff > 180: angle_diff = 360 - angle_diff + wind_weight = min(wspd_new / 15.0, 1.0) # 15kt 以上满权 + wind_shock = min(angle_diff / 90.0, 1.0) * wind_weight * 0.4 + shock_score += wind_shock + + # ② 云量阶跃项 (0~0.35) + # CLR=0, FEW=1, SCT=2, BKN=3, OVC=4 + cloud_old = oldest.get("cloud_rank", 0) + cloud_new = newest.get("cloud_rank", 0) + cloud_jump = abs(cloud_new - cloud_old) + cloud_shock = min(cloud_jump / 3.0, 1.0) * 0.35 + shock_score += cloud_shock + + # ③ 气压变化项 (0~0.25) + # 2h 内气压变化超过 2hPa 视为异常 + altim_old = oldest.get("altim") + altim_new = newest.get("altim") + if altim_old is not None and altim_new is not None: + press_diff = abs(altim_new - altim_old) + press_shock = min(press_diff / 4.0, 1.0) * 0.25 + shock_score += press_shock + + # 应用 shock_score: 放宽 σ + if shock_score > 0.05: + sigma *= (1 + 0.5 * shock_score) + # 时间修正:根据当前时间距峰值的位置调整 σ # 峰值前:σ 不变(不确定性最大) # 峰值窗口内:σ 缩小 30%(正在定型) diff --git a/src/data_collection/weather_sources.py b/src/data_collection/weather_sources.py index d4cf52f1..27ed3920 100644 --- a/src/data_collection/weather_sources.py +++ b/src/data_collection/weather_sources.py @@ -309,15 +309,31 @@ class WeatherDataCollector: except: continue - # 3. 提取最近 4 条报文的温度(用于趋势分析) + # 3. 提取最近 4 条报文的多维数据(温度 + 风/云/压强,用于趋势和 shock_score) recent_temps_raw = [] # [(local_time_str, temp_c), ...] + recent_obs_raw = [] # [{time, temp, wdir, wspd, clouds, altim}, ...] for obs in data[:4]: # data 已按时间倒序 obs_temp = obs.get("temp") - if obs_temp is not None: - obs_dt_iter = _parse_rawob_time(obs) - if obs_dt_iter: - local_rt = obs_dt_iter + timedelta(seconds=utc_offset) - recent_temps_raw.append((local_rt.strftime("%H:%M"), obs_temp)) + obs_dt_iter = _parse_rawob_time(obs) + if obs_temp is not None and obs_dt_iter: + local_rt = obs_dt_iter + timedelta(seconds=utc_offset) + recent_temps_raw.append((local_rt.strftime("%H:%M"), obs_temp)) + # 云量码映射: CLR=0, FEW=1, SCT=2, BKN=3, OVC=4 + cloud_rank_map = {"CLR": 0, "SKC": 0, "FEW": 1, "SCT": 2, "BKN": 3, "OVC": 4} + clouds = obs.get("clouds", []) + max_cloud_rank = 0 + for c in clouds: + rank = cloud_rank_map.get(c.get("cover", ""), 0) + if rank > max_cloud_rank: + max_cloud_rank = rank + recent_obs_raw.append({ + "time": local_rt.strftime("%H:%M"), + "temp": obs_temp, + "wdir": obs.get("wdir"), + "wspd": obs.get("wspd"), + "cloud_rank": max_cloud_rank, # 0~4 + "altim": obs.get("altim"), + }) # 转换为单位 if use_fahrenheit: @@ -354,6 +370,7 @@ class WeatherDataCollector: "clouds": latest.get("clouds", []), }, "recent_temps": recent_temps, # 最近4条: [("15:00", 5), ("14:20", 5), ...] + "recent_obs": recent_obs_raw, # 最近4条多维数据(风/云/压强) "unit": unit, }