fix: 修复特征分析页面图表 + 补充派生特征计算脚本

- src/routes.py: 补充火箭炮分析API缺失的8个字段(射速/射程/发动机/机动参数)
- AnalysisPage.vue: 修复火力/机动性能图表data映射,机动图表改用双Y轴avoid ECharts渲染失败
- AlgorithmDemoPage.vue: 移除算法卡片中的平均绝对误差和均方根误差显示
- scripts/calculate_features.py: 新增派生特征计算脚本(min-max归一化评分、巡飞弹缺失字段估算)
This commit is contained in:
tian 2026-06-09 17:25:32 +08:00
parent 48ba547c36
commit ca8cf02aa4
4 changed files with 418 additions and 13 deletions

View File

@ -86,10 +86,7 @@
<el-tag v-if="row.key === result.best_model" size="small" type="success">最佳</el-tag>
</div>
<strong>{{ formatScore(row.r2) }}</strong>
<div class="metric-values">
<span>平均绝对误差 {{ formatMoney(row.mae) }}</span>
<span>均方根误差 {{ formatMoney(row.rmse) }}</span>
</div>
</article>
</section>

View File

@ -735,7 +735,13 @@ const renderCharts = () => {
mobilityScore: analysisResult.value.mobility_score || [],
deploymentScore: analysisResult.value.deployment_score || [],
terrainScore: analysisResult.value.terrain_adaptability_score || [],
combatReadinessScore: analysisResult.value.combat_readiness_score || []
combatReadinessScore: analysisResult.value.combat_readiness_score || [],
rocketWeight: analysisResult.value.rocket_weight_kg || [],
rocketDiameter: analysisResult.value.rocket_diameter_mm || [],
rocketLength: analysisResult.value.rocket_length_m || [],
speedKmh: analysisResult.value.speed_kmh || [],
powerHp: analysisResult.value.power_hp || [],
travelRangeKm: analysisResult.value.travel_range_km || []
}
//
@ -849,7 +855,7 @@ const renderCharts = () => {
top: 30,
data: [
'机动性评分', '部署评分', '地形适应性评分', '战备状态评分',
'行驶速度', '功率', '行程' //
'行驶速度', '功率', '行程'
]
},
grid: {
@ -861,12 +867,20 @@ const renderCharts = () => {
type: 'category',
data: chartData.names
},
yAxis: {
type: 'value',
name: '评分',
min: 0,
max: 10
},
yAxis: [
{
type: 'value',
name: '评分',
position: 'left',
min: 0,
max: 10
},
{
type: 'value',
name: '速度/功率/行程',
position: 'right'
}
],
series: [
{
name: '机动性评分',
@ -891,16 +905,19 @@ const renderCharts = () => {
{
name: '行驶速度',
type: 'line',
yAxisIndex: 1,
data: chartData.speedKmh
},
{
name: '功率',
type: 'line',
yAxisIndex: 1,
data: chartData.powerHp
},
{
name: '行程',
type: 'line',
yAxisIndex: 1,
data: chartData.travelRangeKm
}
]

View File

@ -0,0 +1,383 @@
"""
计算并填充数据库中的派生/计算特征字段包括
1. 火箭炮火力密度射程比各项评分
2. 巡飞弹长宽比重量射程比等 + 估算缺失的原始字段
(engine_power_kw, engine_thrust_n, min/max_altitude_m,
guidance_accuracy_m, datalink_range_km)
用法
python -m scripts.calculate_features
"""
import sys
import os
import math
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from src.database.db_connection import get_db_connection, init_db
from src.logger import setup_logger
logger = setup_logger(__name__)
def clamp(value, low=1, high=10):
return max(low, min(high, value))
def safe_float(val, default=0.0):
if val is None:
return default
return float(val)
# ────────────────────────────────────────────────
# 火箭炮
# ────────────────────────────────────────────────
def calculate_rocket_features(conn):
cursor = conn.cursor()
cursor.execute("""
SELECT rap.id, rap.equipment_id,
rap.rate_of_fire, rap.rocket_weight_kg, rap.rocket_length_m,
rap.rocket_diameter_mm, rap.min_range_km, rap.max_range_km,
rap.speed_kmh, rap.power_hp, rap.combat_weight_kg,
rap.travel_range_km, rap.firing_angle_horizontal,
rap.firing_angle_vertical,
cp.length_m, cp.width_m, cp.weight_kg
FROM rocket_artillery_params rap
LEFT JOIN common_params cp ON rap.equipment_id = cp.equipment_id
""")
rows = cursor.fetchall()
# ── 第一遍:收集原始值,计算 min/max用于归一化──
all_vals = {
'spd': [], 'power_ratio': [], 'tr': [],
'angle_cov': [], 'deploy_factor': [], 'rof': [],
'cw': []
}
for row in rows:
spd = safe_float(row['speed_kmh'])
pwr = safe_float(row['power_hp'])
cw = safe_float(row['combat_weight_kg'])
tr = safe_float(row['travel_range_km'])
ah = safe_float(row['firing_angle_horizontal'])
av = safe_float(row['firing_angle_vertical'])
rof = safe_float(row['rate_of_fire'])
all_vals['spd'].append(spd)
all_vals['power_ratio'].append(pwr / max(1, cw))
all_vals['tr'].append(tr)
all_vals['angle_cov'].append((ah / 360) * (av / 90))
all_vals['deploy_factor'].append(1.0 / max(1, cw)) # 越小越难部署
all_vals['rof'].append(rof)
all_vals['cw'].append(cw)
def norm10(v, vals):
"""Min-max 归一化到 1-10"""
mn, mx = min(vals), max(vals)
span = mx - mn
if span == 0:
return 5.0
return 1.0 + (v - mn) / span * 9.0
# ── 第二遍:逐条计算并更新 ──
updates = []
for row in rows:
rof = safe_float(row['rate_of_fire'])
rw = safe_float(row['rocket_weight_kg'])
rl = safe_float(row['rocket_length_m'])
rd = safe_float(row['rocket_diameter_mm'])
min_r = safe_float(row['min_range_km'])
max_r = safe_float(row['max_range_km'])
spd = safe_float(row['speed_kmh'])
pwr = safe_float(row['power_hp'])
cw = safe_float(row['combat_weight_kg'])
tr = safe_float(row['travel_range_km'])
ah = safe_float(row['firing_angle_horizontal'])
av = safe_float(row['firing_angle_vertical'])
# 火力密度 (kg/min)
fire_density = round(rw * rof, 2)
# 射程比
range_ratio = round(max_r / max(1, min_r), 4)
# 火箭弹功重比
if rd > 0 and rl > 0:
rocket_power_ratio = round((rw * rl * rof) / (rd * 10), 4)
else:
rocket_power_ratio = 0.0
# 平台效率 (m/kg)
platform_efficiency = round(max_r / max(1, cw) * 1000, 4)
power_ratio = pwr / max(1, cw)
angle_cov = (ah / 360) * (av / 90)
deploy_factor = 1.0 / max(1, cw)
# 机动性评分 (1-10)综合速度、功重比、行程min-max 归一化
s_speed = norm10(spd, all_vals['spd'])
s_power = norm10(power_ratio, all_vals['power_ratio'])
s_range = norm10(tr, all_vals['tr'])
mobility_score = int(round((s_speed + s_power + s_range) / 3))
# 战备状态评分 (1-10):射界覆盖、部署便捷度(轻量化)、射速
s_angle = norm10(angle_cov, all_vals['angle_cov'])
s_deploy = norm10(deploy_factor, all_vals['deploy_factor'])
s_rof = norm10(rof, all_vals['rof'])
combat_readiness_score = int(round((s_angle + s_deploy + s_rof) / 3))
# 部署评分 (1-10):轻量化 + 速度
s_weight = 11.0 - norm10(cw, all_vals['cw']) # 越轻越高
s_speed2 = norm10(spd, all_vals['spd'])
deployment_score = int(round((s_weight + s_speed2) / 2))
# 地形适应性评分 (1-10):功重比 + 速度
terrain_score = int(round((s_power + s_speed2) / 2))
updates.append((
fire_density, range_ratio, mobility_score,
combat_readiness_score, deployment_score, terrain_score,
rocket_power_ratio, platform_efficiency,
row['id']
))
if updates:
cursor.executemany("""
UPDATE rocket_artillery_params
SET fire_density = ?, range_ratio = ?, mobility_score = ?,
combat_readiness_score = ?, deployment_score = ?,
terrain_adaptability_score = ?, rocket_power_ratio = ?,
platform_efficiency = ?
WHERE id = ?
""", updates)
logger.info(f"火箭炮:更新了 {len(updates)} 条记录")
return len(updates)
# ────────────────────────────────────────────────
# 巡飞弹
# ────────────────────────────────────────────────
def estimate_guidance_accuracy(guidance_system):
"""根据制导系统文字估算制导精度 (m)"""
gs = (guidance_system or '').lower()
# 统计制导模式数量作为精度指标
modes = [m.strip() for m in guidance_system.split('/') if m.strip()] if guidance_system else []
mode_count = len(modes)
# 基础精度:模式越多越精确
base_accuracy = {
1: 50, 2: 30, 3: 20, 4: 15, 5: 10, 6: 5, 7: 3, 8: 2, 9: 1.5, 10: 1,
}.get(mode_count, max(1, 60 - mode_count * 6))
# 根据制导类型微调
if any(k in gs for k in ['卫星', 'satellite', 'gps']):
base_accuracy = max(3, base_accuracy * 0.7)
if any(k in gs for k in ['激光', 'laser']):
base_accuracy = max(1, base_accuracy * 0.4)
if any(k in gs for k in ['红外', 'infrared', 'ir', '光电']):
base_accuracy = max(2, base_accuracy * 0.6)
if any(k in gs for k in ['雷达', 'radar']):
base_accuracy = max(5, base_accuracy * 0.8)
if any(k in gs for k in ['ai', '辅助']):
base_accuracy = base_accuracy * 0.8
return round(base_accuracy, 1)
def calculate_guidance_score(guidance_system, estimated_accuracy):
"""计算制导系统评分 (1-10)"""
gs = (guidance_system or '').lower()
modes = [m.strip() for m in guidance_system.split('/') if m.strip()] if guidance_system else []
# 制导模式数量 (1-7 映射到 1-6 分)
mode_score = clamp(len(modes) / 2, 0.5, 6)
# 精度分 (精度越高分越高)
if estimated_accuracy <= 3:
accuracy_score = 5
elif estimated_accuracy <= 5:
accuracy_score = 4.5
elif estimated_accuracy <= 10:
accuracy_score = 4
elif estimated_accuracy <= 20:
accuracy_score = 3
elif estimated_accuracy <= 30:
accuracy_score = 2
elif estimated_accuracy <= 50:
accuracy_score = 1
else:
accuracy_score = 0.5
# 高端制导方式加成
high_end_bonus = 0
if any(k in gs for k in ['卫星', 'satellite']):
high_end_bonus += 0.5
return int(round(clamp(mode_score + accuracy_score + high_end_bonus, 1, 10)))
def calculate_warhead_score(warhead_type, warhead_weight_kg):
"""计算战斗部威力评分 (1-10)"""
wt_lower = (warhead_type or '').lower()
# 重量得分: 5kg→2, 10kg→4, 20kg→6, 30kg→8, 50kg→10
weight_score = clamp(warhead_weight_kg * 0.2, 1, 10)
# 类型加成
type_kw_scores = {
'模块化': 3, 'modular': 3,
'高爆': 2, 'explosive': 2, 'he': 2,
'云爆': 3, 'thermobaric': 3,
'破片杀伤': 1.5, 'fragmentation': 1.5,
'穿甲': 2.5, 'armor': 2.5, 'penetrat': 2.5,
'动能': 2, 'kinetic': 2,
'双用': 2, 'dual': 2,
'破甲': 2, 'heat': 2,
'子母': 2, 'cluster': 2, 'submunition': 2
}
type_bonus = 0
for kw, val in type_kw_scores.items():
if kw in wt_lower:
type_bonus = max(type_bonus, val)
return int(round(clamp(weight_score + type_bonus, 1, 10)))
def calculate_loitering_features(conn):
"""计算巡飞弹的派生特征 + 估算缺失的原始字段"""
cursor = conn.cursor()
cursor.execute("""
SELECT lmp.id, lmp.equipment_id,
lmp.wingspan_m, lmp.warhead_weight_kg, lmp.max_speed_ms,
lmp.max_range_km, lmp.guidance_accuracy_m, lmp.datalink_range_km,
lmp.engine_power_kw, lmp.engine_thrust_n,
lmp.min_altitude_m, lmp.max_altitude_m,
lmp.guidance_system, lmp.warhead_type,
lmp.cruise_speed_kmh, lmp.endurance_min,
lmp.ceiling_altitude_m, lmp.max_payload_kg,
lmp.combat_radius_km,
cp.length_m, cp.width_m, cp.weight_kg
FROM loitering_munition_params lmp
LEFT JOIN common_params cp ON lmp.equipment_id = cp.equipment_id
""")
rows = cursor.fetchall()
updates = []
for row in rows:
length = safe_float(row['length_m'])
width = safe_float(row['width_m'])
weight = safe_float(row['weight_kg'])
warhead_weight = safe_float(row['warhead_weight_kg'])
max_speed = safe_float(row['max_speed_ms'])
cruise_speed = safe_float(row['cruise_speed_kmh'])
max_range = safe_float(row['max_range_km'])
endurance = safe_float(row['endurance_min'])
ceiling = safe_float(row['ceiling_altitude_m'])
payload = safe_float(row['max_payload_kg'])
combat_radius = safe_float(row['combat_radius_km'])
guidance_system = row['guidance_system'] or ''
warhead_type = row['warhead_type'] or ''
# ── 1. 长宽比 ──
length_width_ratio = round(length / max(0.1, width), 4)
# ── 2. 重量射程比 (kg/km) ──
weight_range_ratio = round(weight / max(1, max_range), 4)
# ── 3. 速度重量比 ──
speed_weight_ratio = round(max_speed / max(1, weight), 4)
# ── 4. 估算缺失的原始字段 ──
# 制导精度 (m):当前全为 0根据制导系统文字估算
estimated_accuracy = estimate_guidance_accuracy(guidance_system)
# 数据链距离 (km):用战斗半径或最大射程的 80% 估算
base_ref = combat_radius if combat_radius > 0 else max_range
estimated_datalink = round(base_ref * 0.85, 1)
# 发动机功率 (kw):基于重量和速度估算
# 小型无人机/巡飞弹功率密度约 0.05-0.3 kW/kg
total_weight = weight + payload
power_density = 0.08 + (max_speed / 200) * 0.12 # 越快功率密度越高
power_density = min(power_density, 0.28) # 上限避免重型弹药估算过高
estimated_engine_kw = round(total_weight * power_density, 1)
# 发动机推力 (N):基于总重估算,推力 ≈ 重量 * 推重比
# 巡飞弹推重比通常 0.2-0.5,但重型滑翔弹药不具备大推力发动机
thrust_ratio = 0.15 + (max_speed / 150) * 0.20
thrust_ratio = min(thrust_ratio, 0.3) # 上限
estimated_thrust_n = round(total_weight * 9.8 * thrust_ratio, 1)
# 最小作战高度 (m):巡飞弹通常 50-500m
if cruise_speed > 0:
estimated_min_alt = round(max(30, min(300, cruise_speed * 0.8)), 0)
else:
estimated_min_alt = round(max(30, min(300, max_speed * 2.5)), 0)
# 最大作战高度 (m):优先用 ceiling_altitude_m否则估算
if ceiling > 0:
estimated_max_alt = ceiling
else:
estimated_max_alt = round(max(500, min(10000, endurance * 15)), 0)
# ── 5. 制导系统评分 (1-10) ──
guidance_system_score = calculate_guidance_score(guidance_system, estimated_accuracy)
# ── 6. 战斗部威力评分 (1-10) ──
warhead_power_score = calculate_warhead_score(warhead_type, warhead_weight)
updates.append((
length_width_ratio, weight_range_ratio, speed_weight_ratio,
guidance_system_score, warhead_power_score,
estimated_accuracy, estimated_datalink,
estimated_engine_kw, estimated_thrust_n,
estimated_min_alt, estimated_max_alt,
row['id']
))
if updates:
cursor.executemany("""
UPDATE loitering_munition_params
SET length_width_ratio = ?, weight_range_ratio = ?,
speed_weight_ratio = ?, guidance_system_score = ?,
warhead_power_score = ?,
guidance_accuracy_m = ?, datalink_range_km = ?,
engine_power_kw = ?, engine_thrust_n = ?,
min_altitude_m = ?, max_altitude_m = ?
WHERE id = ?
""", updates)
logger.info(f"巡飞弹:更新了 {len(updates)} 条记录")
return len(updates)
def main():
init_db()
conn = None
try:
with get_db_connection() as conn:
rc = calculate_rocket_features(conn)
lc = calculate_loitering_features(conn)
conn.commit()
print(f"\n完成!")
print(f" 火箭炮:{rc} 条记录已更新")
print(f" 巡飞弹:{lc} 条记录已更新")
except Exception as e:
if conn:
conn.rollback()
logger.error(f"特征计算失败: {e}")
raise
if __name__ == '__main__':
main()

View File

@ -259,7 +259,15 @@ def analyze_features():
'mobility_score': [float(item['mobility_score']) if item['mobility_score'] is not None else 0 for item in equipment_data],
'combat_readiness_score': [float(item['combat_readiness_score']) if item['combat_readiness_score'] is not None else 0 for item in equipment_data],
'deployment_score': [float(item['deployment_score']) if item['deployment_score'] is not None else 0 for item in equipment_data],
'terrain_adaptability_score': [float(item['terrain_adaptability_score']) if item['terrain_adaptability_score'] is not None else 0 for item in equipment_data]
'terrain_adaptability_score': [float(item['terrain_adaptability_score']) if item['terrain_adaptability_score'] is not None else 0 for item in equipment_data],
'rate_of_fire': [float(item['rate_of_fire']) if item['rate_of_fire'] is not None else 0 for item in equipment_data],
'max_range_km': [float(item['max_range_km']) if item['max_range_km'] is not None else 0 for item in equipment_data],
'rocket_length_m': [float(item['rocket_length_m']) if item['rocket_length_m'] is not None else 0 for item in equipment_data],
'rocket_diameter_mm': [float(item['rocket_diameter_mm']) if item['rocket_diameter_mm'] is not None else 0 for item in equipment_data],
'rocket_weight_kg': [float(item['rocket_weight_kg']) if item['rocket_weight_kg'] is not None else 0 for item in equipment_data],
'speed_kmh': [float(item['speed_kmh']) if item['speed_kmh'] is not None else 0 for item in equipment_data],
'power_hp': [float(item['power_hp']) if item['power_hp'] is not None else 0 for item in equipment_data],
'travel_range_km': [float(item['travel_range_km']) if item['travel_range_km'] is not None else 0 for item in equipment_data],
}
analysis_result.update(rocket_data)
else: