优化参数

This commit is contained in:
wzy-warehouse
2026-06-06 11:29:08 +08:00
parent 9c3b0575d2
commit cb2d8c2c54
4 changed files with 154 additions and 6 deletions
+3 -2
View File
@@ -203,6 +203,7 @@ pipe_density:
description: "供水管网密度" description: "供水管网密度"
unit: "m/m²" unit: "m/m²"
# 数据: [0.0, 0.07], 约80%为0.090%分位数0.01395%分位数0.023 # 数据: [0.0, 0.07], 约80%为0.090%分位数0.01395%分位数0.023
# 分位数: [0.0, 0.013, 0.023, 0.065] # 分箱策略:0单独一类,其余3等分(分位数分箱)
bins: [0.0, 0.013, 0.023, 0.065] # 分位数(非零): [0.013, 0.023, 0.065]
bins: [0.0, 0.001, 0.013, 0.023, 0.065]
labels: [none, low, medium, high] labels: [none, low, medium, high]
+19 -2
View File
@@ -214,7 +214,8 @@ class RainfallDBN:
def predict_single_point(self, point: Dict[str, Any], def predict_single_point(self, point: Dict[str, Any],
rainfall: Optional[float] = None, rainfall: Optional[float] = None,
duration: Optional[float] = None, duration: Optional[float] = None,
query_time: Optional[datetime] = None) -> Dict[str, Any]: query_time: Optional[datetime] = None,
rainfall_data_override: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
""" """
对单个点进行预测 对单个点进行预测
@@ -223,6 +224,7 @@ class RainfallDBN:
rainfall: 累计降雨量(可选) rainfall: 累计降雨量(可选)
duration: 持续时间(可选) duration: 持续时间(可选)
query_time: 查询时间(可选) query_time: 查询时间(可选)
rainfall_data_override: 预取的降雨数据(批量模式下避免重复查询)
Returns: Returns:
预测结果 预测结果
@@ -242,6 +244,8 @@ class RainfallDBN:
'duration_hours': duration, 'duration_hours': duration,
'rain_intensity': rain_intensity 'rain_intensity': rain_intensity
} }
elif rainfall_data_override is not None:
rainfall_data = rainfall_data_override
else: else:
rainfall_data = DbnRepository.get_rainfall_data_with_duration(lon, lat, query_time) rainfall_data = DbnRepository.get_rainfall_data_with_duration(lon, lat, query_time)
@@ -389,11 +393,24 @@ class RainfallDBN:
Returns: Returns:
预测结果列表 预测结果列表
""" """
# 批量预取降雨数据(避免逐点N+1查询)
batch_rainfall: Optional[Dict[str, Dict[str, Any]]] = None
if (rainfall is None or duration is None) and points:
batch_points = [
{'id': p.get('id'), 'lon': p.get('lon'), 'lat': p.get('lat')}
for p in points
]
batch_rainfall = DbnRepository.get_rainfall_data_batch(batch_points, query_time)
logger.info(f"批量预取降雨数据完成,覆盖 {len(batch_rainfall)} 个点")
results = [] results = []
for point in points: for point in points:
try: try:
point_id = point.get('id')
override = batch_rainfall.get(point_id) if batch_rainfall else None
result = self.predict_single_point( result = self.predict_single_point(
point, rainfall=rainfall, duration=duration, query_time=query_time point, rainfall=rainfall, duration=duration,
query_time=query_time, rainfall_data_override=override
) )
results.append(result) results.append(result)
except Exception as e: except Exception as e:
+128
View File
@@ -314,6 +314,134 @@ class DbnRepository:
'rain_intensity': rain_intensity 'rain_intensity': rain_intensity
} }
# ---- 批量降雨查询(性能优化) ----
_cached_stations: Optional[List[Dict[str, Any]]] = None
@classmethod
def _ensure_stations_cached(cls) -> List[Dict[str, Any]]:
"""一次性加载所有气象站点坐标到内存(188个站点,约2KB)"""
if cls._cached_stations is not None:
return cls._cached_stations
sql = "SELECT DISTINCT lon, lat FROM xian_meteorology"
cls._cached_stations = db_helper.execute_query(sql)
logger.info(f"已缓存 {len(cls._cached_stations)} 个气象站点坐标")
return cls._cached_stations
@staticmethod
def _haversine_distance(lon1: float, lat1: float, lon2: float, lat2: float) -> float:
"""Haversine公式计算两点间距离(米)"""
R = 6371000
phi1, phi2 = math.radians(lat1), math.radians(lat2)
dphi = math.radians(lat2 - lat1)
dlam = math.radians(lon2 - lon1)
a = math.sin(dphi / 2) ** 2 + math.cos(phi1) * math.cos(phi2) * math.sin(dlam / 2) ** 2
return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
@classmethod
def _find_nearest_station(cls, lon: float, lat: float) -> Optional[Dict[str, Any]]:
"""在缓存的站点中找最近的一个(纯Python,微秒级)"""
stations = cls._ensure_stations_cached()
if not stations:
return None
best = None
best_dist = float('inf')
for s in stations:
d = cls._haversine_distance(lon, lat, s['lon'], s['lat'])
if d < best_dist:
best_dist = d
best = s
if best_dist > 50000:
return None
return {'lon': best['lon'], 'lat': best['lat'], 'dist': best_dist}
@classmethod
def get_rainfall_data_batch(cls, points: List[Dict[str, Any]],
query_time: Optional[datetime] = None) -> Dict[str, Dict[str, Any]]:
"""
批量获取多个点的降雨数据(2次DB查询替代 N×2次)
Args:
points: 预测点列表,每个含 {'id': str, 'lon': float, 'lat': float}
query_time: 查询时间
Returns:
{point_id: {accum_rain, duration_hours, rain_intensity}}
"""
if query_time is None:
query_time = datetime.now()
# 结果模板(无数据时的默认值)
default = {'accum_rain': 0.0, 'duration_hours': 0, 'rain_intensity': 0.0}
result: Dict[str, Dict[str, Any]] = {}
# 1. 为每个点找最近站点(纯Python,瞬间完成)
station_to_points: Dict[tuple, List[str]] = {}
for p in points:
station = cls._find_nearest_station(p['lon'], p['lat'])
if station is None:
result[p['id']] = default.copy()
continue
key = (station['lon'], station['lat'])
station_to_points.setdefault(key, []).append(p['id'])
if not station_to_points:
return result
# 2. 一次批量查所有站点的72小时降雨数据
station_keys = list(station_to_points.keys())
placeholders = ', '.join(['(%s, %s)'] * len(station_keys))
params: List[Any] = []
for slon, slat in station_keys:
params.extend([slon, slat])
params.extend([query_time, query_time])
# noinspection SqlNoDataSourceInspection
sql = f"""
SELECT lon, lat, datetime, CAST(rainfall_1h AS DOUBLE PRECISION) as rainfall
FROM xian_meteorology
WHERE (lon, lat) IN ({placeholders})
AND datetime BETWEEN
CAST(EXTRACT(EPOCH FROM (%s::timestamp - INTERVAL '72 hours')) AS BIGINT)
AND CAST(EXTRACT(EPOCH FROM %s::timestamp) AS BIGINT)
ORDER BY lon, lat, datetime DESC
"""
rows = db_helper.execute_query(sql, tuple(params))
# 3. 按站点分组,计算累计降雨量和持续时间
from itertools import groupby
station_rainfall: Dict[tuple, Dict[str, Any]] = {}
for (slon, slat), group in groupby(rows, key=lambda r: (r['lon'], r['lat'])):
accum_rain = 0.0
duration_hours = 0
consecutive_no_rain = 0
for row in group:
rainfall = float(row['rainfall']) if row['rainfall'] else 0.0
if rainfall > 0:
accum_rain += rainfall
duration_hours += 1
consecutive_no_rain = 0
else:
consecutive_no_rain += 1
if consecutive_no_rain >= 3:
break
if accum_rain > 0:
duration_hours += 1
intensity = accum_rain / duration_hours if duration_hours > 0 else 0.0
station_rainfall[(slon, slat)] = {
'accum_rain': accum_rain,
'duration_hours': duration_hours,
'rain_intensity': intensity
}
# 4. 分发给各预测点
for (slon, slat), point_ids in station_to_points.items():
rain_data = station_rainfall.get((slon, slat), default)
for pid in point_ids:
result[pid] = rain_data
return result
# ==================== 空间查询 ==================== # ==================== 空间查询 ====================
@staticmethod @staticmethod
+4 -2
View File
@@ -14,8 +14,10 @@ class RainfallPredictRequest(BaseModel):
point_ids: Optional[List[int]] = Field(None, max_length=500, point_ids: Optional[List[int]] = Field(None, max_length=500,
description="点位ID列表,不传则查询所有点") description="点位ID列表,不传则查询所有点")
region_code: Optional[str] = Field(None, description="行政区划代码(如 '610104'),不传则不限区域") region_code: Optional[str] = Field(None, description="行政区划代码(如 '610104'),不传则不限区域")
rainfall: float = Field(..., ge=0, description="累计降雨量(mm)") rainfall: Optional[float] = Field(None, ge=0,
duration: float = Field(..., ge=0, description="降雨持续时间(h)") description="累计降雨量(mm),不传则从气象表自动获取")
duration: Optional[float] = Field(None, ge=0,
description="降雨持续时间(h),不传则从气象表自动获取")
# ============================================================ # ============================================================