cd_prediction_service_v3.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732
  1. """
  2. Cd预测服务类 v3.0
  3. @description: 完全自包含的服务,不依赖Cd_Prediction_Integrated_System
  4. @author: AcidMap Team
  5. @version: 3.0.0
  6. """
  7. import os
  8. import logging
  9. import asyncio
  10. import tempfile
  11. import json
  12. from datetime import datetime
  13. from typing import Dict, Any, Optional, List
  14. import pandas as pd
  15. import glob
  16. from ..config.cd_prediction_config import cd_config
  17. from ..models.cd_prediction import CdPredictionEngine
  18. from ..database import SessionLocal
  19. from .admin_boundary_service import get_boundary_geojson_by_name
  20. class CdPredictionServiceV3:
  21. """
  22. Cd预测服务类 v3.0 - 完全自包含版本
  23. @description: 使用自包含的预测模型,完全不依赖外部集成系统
  24. """
  25. def __init__(self):
  26. """
  27. 初始化Cd预测服务
  28. """
  29. # 设置日志
  30. self.logger = logging.getLogger(__name__)
  31. # 获取配置
  32. self.config = cd_config
  33. # 设置输出目录(使用基础输出目录)
  34. self.output_base_dir = self.config.get_output_dir("base")
  35. # 初始化预测引擎
  36. self.engine = CdPredictionEngine(self.output_base_dir)
  37. # 输出子目录
  38. self.output_figures_dir = os.path.join(self.output_base_dir, "figures")
  39. self.output_raster_dir = os.path.join(self.output_base_dir, "raster")
  40. self.output_data_dir = os.path.join(self.output_base_dir, "data")
  41. # 支持的县市配置
  42. self.supported_counties = self._load_supported_counties()
  43. self.logger.info("Cd预测服务v3.0初始化完成(完全自包含)")
  44. def _load_supported_counties(self) -> Dict[str, Dict]:
  45. """
  46. 加载支持的县市配置
  47. @returns {Dict[str, Dict]} 支持的县市配置信息
  48. """
  49. return {
  50. "乐昌市": {
  51. "region_code": "440282",
  52. "display_name": "乐昌市",
  53. "province": "广东省"
  54. }
  55. # 可以继续添加更多县市
  56. }
  57. def is_county_supported(self, county_name: str) -> bool:
  58. """
  59. 检查县市是否被支持
  60. @param {str} county_name - 县市名称
  61. @returns {bool} 是否支持该县市
  62. """
  63. return county_name in self.supported_counties
  64. def get_supported_counties(self) -> List[str]:
  65. """
  66. 获取支持的县市名称列表
  67. @returns {List[str]} 支持的县市名称列表
  68. """
  69. return list(self.supported_counties.keys())
  70. def get_supported_counties_info(self) -> List[Dict[str, Any]]:
  71. """
  72. 获取支持的县市详细信息
  73. @returns {List[Dict[str, Any]]} 支持的县市详细信息列表
  74. """
  75. counties_info = []
  76. for county_name, config in self.supported_counties.items():
  77. counties_info.append({
  78. "name": county_name,
  79. "display_name": config.get("display_name", county_name),
  80. "province": config.get("province", ""),
  81. "region_code": config.get("region_code", ""),
  82. "supported": True # v3.0版本默认都支持
  83. })
  84. return counties_info
  85. def validate_input_data(self, df: pd.DataFrame, county_name: str) -> Dict[str, Any]:
  86. """
  87. 验证输入数据格式
  88. @param {pd.DataFrame} df - 输入数据
  89. @param {str} county_name - 县市名称
  90. @returns {Dict[str, Any]} 验证结果
  91. """
  92. # 基本要求:前两列为坐标,至少需要3列数据
  93. if df.shape[1] < 3:
  94. return {
  95. "valid": False,
  96. "errors": ["数据至少需要3列:前两列为经纬度,后续列为环境因子"],
  97. "warnings": [],
  98. "data_shape": df.shape,
  99. "county_supported": self.is_county_supported(county_name)
  100. }
  101. # 坐标列验证
  102. coordinate_issues = []
  103. try:
  104. # 检查前两列是否为数值型
  105. if not pd.api.types.is_numeric_dtype(df.iloc[:, 0]):
  106. coordinate_issues.append("第一列(经度)不是数值型数据")
  107. elif not df.iloc[:, 0].between(70, 140).all():
  108. coordinate_issues.append("经度值超出合理范围(70-140度)")
  109. if not pd.api.types.is_numeric_dtype(df.iloc[:, 1]):
  110. coordinate_issues.append("第二列(纬度)不是数值型数据")
  111. elif not df.iloc[:, 1].between(15, 55).all():
  112. coordinate_issues.append("纬度值超出合理范围(15-55度)")
  113. except Exception as e:
  114. coordinate_issues.append(f"坐标数据验证失败: {str(e)}")
  115. # 检查数据完整性
  116. null_counts = df.isnull().sum()
  117. high_null_columns = [col for col, count in null_counts.items()
  118. if count > len(df) * 0.1]
  119. # 检查环境因子列是否为数值型
  120. non_numeric_columns = []
  121. for i in range(2, df.shape[1]):
  122. col_name = df.columns[i]
  123. if not pd.api.types.is_numeric_dtype(df.iloc[:, i]):
  124. non_numeric_columns.append(col_name)
  125. warnings = []
  126. if high_null_columns:
  127. warnings.append(f"以下列空值较多: {', '.join(high_null_columns)}")
  128. if coordinate_issues:
  129. warnings.extend(coordinate_issues)
  130. if non_numeric_columns:
  131. warnings.append(f"以下列不是数值型: {', '.join(non_numeric_columns)}")
  132. # 如果有严重的坐标问题,标记为无效
  133. critical_errors = []
  134. if any("不是数值型数据" in issue for issue in coordinate_issues):
  135. critical_errors.extend([issue for issue in coordinate_issues if "不是数值型数据" in issue])
  136. return {
  137. "valid": len(critical_errors) == 0,
  138. "errors": critical_errors,
  139. "warnings": warnings,
  140. "data_shape": df.shape,
  141. "county_supported": self.is_county_supported(county_name)
  142. }
  143. def save_temp_data(self, df: pd.DataFrame, county_name: str) -> str:
  144. """
  145. 保存临时数据文件
  146. @param {pd.DataFrame} df - 数据
  147. @param {str} county_name - 县市名称
  148. @returns {str} 临时文件路径
  149. """
  150. timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
  151. filename = f"{county_name}_temp_data_{timestamp}.csv"
  152. file_path = os.path.join(self.output_data_dir, "temp", filename)
  153. # 确保目录存在
  154. os.makedirs(os.path.dirname(file_path), exist_ok=True)
  155. # 保存数据
  156. df.to_csv(file_path, index=False, encoding='utf-8-sig')
  157. file_size = os.path.getsize(file_path) if os.path.exists(file_path) else 0
  158. self.logger.info(f"临时数据文件已保存: {file_path} ({file_size:,} bytes)")
  159. # 清理旧的临时文件
  160. self._cleanup_temp_files(county_name)
  161. return file_path
  162. def _get_boundary_geojson(self, county_name: str) -> Optional[str]:
  163. """
  164. 获取县市边界的GeoJSON文件
  165. @param {str} county_name - 县市名称
  166. @returns {Optional[str]} GeoJSON文件路径,如果失败则返回None
  167. """
  168. try:
  169. db = SessionLocal()
  170. feature = get_boundary_geojson_by_name(db, county_name, level="auto")
  171. fc = {"type": "FeatureCollection", "features": [feature]}
  172. # 创建临时GeoJSON文件
  173. tmp_dir = tempfile.mkdtemp()
  174. tmp_geojson = os.path.join(tmp_dir, "boundary.geojson")
  175. with open(tmp_geojson, 'w', encoding='utf-8') as f:
  176. json.dump(fc, f, ensure_ascii=False)
  177. return tmp_geojson
  178. except Exception as e:
  179. self.logger.warning(f"从数据库获取边界失败: {str(e)}")
  180. return None
  181. finally:
  182. try:
  183. db.close()
  184. except Exception:
  185. pass
  186. async def generate_crop_cd_prediction_for_county(
  187. self,
  188. county_name: str,
  189. data_file: Optional[str] = None,
  190. raster_config_override: Optional[Dict[str, Any]] = None
  191. ) -> Dict[str, Any]:
  192. """
  193. 为指定县市生成作物Cd预测
  194. @param {str} county_name - 县市名称
  195. @param {Optional[str]} data_file - 可选的数据文件路径
  196. @param {Optional[Dict[str, Any]]} raster_config_override - 栅格配置覆盖参数
  197. @returns {Dict[str, Any]} 预测结果信息
  198. """
  199. if not self.is_county_supported(county_name):
  200. raise ValueError(f"不支持的县市: {county_name}")
  201. try:
  202. self.logger.info(f"开始为{county_name}生成作物Cd预测(v3.0自包含版本)")
  203. # 读取数据文件
  204. if not data_file or not os.path.exists(data_file):
  205. raise ValueError("需要提供有效的数据文件")
  206. df = pd.read_csv(data_file, encoding='utf-8')
  207. # 确保前两列为经纬度
  208. df.columns = ['longitude', 'latitude'] + list(df.columns[2:])
  209. # 获取边界文件
  210. boundary_geojson = self._get_boundary_geojson(county_name)
  211. # 在线程池中运行预测
  212. loop = asyncio.get_event_loop()
  213. result = await loop.run_in_executor(
  214. None,
  215. self._run_crop_cd_prediction,
  216. df, county_name, boundary_geojson, raster_config_override
  217. )
  218. # 使用统一绘图接口,文件已直接生成在正确位置,无需复制
  219. api_result = self._format_api_result(result, county_name, "crop_cd")
  220. # 清理临时文件
  221. self._cleanup_after_prediction(boundary_geojson)
  222. return api_result
  223. except Exception as e:
  224. self.logger.error(f"为{county_name}生成作物Cd预测失败: {str(e)}")
  225. raise
  226. async def generate_effective_cd_prediction_for_county(
  227. self,
  228. county_name: str,
  229. data_file: Optional[str] = None,
  230. raster_config_override: Optional[Dict[str, Any]] = None
  231. ) -> Dict[str, Any]:
  232. """
  233. 为指定县市生成有效态Cd预测
  234. @param {str} county_name - 县市名称
  235. @param {Optional[str]} data_file - 可选的数据文件路径
  236. @param {Optional[Dict[str, Any]]} raster_config_override - 栅格配置覆盖参数
  237. @returns {Dict[str, Any]} 预测结果信息
  238. """
  239. if not self.is_county_supported(county_name):
  240. raise ValueError(f"不支持的县市: {county_name}")
  241. try:
  242. self.logger.info(f"开始为{county_name}生成有效态Cd预测(v3.0自包含版本)")
  243. # 读取数据文件
  244. if not data_file or not os.path.exists(data_file):
  245. raise ValueError("需要提供有效的数据文件")
  246. df = pd.read_csv(data_file, encoding='utf-8')
  247. # 确保前两列为经纬度
  248. df.columns = ['longitude', 'latitude'] + list(df.columns[2:])
  249. # 获取边界文件
  250. boundary_geojson = self._get_boundary_geojson(county_name)
  251. # 在线程池中运行预测
  252. loop = asyncio.get_event_loop()
  253. result = await loop.run_in_executor(
  254. None,
  255. self._run_effective_cd_prediction,
  256. df, county_name, boundary_geojson, raster_config_override
  257. )
  258. # 使用统一绘图接口,文件已直接生成在正确位置,无需复制
  259. api_result = self._format_api_result(result, county_name, "effective_cd")
  260. # 清理临时文件
  261. self._cleanup_after_prediction(boundary_geojson)
  262. return api_result
  263. except Exception as e:
  264. self.logger.error(f"为{county_name}生成有效态Cd预测失败: {str(e)}")
  265. raise
  266. def _run_crop_cd_prediction(self, df: pd.DataFrame, county_name: str,
  267. boundary_geojson: Optional[str],
  268. raster_config_override: Optional[Dict[str, Any]]) -> Dict[str, Any]:
  269. """
  270. 执行作物Cd预测(同步方法)
  271. @param {pd.DataFrame} df - 输入数据
  272. @param {str} county_name - 县市名称
  273. @param {Optional[str]} boundary_geojson - 边界文件路径
  274. @param {Optional[Dict[str, Any]]} raster_config_override - 栅格配置覆盖参数
  275. @returns {Dict[str, Any]} 预测结果
  276. """
  277. try:
  278. # 准备边界数据
  279. boundary_gdf = None
  280. if boundary_geojson:
  281. try:
  282. import geopandas as gpd
  283. boundary_gdf = gpd.read_file(boundary_geojson)
  284. self.logger.info(f"已加载边界文件: {boundary_geojson}")
  285. except Exception as e:
  286. self.logger.warning(f"加载边界文件失败,将不使用边界限制: {str(e)}")
  287. boundary_gdf = None
  288. # 执行预测和可视化(使用统一绘图接口,不保存栅格文件)
  289. result = self.engine.predict_and_visualize(
  290. input_data=df,
  291. model_type="crop_cd",
  292. county_name=county_name,
  293. boundary_gdf=boundary_gdf,
  294. raster_config_override=raster_config_override,
  295. save_raster=False # 不保存栅格文件,节省存储空间
  296. )
  297. return result
  298. except Exception as e:
  299. self.logger.error(f"作物Cd预测执行失败: {str(e)}")
  300. raise
  301. def _run_effective_cd_prediction(self, df: pd.DataFrame, county_name: str,
  302. boundary_geojson: Optional[str],
  303. raster_config_override: Optional[Dict[str, Any]]) -> Dict[str, Any]:
  304. """
  305. 执行有效态Cd预测(同步方法)
  306. @param {pd.DataFrame} df - 输入数据
  307. @param {str} county_name - 县市名称
  308. @param {Optional[str]} boundary_geojson - 边界文件路径
  309. @param {Optional[Dict[str, Any]]} raster_config_override - 栅格配置覆盖参数
  310. @returns {Dict[str, Any]} 预测结果
  311. """
  312. try:
  313. # 准备边界数据
  314. boundary_gdf = None
  315. if boundary_geojson:
  316. try:
  317. import geopandas as gpd
  318. boundary_gdf = gpd.read_file(boundary_geojson)
  319. self.logger.info(f"已加载边界文件: {boundary_geojson}")
  320. except Exception as e:
  321. self.logger.warning(f"加载边界文件失败,将不使用边界限制: {str(e)}")
  322. boundary_gdf = None
  323. # 执行预测和可视化(使用统一绘图接口,不保存栅格文件)
  324. result = self.engine.predict_and_visualize(
  325. input_data=df,
  326. model_type="effective_cd",
  327. county_name=county_name,
  328. boundary_gdf=boundary_gdf,
  329. raster_config_override=raster_config_override,
  330. save_raster=False # 不保存栅格文件,节省存储空间
  331. )
  332. return result
  333. except Exception as e:
  334. self.logger.error(f"有效态Cd预测执行失败: {str(e)}")
  335. raise
  336. def _format_api_result(self, result: Dict[str, Any], county_name: str,
  337. model_type: str) -> Dict[str, Any]:
  338. """
  339. 格式化API返回结果(使用统一绘图接口,无需复制文件)
  340. @param {Dict[str, Any]} result - 预测结果
  341. @param {str} county_name - 县市名称
  342. @param {str} model_type - 模型类型
  343. @returns {Dict[str, Any]} 格式化的API结果
  344. """
  345. try:
  346. model_prefix = f"{model_type}_{county_name}"
  347. # 直接使用引擎生成的文件路径,无需复制
  348. api_result = {
  349. 'map_path': result.get('map_path'),
  350. 'histogram_path': result.get('histogram_path'),
  351. 'raster_path': result.get('raster_path'),
  352. 'model_type': model_type,
  353. 'county_name': county_name,
  354. 'timestamp': result.get('timestamp'),
  355. 'validation': result.get('validation', {}),
  356. 'stats': self._get_file_stats(result.get('map_path'))
  357. }
  358. # 清理旧文件(保留)
  359. self._cleanup_old_files(model_prefix)
  360. self.logger.info(f"API结果格式化完成(统一接口): {model_type}_{county_name}")
  361. return api_result
  362. except Exception as e:
  363. self.logger.error(f"格式化API结果失败: {str(e)}")
  364. raise
  365. def _cleanup_after_prediction(self, boundary_geojson: Optional[str]):
  366. """
  367. 预测后清理工作
  368. @param {Optional[str]} boundary_geojson - 临时边界文件路径
  369. """
  370. try:
  371. # 清理预测引擎的临时文件
  372. self.engine.cleanup_temp_files()
  373. # 清理临时边界文件
  374. if boundary_geojson and os.path.exists(boundary_geojson):
  375. import shutil
  376. shutil.rmtree(os.path.dirname(boundary_geojson), ignore_errors=True)
  377. self.logger.info("临时边界文件已清理")
  378. except Exception as e:
  379. self.logger.warning(f"预测后清理失败: {str(e)}")
  380. def _cleanup_temp_files(self, county_name: str, max_files: int = 5):
  381. """
  382. 清理临时数据文件
  383. @param {str} county_name - 县市名称
  384. @param {int} max_files - 最大保留文件数
  385. """
  386. try:
  387. temp_dir = os.path.join(self.output_data_dir, "temp")
  388. temp_pattern = os.path.join(temp_dir, f"{county_name}_temp_data_*.csv")
  389. temp_files = glob.glob(temp_pattern)
  390. if len(temp_files) > max_files:
  391. temp_files.sort(key=os.path.getmtime)
  392. files_to_delete = temp_files[:-max_files]
  393. for file_to_delete in files_to_delete:
  394. os.remove(file_to_delete)
  395. self.logger.info(f"已删除旧临时文件: {os.path.basename(file_to_delete)}")
  396. except Exception as e:
  397. self.logger.warning(f"清理临时文件失败: {str(e)}")
  398. def _cleanup_old_files(self, model_prefix: str):
  399. """
  400. 清理旧的预测文件
  401. @param {str} model_prefix - 模型前缀
  402. """
  403. try:
  404. max_files = 10
  405. # 清理地图文件
  406. map_pattern = os.path.join(self.output_figures_dir, f"{model_prefix}_prediction_map_*.jpg")
  407. self._cleanup_files_by_pattern(map_pattern, max_files)
  408. # 清理直方图文件
  409. histogram_pattern = os.path.join(self.output_figures_dir, f"{model_prefix}_prediction_histogram_*.jpg")
  410. self._cleanup_files_by_pattern(histogram_pattern, max_files)
  411. except Exception as e:
  412. self.logger.warning(f"清理旧文件失败: {str(e)}")
  413. def _cleanup_files_by_pattern(self, pattern: str, max_files: int):
  414. """
  415. 按模式清理文件
  416. @param {str} pattern - 文件模式
  417. @param {int} max_files - 最大保留文件数
  418. """
  419. try:
  420. files = glob.glob(pattern)
  421. if len(files) > max_files:
  422. files.sort(key=os.path.getmtime)
  423. for file_to_delete in files[:-max_files]:
  424. os.remove(file_to_delete)
  425. self.logger.info(f"已删除旧文件: {os.path.basename(file_to_delete)}")
  426. except Exception as e:
  427. self.logger.warning(f"清理文件失败 {pattern}: {str(e)}")
  428. def _get_file_stats(self, file_path: Optional[str]) -> Dict[str, Any]:
  429. """
  430. 获取文件统计信息
  431. @param {Optional[str]} file_path - 文件路径
  432. @returns {Dict[str, Any]} 文件统计信息
  433. """
  434. if not file_path or not os.path.exists(file_path):
  435. return {}
  436. try:
  437. stat = os.stat(file_path)
  438. return {
  439. 'file_size': stat.st_size,
  440. 'created_time': datetime.fromtimestamp(stat.st_ctime).isoformat(),
  441. 'modified_time': datetime.fromtimestamp(stat.st_mtime).isoformat()
  442. }
  443. except Exception:
  444. return {}
  445. def get_engine_info(self) -> Dict[str, Any]:
  446. """
  447. 获取引擎信息
  448. @returns {Dict[str, Any]} 引擎信息
  449. """
  450. try:
  451. return self.engine.get_model_info()
  452. except Exception as e:
  453. self.logger.error(f"获取引擎信息失败: {str(e)}")
  454. return {"error": str(e)}
  455. # =============================================================================
  456. # 统计信息方法(简化版本,从最终数据文件中读取)
  457. # =============================================================================
  458. def get_crop_cd_statistics(self, county_name: str) -> Optional[Dict[str, Any]]:
  459. """
  460. 获取作物Cd预测结果的统计信息
  461. @param {str} county_name - 县市名称
  462. @returns {Optional[Dict[str, Any]]} 统计信息
  463. """
  464. try:
  465. # 查找最新的最终数据文件
  466. final_pattern = os.path.join(self.output_data_dir, "final", f"Final_predictions_crop_cd_*.csv")
  467. final_files = glob.glob(final_pattern)
  468. if not final_files:
  469. return None
  470. latest_file = max(final_files, key=os.path.getmtime)
  471. df = pd.read_csv(latest_file)
  472. if 'Prediction' not in df.columns:
  473. return None
  474. predictions = df['Prediction']
  475. basic_stats = {
  476. "数据点总数": len(predictions),
  477. "均值": float(predictions.mean()),
  478. "中位数": float(predictions.median()),
  479. "标准差": float(predictions.std()),
  480. "最小值": float(predictions.min()),
  481. "最大值": float(predictions.max()),
  482. "25%分位数": float(predictions.quantile(0.25)),
  483. "75%分位数": float(predictions.quantile(0.75))
  484. }
  485. return {
  486. "模型类型": "作物Cd模型v3.0",
  487. "县市名称": county_name,
  488. "数据更新时间": datetime.fromtimestamp(os.path.getmtime(latest_file)).isoformat(),
  489. "基础统计": basic_stats
  490. }
  491. except Exception as e:
  492. self.logger.error(f"获取作物Cd统计信息失败: {str(e)}")
  493. return None
  494. def get_effective_cd_statistics(self, county_name: str) -> Optional[Dict[str, Any]]:
  495. """
  496. 获取有效态Cd预测结果的统计信息
  497. @param {str} county_name - 县市名称
  498. @returns {Optional[Dict[str, Any]]} 统计信息
  499. """
  500. try:
  501. # 查找最新的最终数据文件
  502. final_pattern = os.path.join(self.output_data_dir, "final", f"Final_predictions_effective_cd_*.csv")
  503. final_files = glob.glob(final_pattern)
  504. if not final_files:
  505. return None
  506. latest_file = max(final_files, key=os.path.getmtime)
  507. df = pd.read_csv(latest_file)
  508. if 'Prediction' not in df.columns:
  509. return None
  510. predictions = df['Prediction']
  511. basic_stats = {
  512. "数据点总数": len(predictions),
  513. "均值": float(predictions.mean()),
  514. "中位数": float(predictions.median()),
  515. "标准差": float(predictions.std()),
  516. "最小值": float(predictions.min()),
  517. "最大值": float(predictions.max()),
  518. "25%分位数": float(predictions.quantile(0.25)),
  519. "75%分位数": float(predictions.quantile(0.75))
  520. }
  521. return {
  522. "模型类型": "有效态Cd模型v3.0",
  523. "县市名称": county_name,
  524. "数据更新时间": datetime.fromtimestamp(os.path.getmtime(latest_file)).isoformat(),
  525. "基础统计": basic_stats
  526. }
  527. except Exception as e:
  528. self.logger.error(f"获取有效态Cd统计信息失败: {str(e)}")
  529. return None
  530. def get_combined_statistics(self, county_name: str) -> Optional[Dict[str, Any]]:
  531. """
  532. 获取综合预测统计信息
  533. @param {str} county_name - 县市名称
  534. @returns {Optional[Dict[str, Any]]} 综合统计信息
  535. """
  536. try:
  537. crop_stats = self.get_crop_cd_statistics(county_name)
  538. effective_stats = self.get_effective_cd_statistics(county_name)
  539. if not crop_stats and not effective_stats:
  540. return None
  541. return {
  542. "县市名称": county_name,
  543. "作物Cd统计": crop_stats,
  544. "有效态Cd统计": effective_stats,
  545. "生成时间": datetime.now().isoformat(),
  546. "版本": "v3.0自包含版本"
  547. }
  548. except Exception as e:
  549. self.logger.error(f"获取综合统计信息失败: {str(e)}")
  550. return None
  551. def get_all_counties_statistics(self) -> Dict[str, Any]:
  552. """
  553. 获取所有支持县市的统计概览
  554. @returns {Dict[str, Any]} 所有县市的统计概览
  555. """
  556. try:
  557. all_stats = {
  558. "支持县市总数": len(self.supported_counties),
  559. "统计生成时间": datetime.now().isoformat(),
  560. "版本": "v3.0自包含版本",
  561. "县市统计": {},
  562. "汇总信息": {
  563. "有作物Cd数据的县市": 0,
  564. "有有效态Cd数据的县市": 0,
  565. "数据完整的县市": 0
  566. }
  567. }
  568. for county_name in self.supported_counties.keys():
  569. county_stats = {
  570. "县市名称": county_name,
  571. "有作物Cd数据": False,
  572. "有有效态Cd数据": False,
  573. "数据完整": False
  574. }
  575. # 检查作物Cd数据
  576. crop_stats = self.get_crop_cd_statistics(county_name)
  577. if crop_stats:
  578. county_stats["有作物Cd数据"] = True
  579. all_stats["汇总信息"]["有作物Cd数据的县市"] += 1
  580. # 检查有效态Cd数据
  581. effective_stats = self.get_effective_cd_statistics(county_name)
  582. if effective_stats:
  583. county_stats["有有效态Cd数据"] = True
  584. all_stats["汇总信息"]["有有效态Cd数据的县市"] += 1
  585. # 检查数据完整性
  586. if county_stats["有作物Cd数据"] and county_stats["有有效态Cd数据"]:
  587. county_stats["数据完整"] = True
  588. all_stats["汇总信息"]["数据完整的县市"] += 1
  589. all_stats["县市统计"][county_name] = county_stats
  590. return all_stats
  591. except Exception as e:
  592. self.logger.error(f"获取所有县市统计概览失败: {str(e)}")
  593. return {
  594. "error": f"获取统计概览失败: {str(e)}",
  595. "支持县市总数": len(self.supported_counties),
  596. "统计生成时间": datetime.now().isoformat(),
  597. "版本": "v3.0自包含版本"
  598. }