| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444 |
- #!/usr/bin/env python3
- """
- 气候季节起始日分析脚本
- 依据:《气候季节划分指标》标准(GB/T 41538-2022)
- - 常年法:基于历史多年平均气温,计算常年春/夏/秋/冬起始日
- - 当年法:基于当年气温数据,结合常年参考进行初判与二次判断
- """
- import csv
- import json
- from collections import defaultdict
- from datetime import datetime
- from typing import Optional
- # ---------------------------------------------------------------------------
- # 工具函数
- # ---------------------------------------------------------------------------
- def month_day_to_doy(month: int, day: int) -> int:
- """月份+日期 -> 一年中的第几天(1-based,按非闰年算)"""
- days_in_month = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
- return sum(days_in_month[:month]) + day
- def doy_to_date(doy: int, year: int = 2023) -> str:
- """日序 -> YYYY-MM-DD"""
- days_in_month = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
- m = 1
- while doy > days_in_month[m]:
- doy -= days_in_month[m]
- m += 1
- return f"{year}-{m:02d}-{doy:02d}"
- def day_diff(date1: str, date2: str) -> int:
- """两个 YYYY-MM-DD 字符串的天数差(date2 - date1)"""
- d1 = datetime.strptime(date1, "%Y-%m-%d")
- d2 = datetime.strptime(date2, "%Y-%m-%d")
- return (d2 - d1).days
- # ---------------------------------------------------------------------------
- # 1. 数据加载
- # ---------------------------------------------------------------------------
- def load_temperature_data(csv_path: str) -> dict[str, dict[str, float]]:
- """
- 读取 CSV,过滤掉 mean_temp 为空的行。
- 返回 {city: {date_str: mean_temp}}
- """
- data: dict[str, dict[str, float]] = defaultdict(dict)
- with open(csv_path, encoding="utf-8-sig") as f:
- reader = csv.reader(f)
- next(reader) # skip header
- for row in reader:
- if len(row) < 3 or not row[2].strip():
- continue
- data[row[1]][row[0]] = float(row[2])
- return dict(data)
- # ---------------------------------------------------------------------------
- # 2. 常年日平均气温序列(公式1)
- # ---------------------------------------------------------------------------
- def compute_clim_daily_avg(
- city_data: dict[str, float],
- years: list[str],
- ) -> list[float]:
- """
- 对选定年份逐日(日序 1~365)求平均,得常年日平均气温序列。
- 返回列表索引 j 对应日序 j+1(0-based)。
- """
- totals = [0.0] * 366
- counts = [0] * 366
- for year in years:
- yd = {k: v for k, v in city_data.items() if k.startswith(year)}
- for date_str, temp in yd.items():
- m = int(date_str[5:7])
- d = int(date_str[8:10])
- doy = month_day_to_doy(m, d)
- if doy == 60: # skip leap Feb 29
- continue
- totals[doy] += temp
- counts[doy] += 1
- return [
- round(totals[j] / counts[j], 2) if counts[j] > 0 else None
- for j in range(1, 366)
- ]
- # ---------------------------------------------------------------------------
- # 3. 5日滑动平均(公式2,首尾相接)
- # ---------------------------------------------------------------------------
- def moving_avg_5d(series: list[float]) -> list[float]:
- """
- 计算5日滑动平均序列。
- 边界处用相邻年份数据补齐(首尾相接),保留2位小数。
- 返回列表与输入等长,索引 j 对应日序 j+1。
- """
- n = len(series)
- # 前补2个、后补4个(需要4个来覆盖最后两天的右边界窗口)
- padded = series[-2:] + series + series[:4]
- avg = []
- for i in range(n):
- vals = [padded[i + 2], padded[i + 3], padded[i + 4],
- padded[i + 5], padded[i + 6]]
- non_none = [v for v in vals if v is not None]
- if non_none:
- avg.append(round(sum(non_none) / len(non_none), 2))
- else:
- avg.append(None)
- return avg
- # ---------------------------------------------------------------------------
- # 4. 找到首次连续W天满足条件的起始索引(0-based)
- # ---------------------------------------------------------------------------
- def find_season_start(
- avg_list: list[float],
- threshold: float,
- direction: str,
- search_start_doy: int = 1,
- ) -> Optional[int]:
- """
- 在序列中找到首次连续5天满足条件的起始日序。
- direction='above': >= threshold(春/夏)
- direction='below': < threshold (秋/冬)
- search_start_doy: 从该日序开始搜索(避免跨年边界误判),默认从1开始。
- 返回日序(1-based)或 None。
- """
- streak = 0
- for j, v in enumerate(avg_list):
- doy = j + 1 # 1-based day of year
- if doy < search_start_doy:
- streak = 0
- continue
- if v is None:
- streak = 0
- continue
- if direction == "above" and v >= threshold:
- streak += 1
- elif direction == "below" and v < threshold:
- streak += 1
- else:
- streak = 0
- if streak == 5:
- return doy - 4 # convert to 1-based day-of-year of first day
- return None
- # ---------------------------------------------------------------------------
- # 5. 常年气候季节分析
- # ---------------------------------------------------------------------------
- def analyze_climate_season(
- city_data: dict[str, float],
- hist_years: list[str],
- ) -> dict:
- daily_avg = compute_clim_daily_avg(city_data, hist_years)
- avg_5d = moving_avg_5d(daily_avg)
- # 秋季搜索从夏季起始日后开始,冬季搜索从秋季起始日后开始
- autumn_start = (climate_ref_summer_doy or 1) + 1 if False else None
- # 由于我们还没有计算 climate_ref,用保守估计:
- # 秋季从夏季日后30天开始搜索(roughly June+30 = July),冬季从秋季日后30天开始
- summer_doy_val = find_season_start(avg_5d, 22.0, "above")
- autumn_doy_val = find_season_start(avg_5d, 22.0, "below", search_start_doy=(summer_doy_val or 1) + 30)
- winter_doy_val = find_season_start(avg_5d, 10.0, "below", search_start_doy=(autumn_doy_val or 1) + 30)
- return {
- "daily_avg": daily_avg,
- "avg_5d": avg_5d,
- "spring_doy": find_season_start(avg_5d, 10.0, "above"),
- "summer_doy": summer_doy_val,
- "autumn_doy": autumn_doy_val,
- "winter_doy": winter_doy_val,
- }
- # ---------------------------------------------------------------------------
- # 6. 当年季节起始日分析(含初判 & 二次判断)
- # ---------------------------------------------------------------------------
- def compute_current_year_avg_5d(
- city_data: dict[str, float],
- target_year: str,
- ) -> tuple[list[str], list[float]]:
- """
- 计算目标年每日5日滑动平均。
- 返回 (sorted_dates, avg_5d_list),avg_5d_list 与 sorted_dates 一一对应。
- """
- yd = {k: v for k, v in city_data.items() if k.startswith(target_year)}
- sorted_dates = sorted(yd.keys())
- temps = [yd[d] for d in sorted_dates]
- n = len(temps)
- # 首尾相接补齐
- padded = temps[-2:] + temps + temps[:4]
- avg_5d = []
- for i in range(n):
- vals = [padded[i + 2], padded[i + 3], padded[i + 4],
- padded[i + 5], padded[i + 6]]
- non_none = [v for v in vals if v is not None]
- if non_none:
- avg_5d.append(round(sum(non_none) / len(non_none), 2))
- else:
- avg_5d.append(None)
- return sorted_dates, avg_5d
- def find_all_season_starts(
- avg_list: list[float],
- threshold: float,
- direction: str,
- ) -> list[int]:
- """
- 找出所有首次连续5日满足条件的起始日期(0-based 索引)。
- 用于收集初判及所有二次/多次候选。
- """
- candidates = []
- streak = 0
- for j, v in enumerate(avg_list):
- if v is None:
- streak = 0
- continue
- if direction == "above" and v >= threshold:
- streak += 1
- elif direction == "below" and v < threshold:
- streak += 1
- else:
- streak = 0
- if streak == 5:
- candidates.append(j - 4) # 0-based index of first day
- streak = 0 # 不重叠,继续搜索
- return candidates
- def analyze_current_year(
- city_data: dict[str, float],
- target_year: str,
- climate_info: dict,
- ) -> dict:
- sorted_dates, avg_5d_curr = compute_current_year_avg_5d(city_data, target_year)
- n = len(sorted_dates)
- spring_candidates = find_all_season_starts(avg_5d_curr, 10.0, "above")
- summer_candidates = find_all_season_starts(avg_5d_curr, 22.0, "above")
- # 初判
- initial_spring = sorted_dates[spring_candidates[0]] if spring_candidates else None
- initial_summer = sorted_dates[summer_candidates[0]] if summer_candidates else None
- # 二次/多次候选(初判之后的所有候选)
- secondary_spring = [sorted_dates[i] for i in spring_candidates[1:]]
- secondary_summer = [sorted_dates[i] for i in summer_candidates[1:]]
- # 与常年对比
- ref_spring_doy = climate_info["spring_doy"]
- ref_summer_doy = climate_info["summer_doy"]
- spring_diff_days: Optional[int] = None
- summer_diff_days: Optional[int] = None
- if initial_spring and ref_spring_doy:
- ref_date = doy_to_date(ref_spring_doy, int(target_year))
- spring_diff_days = day_diff(ref_date, initial_spring)
- if initial_summer and ref_summer_doy:
- ref_date = doy_to_date(ref_summer_doy, int(target_year))
- summer_diff_days = day_diff(ref_date, initial_summer)
- # 最终判定(按 6.1.2 规则)
- final_spring = initial_spring
- final_summer = initial_summer
- spring_note = ""
- summer_note = ""
- if initial_spring and ref_spring_doy:
- if spring_diff_days > 30:
- if secondary_spring:
- final_spring = secondary_spring[0]
- spring_note = "(二次判断)"
- else:
- spring_note = "(无二次候选,保留初判)"
- if initial_summer and ref_summer_doy:
- if summer_diff_days > 30:
- if secondary_summer:
- final_summer = secondary_summer[0]
- summer_note = "(二次判断)"
- else:
- summer_note = "(无二次候选,保留初判)"
- # 数据完整性提示
- warnings = []
- last_date = sorted_dates[-1] if sorted_dates else ""
- if initial_spring is None and last_date <= f"{target_year}-08-31":
- warnings.append("数据截至8月底,春季尚未完成判定")
- if initial_summer is None and last_date <= f"{target_year}-08-31":
- warnings.append("数据截至8月底,夏季尚未完成判定")
- return {
- "target_year": target_year,
- "initial_spring": initial_spring,
- "initial_summer": initial_summer,
- "secondary_spring": secondary_spring,
- "secondary_summer": secondary_summer,
- "final_spring": final_spring,
- "final_summer": final_summer,
- "spring_diff_days": spring_diff_days,
- "summer_diff_days": summer_diff_days,
- "spring_note": spring_note,
- "summer_note": summer_note,
- "warnings": warnings,
- }
- # ---------------------------------------------------------------------------
- # 7. 输出
- # ---------------------------------------------------------------------------
- def print_results(all_cities: dict, hist_years: list[str], target_year: str):
- print("=" * 70)
- print(f" 气候季节起始日分析 | 历史年: {', '.join(hist_years)} | 目标年: {target_year}")
- print("=" * 70)
- for city in sorted(all_cities):
- city_data = all_cities[city]
- # 常年
- climate = analyze_climate_season(city_data, hist_years)
- c_spring = doy_to_date(climate["spring_doy"], 2023) if climate["spring_doy"] else "--"
- c_summer = doy_to_date(climate["summer_doy"], 2023) if climate["summer_doy"] else "--"
- c_autumn = doy_to_date(climate["autumn_doy"], 2023) if climate["autumn_doy"] else "--"
- c_winter = doy_to_date(climate["winter_doy"], 2023) if climate["winter_doy"] else "--"
- # 当年
- current = analyze_current_year(city_data, target_year, climate)
- print(f"\n【{city}】")
- print(f" 常年参考({','.join(hist_years)} 平均):")
- print(f" 春季起始日: {c_spring} 夏季起始日: {c_summer}")
- print(f" 秋季起始日: {c_autumn} 冬季起始日: {c_winter}")
- init_s = current["initial_spring"] or "--"
- init_sm = current["initial_summer"] or "--"
- # 初判信息
- diff_s_str = ""
- if current["spring_diff_days"] is not None:
- if current["spring_diff_days"] < 0:
- diff_s_str = f" (比常年早{abs(current['spring_diff_days'])}天,触发二次判断)"
- elif current["spring_diff_days"] <= 30:
- diff_s_str = f" (比常年晚{current['spring_diff_days']}天,初判有效)"
- else:
- diff_s_str = f" (比常年晚{current['spring_diff_days']}天,需进一步判断)"
- else:
- diff_s_str = " (无常年参考)"
- diff_sm_str = ""
- if current["summer_diff_days"] is not None:
- if current["summer_diff_days"] < 0:
- diff_sm_str = f" (比常年早{abs(current['summer_diff_days'])}天,触发二次判断)"
- elif current["summer_diff_days"] <= 30:
- diff_sm_str = f" (比常年晚{current['summer_diff_days']}天,初判有效)"
- else:
- diff_sm_str = f" (比常年晚{current['summer_diff_days']}天,需进一步判断)"
- else:
- diff_sm_str = " (无常年参考)"
- print(f" 当年法判定({target_year}):")
- print(f" 初判春季起始日: {init_s}{diff_s_str}")
- sec_spring_list = current["secondary_spring"]
- if sec_spring_list:
- shown = ', '.join(sec_spring_list[:5])
- suffix = f' 等{len(sec_spring_list)}个' if len(sec_spring_list) > 5 else ''
- print(f" 二次候选春季: {shown}{suffix}")
- else:
- print(f" 二次候选春季: (无)")
- print(f" 初判夏季起始日: {init_sm}{diff_sm_str}")
- sec_summer_list = current["secondary_summer"]
- if sec_summer_list:
- shown = ', '.join(sec_summer_list[:5])
- suffix = f' 等{len(sec_summer_list)}个' if len(sec_summer_list) > 5 else ''
- print(f" 二次候选夏季: {shown}{suffix}")
- else:
- print(f" 二次候选夏季: (无)")
- print(f" >> 最终判定 春季起始日: 【{current['final_spring'] or '--'}】{current['spring_note']}")
- print(f" >> 最终判定 夏季起始日: 【{current['final_summer'] or '--'}】{current['summer_note']}")
- for w in current["warnings"]:
- print(f" 提示: {w}")
- print("\n" + "=" * 70)
- # ---------------------------------------------------------------------------
- # 8. 导出常年参考(JSON)
- # ---------------------------------------------------------------------------
- def export_climate_reference(
- all_cities: dict,
- hist_years: list[str],
- out_path: str,
- ):
- result = {}
- for city, city_data in all_cities.items():
- climate = analyze_climate_season(city_data, hist_years)
- result[city] = {
- "spring_doy": climate["spring_doy"],
- "summer_doy": climate["summer_doy"],
- "autumn_doy": climate["autumn_doy"],
- "winter_doy": climate["winter_doy"],
- }
- with open(out_path, "w", encoding="utf-8") as f:
- json.dump(result, f, ensure_ascii=False, indent=2)
- print(f"常年参考已导出至: {out_path}")
- # ---------------------------------------------------------------------------
- # 主程序
- # ---------------------------------------------------------------------------
- if __name__ == "__main__":
- CSV_PATH = "/home/proc/new_mage/china_city_temperature_2020_2026.csv"
- HIST_YEARS = ["2020", "2021", "2022", "2023", "2024", "2025"]
- TARGET_YEAR = "2026"
- CLIMATE_OUT = "/home/proc/new_mage/climate_reference.json"
- print("正在加载数据...")
- all_cities = load_temperature_data(CSV_PATH)
- print(f"已加载 {len(all_cities)} 个城市\n")
- for c in sorted(all_cities):
- print(f" {c}: {len(all_cities[c])} 条记录")
- export_climate_reference(all_cities, HIST_YEARS, CLIMATE_OUT)
- print()
- print_results(all_cities, HIST_YEARS, TARGET_YEAR)
- print("\n完成。")
|