完成了基本功能
This commit is contained in:
parent
6e5172962a
commit
fccd4c4366
24
.cursorrules
24
.cursorrules
@ -1,25 +1,3 @@
|
|||||||
# 开发流程
|
|
||||||
|
|
||||||
First ensure basic functionality works
|
|
||||||
Implement core functionality using the simplest direct approach
|
|
||||||
Ensure data flow is working correctly
|
|
||||||
Verify results are accurate
|
|
||||||
Then gradually add additional features
|
|
||||||
Add error handling
|
|
||||||
Add data validation
|
|
||||||
Add format conversion
|
|
||||||
Add logging
|
|
||||||
Improve user experience
|
|
||||||
Avoid premature optimization
|
|
||||||
Don't do complex data validation at the start
|
|
||||||
Don't worry about performance optimization early
|
|
||||||
Don't over-engineer
|
|
||||||
This development flow:
|
|
||||||
Quickly validates if core functionality works
|
|
||||||
Identifies and fixes fundamental issues early
|
|
||||||
Avoids wasting time on unnecessary optimizations
|
|
||||||
Makes code easier to maintain and debug
|
|
||||||
These principles should guide all code responses, focusing on getting the basics working first before adding complexity.
|
|
||||||
|
|
||||||
# 代码修改最佳实践
|
# 代码修改最佳实践
|
||||||
|
|
||||||
@ -78,5 +56,3 @@ These principles should guide all code responses, focusing on getting the basics
|
|||||||
- 处理异常情况
|
- 处理异常情况
|
||||||
- 保护敏感信息
|
- 保护敏感信息
|
||||||
- 添加访问控制
|
- 添加访问控制
|
||||||
|
|
||||||
These practices help maintain code quality and reduce potential issues.
|
|
||||||
|
|||||||
8
app.py
8
app.py
@ -1,8 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
# 配置日志
|
|
||||||
logging.basicConfig(
|
|
||||||
filename='logs/api.log',
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
|
||||||
)
|
|
||||||
@ -614,3 +614,32 @@ trainingResult.value = null
|
|||||||
2. 可以考虑集成 XGBoost 和 Random Forest
|
2. 可以考虑集成 XGBoost 和 Random Forest
|
||||||
3. 继续调整 LightGBM 的参数
|
3. 继续调整 LightGBM 的参数
|
||||||
4. 暂时不使用 GBDT
|
4. 暂时不使用 GBDT
|
||||||
|
|
||||||
|
### 数据集存在的问题
|
||||||
|
|
||||||
|
火箭炮数据集:
|
||||||
|
|
||||||
|
- Feature length_m missing rate: 0.00%
|
||||||
|
- Feature width_m missing rate: 9.09%
|
||||||
|
- Feature height_m missing rate: 9.09%
|
||||||
|
- Feature weight_kg missing rate: 0.00%
|
||||||
|
- Feature max_range_km missing rate: 45.45%
|
||||||
|
- Feature firing_angle_horizontal missing rate: 45.45%
|
||||||
|
- Feature firing_angle_vertical missing rate: 45.45%
|
||||||
|
- Feature rocket_length_m missing rate: 72.73%
|
||||||
|
- Feature rocket_diameter_mm missing rate: 0.00%
|
||||||
|
- Feature rocket_weight_kg missing rate: 72.73%
|
||||||
|
- Feature rate_of_fire missing rate: 54.55%
|
||||||
|
|
||||||
|
巡飞弹数据集:
|
||||||
|
|
||||||
|
- Feature length_m missing rate: 27.78%
|
||||||
|
- Feature width_m missing rate: 50.00%
|
||||||
|
- Feature height_m missing rate: 50.00%
|
||||||
|
- Feature weight_kg missing rate: 22.22%
|
||||||
|
- Feature max_range_km missing rate: 44.44%
|
||||||
|
- Feature wingspan_m missing rate: 50.00%
|
||||||
|
- Feature warhead_weight_kg missing rate: 77.78%
|
||||||
|
- Feature max_speed_ms missing rate: 77.78%
|
||||||
|
- Feature cruise_speed_kmh missing rate: 61.11%
|
||||||
|
- Feature flight_time_min missing rate: 33.33%
|
||||||
|
|||||||
@ -16,5 +16,5 @@
|
|||||||
"scripthost"
|
"scripthost"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"exclude": ["**/HelloWorld.vue"]
|
"include": ["src/**/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,13 +7,12 @@
|
|||||||
:default-active="$route.path"
|
:default-active="$route.path"
|
||||||
>
|
>
|
||||||
<el-menu-item index="/">首页</el-menu-item>
|
<el-menu-item index="/">首页</el-menu-item>
|
||||||
<el-menu-item index="/predict">机器学习预测</el-menu-item>
|
<el-menu-item index="/predict">成本预测</el-menu-item>
|
||||||
<el-menu-item index="/pls-predict">PLS回归预测</el-menu-item>
|
|
||||||
<el-menu-item index="/analysis">特征分析</el-menu-item>
|
<el-menu-item index="/analysis">特征分析</el-menu-item>
|
||||||
<el-menu-item index="/training">模型训练</el-menu-item>
|
<el-menu-item index="/training">模型训练</el-menu-item>
|
||||||
<el-menu-item index="/data">数据管理</el-menu-item>
|
|
||||||
<el-menu-item index="/datasets">数据集管理</el-menu-item>
|
|
||||||
<el-menu-item index="/models">模型管理</el-menu-item>
|
<el-menu-item index="/models">模型管理</el-menu-item>
|
||||||
|
<el-menu-item index="/datasets">数据集管理</el-menu-item>
|
||||||
|
<el-menu-item index="/data">数据管理</el-menu-item>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,7 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 全局错误处理
|
// 全局错误处理
|
||||||
app.config.errorHandler = (err, vm, info) => {
|
app.config.errorHandler = (err) => {
|
||||||
if (err.message && err.message.includes('ResizeObserver')) {
|
if (err.message && err.message.includes('ResizeObserver')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -31,7 +31,7 @@ app.config.errorHandler = (err, vm, info) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 全局警告处理
|
// 全局警告处理
|
||||||
app.config.warnHandler = (msg, vm, trace) => {
|
app.config.warnHandler = (msg, trace) => {
|
||||||
if (msg.includes('ResizeObserver')) {
|
if (msg.includes('ResizeObserver')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import HomePage from '@/views/HomePage.vue'
|
|||||||
import DataPage from '@/views/DataPage.vue'
|
import DataPage from '@/views/DataPage.vue'
|
||||||
import DatasetPage from '@/views/DatasetPage.vue'
|
import DatasetPage from '@/views/DatasetPage.vue'
|
||||||
import PredictPage from '@/views/PredictPage.vue'
|
import PredictPage from '@/views/PredictPage.vue'
|
||||||
import PLSPredictPage from '@/views/PLSPredictPage.vue'
|
|
||||||
import AnalysisPage from '@/views/AnalysisPage.vue'
|
import AnalysisPage from '@/views/AnalysisPage.vue'
|
||||||
import TrainingPage from '@/views/TrainingPage.vue'
|
import TrainingPage from '@/views/TrainingPage.vue'
|
||||||
|
|
||||||
@ -28,11 +27,6 @@ const routes = [
|
|||||||
name: 'Predict',
|
name: 'Predict',
|
||||||
component: PredictPage
|
component: PredictPage
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/pls-predict',
|
|
||||||
name: 'PLSPredict',
|
|
||||||
component: PLSPredictPage
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/analysis',
|
path: '/analysis',
|
||||||
name: 'Analysis',
|
name: 'Analysis',
|
||||||
|
|||||||
@ -71,9 +71,6 @@ import axios from 'axios'
|
|||||||
import { API_BASE_URL } from '@/config'
|
import { API_BASE_URL } from '@/config'
|
||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
|
|
||||||
// 定义组件名称
|
|
||||||
const __name = 'AnalysisPage'
|
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const analysisForm = ref({
|
const analysisForm = ref({
|
||||||
equipment_type: '',
|
equipment_type: '',
|
||||||
|
|||||||
@ -647,7 +647,7 @@ const isNumericParam = (param) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 添加 handleTabClick 函数
|
// 添加 handleTabClick 函数
|
||||||
const handleTabClick = (tab) => {
|
const handleTabClick = () => {
|
||||||
// 切换标签页时重置过滤条件
|
// 切换标签页时重置过滤条件
|
||||||
searchQuery.value = ''
|
searchQuery.value = ''
|
||||||
filterManufacturer.value = ''
|
filterManufacturer.value = ''
|
||||||
|
|||||||
@ -8,15 +8,8 @@
|
|||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<el-card @click="$router.push('/predict')">
|
<el-card @click="$router.push('/predict')">
|
||||||
<el-icon><Money /></el-icon>
|
<el-icon><Money /></el-icon>
|
||||||
<h3>机器学习预测</h3>
|
<h3>成本预测</h3>
|
||||||
<p>基于机器学习模型的成本预测</p>
|
<p>基于机器学习和 PLS 回归模型的成本预测</p>
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="8">
|
|
||||||
<el-card @click="$router.push('/pls-predict')">
|
|
||||||
<el-icon><TrendCharts /></el-icon>
|
|
||||||
<h3>PLS回归预测</h3>
|
|
||||||
<p>基于偏最小二乘回归的成本预测</p>
|
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
@ -34,10 +27,10 @@
|
|||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<el-card @click="$router.push('/data')">
|
<el-card @click="$router.push('/models')">
|
||||||
<el-icon><Management /></el-icon>
|
<el-icon><Management /></el-icon>
|
||||||
<h3>数据管理</h3>
|
<h3>模型管理</h3>
|
||||||
<p>管理装备数据和成本数据</p>
|
<p>管理训练好的模型</p>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
@ -47,13 +40,20 @@
|
|||||||
<p>管理训练和验证数据集</p>
|
<p>管理训练和验证数据集</p>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card @click="$router.push('/data')">
|
||||||
|
<el-icon><Management /></el-icon>
|
||||||
|
<h3>数据管理</h3>
|
||||||
|
<p>管理装备数据和成本数据</p>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Money, DataAnalysis, Monitor, Management, TrendCharts, Collection } from '@element-plus/icons-vue'
|
import { Money, DataAnalysis, Monitor, Management, Collection } from '@element-plus/icons-vue'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@ -8,13 +8,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 模型列表 -->
|
<!-- 模型列表 -->
|
||||||
<el-table :data="models" border style="width: 100%">
|
<el-table :data="modelList" border style="width: 100%">
|
||||||
<el-table-column prop="model_name" label="模型名称"></el-table-column>
|
|
||||||
<el-table-column prop="model_type" label="模型类型">
|
<el-table-column prop="model_type" label="模型类型">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ formatModelType(scope.row.model_type) }}
|
{{ getModelName(scope.row.model_type) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column prop="model_name" label="模型名称"></el-table-column>
|
||||||
<el-table-column prop="equipment_type" label="装备类型"></el-table-column>
|
<el-table-column prop="equipment_type" label="装备类型"></el-table-column>
|
||||||
<el-table-column prop="r2_score" label="R²分数">
|
<el-table-column prop="r2_score" label="R²分数">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
@ -114,7 +114,7 @@ import { API_BASE_URL } from '@/config'
|
|||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const models = ref([])
|
const modelList = ref([])
|
||||||
const selectedModel = ref(null)
|
const selectedModel = ref(null)
|
||||||
const detailsVisible = ref(false)
|
const detailsVisible = ref(false)
|
||||||
const importanceChartRef = ref(null)
|
const importanceChartRef = ref(null)
|
||||||
@ -124,7 +124,7 @@ const importanceChart = ref(null)
|
|||||||
const loadModels = async () => {
|
const loadModels = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${API_BASE_URL}/models`)
|
const response = await axios.get(`${API_BASE_URL}/models`)
|
||||||
models.value = response.data
|
modelList.value = response.data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('获取模型列表失败')
|
ElMessage.error('获取模型列表失败')
|
||||||
}
|
}
|
||||||
@ -243,6 +243,17 @@ onUnmounted(() => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadModels()
|
loadModels()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const getModelName = (modelType) => {
|
||||||
|
const modelNames = {
|
||||||
|
'pls': 'PLS回归',
|
||||||
|
'xgboost': 'XGBoost',
|
||||||
|
'lightgbm': 'LightGBM',
|
||||||
|
'gbm': 'GBM',
|
||||||
|
'rf': 'Random Forest'
|
||||||
|
}
|
||||||
|
return modelNames[modelType] || modelType
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@ -1,243 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="predict-page">
|
|
||||||
<el-card class="predict-card">
|
|
||||||
<template #header>
|
|
||||||
<h2>PLS回归预测</h2>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<el-form :model="formData" label-width="120px">
|
|
||||||
<!-- 装备类型选择 -->
|
|
||||||
<el-form-item label="装备类型">
|
|
||||||
<el-select v-model="formData.type" @change="handleTypeChange">
|
|
||||||
<el-option label="火箭炮" value="火箭炮"></el-option>
|
|
||||||
<el-option label="巡飞弹" value="巡飞弹"></el-option>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<!-- 通用参数 -->
|
|
||||||
<el-form-item label="总长(m)">
|
|
||||||
<el-input-number v-model="formData.length_m" :precision="2"></el-input-number>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="宽度(m)">
|
|
||||||
<el-input-number v-model="formData.width_m" :precision="2"></el-input-number>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="高度(m)">
|
|
||||||
<el-input-number v-model="formData.height_m" :precision="2"></el-input-number>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="重量(kg)">
|
|
||||||
<el-input-number v-model="formData.weight_kg"></el-input-number>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="最大射程(km)">
|
|
||||||
<el-input-number v-model="formData.max_range_km"></el-input-number>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<!-- 火箭炮特有参数 -->
|
|
||||||
<template v-if="formData.type === '火箭炮'">
|
|
||||||
<el-form-item label="方向射界(度)">
|
|
||||||
<el-input-number v-model="formData.firing_angle_horizontal"></el-input-number>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="高低射界(度)">
|
|
||||||
<el-input-number v-model="formData.firing_angle_vertical"></el-input-number>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="火箭弹长度(m)">
|
|
||||||
<el-input-number v-model="formData.rocket_length_m" :precision="2"></el-input-number>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="弹体直径(mm)">
|
|
||||||
<el-input-number v-model="formData.rocket_diameter_mm"></el-input-number>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="火箭弹重量(kg)">
|
|
||||||
<el-input-number v-model="formData.rocket_weight_kg"></el-input-number>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="射速(发/分钟)">
|
|
||||||
<el-input-number v-model="formData.rate_of_fire"></el-input-number>
|
|
||||||
</el-form-item>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 巡飞弹特有参数 -->
|
|
||||||
<template v-if="formData.type === '巡飞弹'">
|
|
||||||
<el-form-item label="最大速度(km/h)">
|
|
||||||
<el-input-number v-model="formData.max_speed_kmh"></el-input-number>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="巡航速度(km/h)">
|
|
||||||
<el-input-number v-model="formData.cruise_speed_kmh"></el-input-number>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="巡飞时间(min)">
|
|
||||||
<el-input-number v-model="formData.flight_time_min"></el-input-number>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="折叠长度(mm)">
|
|
||||||
<el-input-number v-model="formData.folded_length_mm"></el-input-number>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="折叠宽度(mm)">
|
|
||||||
<el-input-number v-model="formData.folded_width_mm"></el-input-number>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="折叠高度(mm)">
|
|
||||||
<el-input-number v-model="formData.folded_height_mm"></el-input-number>
|
|
||||||
</el-form-item>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<el-form-item>
|
|
||||||
<el-button type="primary" @click="submitForm">预测成本</el-button>
|
|
||||||
<el-button @click="resetForm">重置</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<!-- 预测结果 -->
|
|
||||||
<div v-if="predictionResult" class="prediction-result">
|
|
||||||
<h3>预测结果</h3>
|
|
||||||
<el-descriptions border>
|
|
||||||
<el-descriptions-item label="预测成本">
|
|
||||||
{{ formatMoney(predictionResult.predicted_cost) }} 元
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="置信区间">
|
|
||||||
{{ formatMoney(predictionResult.confidence_interval.lower) }} ~
|
|
||||||
{{ formatMoney(predictionResult.confidence_interval.upper) }} 元
|
|
||||||
</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, reactive } from 'vue'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import axios from 'axios'
|
|
||||||
import { API_BASE_URL } from '@/config'
|
|
||||||
|
|
||||||
const formData = reactive({
|
|
||||||
type: '',
|
|
||||||
length_m: null,
|
|
||||||
width_m: null,
|
|
||||||
height_m: null,
|
|
||||||
weight_kg: null,
|
|
||||||
max_range_km: null
|
|
||||||
})
|
|
||||||
|
|
||||||
const predictionResult = ref(null)
|
|
||||||
|
|
||||||
const handleTypeChange = () => {
|
|
||||||
if (formData.type === '火箭炮') {
|
|
||||||
Object.assign(formData, {
|
|
||||||
firing_angle_horizontal: null,
|
|
||||||
firing_angle_vertical: null,
|
|
||||||
rocket_length_m: null,
|
|
||||||
rocket_diameter_mm: null,
|
|
||||||
rocket_weight_kg: null,
|
|
||||||
rate_of_fire: null
|
|
||||||
})
|
|
||||||
} else if (formData.type === '巡飞弹') {
|
|
||||||
Object.assign(formData, {
|
|
||||||
max_speed_kmh: null,
|
|
||||||
cruise_speed_kmh: null,
|
|
||||||
flight_time_min: null,
|
|
||||||
folded_length_mm: null,
|
|
||||||
folded_width_mm: null,
|
|
||||||
folded_height_mm: null
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitForm = async () => {
|
|
||||||
try {
|
|
||||||
// 验证必填字段
|
|
||||||
if (!formData.type) {
|
|
||||||
throw new Error('请选择装备类型')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证通用参数
|
|
||||||
const commonFields = ['length_m', 'width_m', 'height_m', 'weight_kg', 'max_range_km']
|
|
||||||
for (const field of commonFields) {
|
|
||||||
if (!formData[field]) {
|
|
||||||
throw new Error(`请输入${formatFieldName(field)}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证特有参数
|
|
||||||
if (formData.type === '火箭炮') {
|
|
||||||
const rocketFields = [
|
|
||||||
'firing_angle_horizontal', 'firing_angle_vertical',
|
|
||||||
'rocket_length_m', 'rocket_diameter_mm', 'rocket_weight_kg', 'rate_of_fire'
|
|
||||||
]
|
|
||||||
for (const field of rocketFields) {
|
|
||||||
if (!formData[field]) {
|
|
||||||
throw new Error(`请输入${formatFieldName(field)}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (formData.type === '巡飞弹') {
|
|
||||||
const missileFields = [
|
|
||||||
'max_speed_kmh', 'cruise_speed_kmh', 'flight_time_min',
|
|
||||||
'folded_length_mm', 'folded_width_mm', 'folded_height_mm'
|
|
||||||
]
|
|
||||||
for (const field of missileFields) {
|
|
||||||
if (!formData[field]) {
|
|
||||||
throw new Error(`请输入${formatFieldName(field)}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送预测请求
|
|
||||||
const response = await axios.post(`${API_BASE_URL}/pls/predict`, formData)
|
|
||||||
predictionResult.value = response.data
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error(error.message || '预测失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
formData.type = ''
|
|
||||||
formData.length_m = null
|
|
||||||
formData.width_m = null
|
|
||||||
formData.height_m = null
|
|
||||||
formData.weight_kg = null
|
|
||||||
formData.max_range_km = null
|
|
||||||
predictionResult.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatFieldName = (field) => {
|
|
||||||
const nameMap = {
|
|
||||||
'length_m': '总长',
|
|
||||||
'width_m': '宽度',
|
|
||||||
'height_m': '高度',
|
|
||||||
'weight_kg': '重量',
|
|
||||||
'max_range_km': '最大射程',
|
|
||||||
'firing_angle_horizontal': '方向射界',
|
|
||||||
'firing_angle_vertical': '高低射界',
|
|
||||||
'rocket_length_m': '火箭弹长度',
|
|
||||||
'rocket_diameter_mm': '弹体直径',
|
|
||||||
'rocket_weight_kg': '火箭弹重量',
|
|
||||||
'rate_of_fire': '射速',
|
|
||||||
'max_speed_kmh': '最大速度',
|
|
||||||
'cruise_speed_kmh': '巡航速度',
|
|
||||||
'flight_time_min': '巡飞时间',
|
|
||||||
'folded_length_mm': '折叠长度',
|
|
||||||
'folded_width_mm': '折叠宽度',
|
|
||||||
'folded_height_mm': '折叠高度'
|
|
||||||
}
|
|
||||||
return nameMap[field] || field
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化金额
|
|
||||||
const formatMoney = (value) => {
|
|
||||||
return new Intl.NumberFormat('zh-CN', {
|
|
||||||
minimumFractionDigits: 2,
|
|
||||||
maximumFractionDigits: 2
|
|
||||||
}).format(value)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.predict-page {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.predict-card {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
.prediction-result {
|
|
||||||
margin-top: 20px;
|
|
||||||
padding: 20px;
|
|
||||||
background-color: #f5f7fa;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -2,11 +2,11 @@
|
|||||||
<div class="predict-page">
|
<div class="predict-page">
|
||||||
<el-card class="predict-card">
|
<el-card class="predict-card">
|
||||||
<template #header>
|
<template #header>
|
||||||
<h2>装备成本预测</h2>
|
<h2>成本预测</h2>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 装备类型选择 -->
|
|
||||||
<el-form :model="formData" label-width="120px">
|
<el-form :model="formData" label-width="120px">
|
||||||
|
<!-- 装备类型选择 -->
|
||||||
<el-form-item label="装备类型">
|
<el-form-item label="装备类型">
|
||||||
<el-select v-model="formData.type" @change="handleTypeChange">
|
<el-select v-model="formData.type" @change="handleTypeChange">
|
||||||
<el-option label="火箭炮" value="火箭炮"></el-option>
|
<el-option label="火箭炮" value="火箭炮"></el-option>
|
||||||
@ -95,29 +95,60 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<!-- 预测结果 -->
|
<!-- 预测结果 -->
|
||||||
<div v-if="predictionResult" class="prediction-result">
|
<div v-if="predictionResults" class="prediction-results">
|
||||||
<h3>预测结果</h3>
|
<h3>预测结果</h3>
|
||||||
<el-descriptions border>
|
|
||||||
<el-descriptions-item label="预测成本">
|
<!-- 机器学习模型预测结果 -->
|
||||||
{{ formatCurrency(predictionResult.predicted_cost) }}
|
<div class="ml-prediction">
|
||||||
</el-descriptions-item>
|
<h4>机器学习模型预测</h4>
|
||||||
<el-descriptions-item label="置信区间">
|
<el-descriptions border>
|
||||||
{{ formatCurrency(predictionResult.confidence_interval.lower) }} ~
|
<el-descriptions-item label="模型类型">
|
||||||
{{ formatCurrency(predictionResult.confidence_interval.upper) }}
|
{{ getModelName(mlPrediction.model_info.type) }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
<el-descriptions-item label="模型名称">
|
||||||
|
{{ mlPrediction.model_info.name }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="预测成本">
|
||||||
|
{{ formatMoney(mlPrediction.predicted_cost) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="置信区间">
|
||||||
|
{{ formatMoney(mlPrediction.confidence_interval.lower) }} ~
|
||||||
|
{{ formatMoney(mlPrediction.confidence_interval.upper) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PLS回归预测结果 -->
|
||||||
|
<div class="pls-prediction">
|
||||||
|
<h4>PLS回归预测</h4>
|
||||||
|
<el-descriptions border>
|
||||||
|
<el-descriptions-item label="模型类型">
|
||||||
|
{{ getModelName(plsPrediction.model_info.type) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="模型名称">
|
||||||
|
{{ plsPrediction.model_info.name }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="预测成本">
|
||||||
|
{{ formatMoney(plsPrediction.predicted_cost) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="置信区间">
|
||||||
|
{{ formatMoney(plsPrediction.confidence_interval.lower) }} ~
|
||||||
|
{{ formatMoney(plsPrediction.confidence_interval.upper) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, reactive } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { API_BASE_URL } from '@/config'
|
import { API_BASE_URL } from '@/config'
|
||||||
|
|
||||||
const formData = ref({
|
const formData = reactive({
|
||||||
type: '',
|
type: '',
|
||||||
length_m: null,
|
length_m: null,
|
||||||
width_m: null,
|
width_m: null,
|
||||||
@ -126,83 +157,165 @@ const formData = ref({
|
|||||||
max_range_km: null
|
max_range_km: null
|
||||||
})
|
})
|
||||||
|
|
||||||
const predictionResult = ref(null)
|
const predictionResults = ref(null)
|
||||||
|
const mlPrediction = ref(null)
|
||||||
|
const plsPrediction = ref(null)
|
||||||
|
|
||||||
const handleTypeChange = () => {
|
const handleTypeChange = () => {
|
||||||
// 重置特有参数
|
// 重置特有参数
|
||||||
if (formData.value.type === '火箭炮') {
|
if (formData.type === '火箭炮') {
|
||||||
formData.value = {
|
formData.firing_angle_horizontal = null
|
||||||
...formData.value,
|
formData.firing_angle_vertical = null
|
||||||
firing_angle_horizontal: null,
|
formData.rocket_length_m = null
|
||||||
firing_angle_vertical: null,
|
formData.rocket_diameter_mm = null
|
||||||
rocket_length_m: null,
|
formData.rocket_weight_kg = null
|
||||||
rocket_diameter_mm: null,
|
formData.rate_of_fire = null
|
||||||
rocket_weight_kg: null,
|
} else if (formData.type === '巡飞弹') {
|
||||||
rate_of_fire: null
|
formData.max_speed_kmh = null
|
||||||
}
|
formData.cruise_speed_kmh = null
|
||||||
} else if (formData.value.type === '巡飞弹') {
|
formData.flight_time_min = null
|
||||||
formData.value = {
|
formData.warhead_type = ''
|
||||||
...formData.value,
|
formData.launch_mode = ''
|
||||||
max_speed_kmh: null,
|
formData.folded_length_mm = null
|
||||||
cruise_speed_kmh: null,
|
formData.folded_width_mm = null
|
||||||
flight_time_min: null,
|
formData.folded_height_mm = null
|
||||||
warhead_type: '',
|
|
||||||
launch_mode: '',
|
|
||||||
folded_length_mm: null,
|
|
||||||
folded_width_mm: null,
|
|
||||||
folded_height_mm: null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitForm = async () => {
|
const submitForm = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(`${API_BASE_URL}/predict`, formData.value)
|
// 验证必填字段
|
||||||
predictionResult.value = response.data
|
if (!formData.type) {
|
||||||
|
throw new Error('请选择装备类型')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证通用参数
|
||||||
|
const commonFields = ['length_m', 'width_m', 'height_m', 'weight_kg', 'max_range_km']
|
||||||
|
for (const field of commonFields) {
|
||||||
|
if (!formData[field]) {
|
||||||
|
throw new Error(`请输入${formatFieldName(field)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证特有参数
|
||||||
|
if (formData.type === '火箭炮') {
|
||||||
|
const rocketFields = [
|
||||||
|
'firing_angle_horizontal', 'firing_angle_vertical',
|
||||||
|
'rocket_length_m', 'rocket_diameter_mm', 'rocket_weight_kg', 'rate_of_fire'
|
||||||
|
]
|
||||||
|
for (const field of rocketFields) {
|
||||||
|
if (!formData[field]) {
|
||||||
|
throw new Error(`请输入${formatFieldName(field)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (formData.type === '巡飞弹') {
|
||||||
|
const missileFields = [
|
||||||
|
'max_speed_kmh', 'cruise_speed_kmh', 'flight_time_min',
|
||||||
|
'folded_length_mm', 'folded_width_mm', 'folded_height_mm'
|
||||||
|
]
|
||||||
|
for (const field of missileFields) {
|
||||||
|
if (!formData[field]) {
|
||||||
|
throw new Error(`请输入${formatFieldName(field)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取预测结果
|
||||||
|
const [mlResponse, plsResponse] = await Promise.all([
|
||||||
|
axios.post(`${API_BASE_URL}/predict`, formData),
|
||||||
|
axios.post(`${API_BASE_URL}/pls/predict`, formData)
|
||||||
|
])
|
||||||
|
|
||||||
|
mlPrediction.value = mlResponse.data
|
||||||
|
plsPrediction.value = plsResponse.data
|
||||||
|
predictionResults.value = true
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(error.response?.data?.error || '预测失败')
|
ElMessage.error(error.message || '预测失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
formData.value = {
|
formData.type = ''
|
||||||
type: '',
|
formData.length_m = null
|
||||||
length_m: null,
|
formData.width_m = null
|
||||||
width_m: null,
|
formData.height_m = null
|
||||||
height_m: null,
|
formData.weight_kg = null
|
||||||
weight_kg: null,
|
formData.max_range_km = null
|
||||||
max_range_km: null
|
predictionResults.value = null
|
||||||
}
|
mlPrediction.value = null
|
||||||
predictionResult.value = null
|
plsPrediction.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatCurrency = (value) => {
|
const formatFieldName = (field) => {
|
||||||
|
const nameMap = {
|
||||||
|
'length_m': '总长',
|
||||||
|
'width_m': '宽度',
|
||||||
|
'height_m': '高度',
|
||||||
|
'weight_kg': '重量',
|
||||||
|
'max_range_km': '最大射程',
|
||||||
|
'firing_angle_horizontal': '方向射界',
|
||||||
|
'firing_angle_vertical': '高低射界',
|
||||||
|
'rocket_length_m': '火箭弹长度',
|
||||||
|
'rocket_diameter_mm': '弹体直径',
|
||||||
|
'rocket_weight_kg': '火箭弹重量',
|
||||||
|
'rate_of_fire': '射速',
|
||||||
|
'max_speed_kmh': '最大速度',
|
||||||
|
'cruise_speed_kmh': '巡航速度',
|
||||||
|
'flight_time_min': '巡飞时间',
|
||||||
|
'folded_length_mm': '折叠长度',
|
||||||
|
'folded_width_mm': '折叠宽度',
|
||||||
|
'folded_height_mm': '折叠高度'
|
||||||
|
}
|
||||||
|
return nameMap[field] || field
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatMoney = (value) => {
|
||||||
return new Intl.NumberFormat('zh-CN', {
|
return new Intl.NumberFormat('zh-CN', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: 'CNY'
|
currency: 'CNY'
|
||||||
}).format(value)
|
}).format(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getModelName = (modelType) => {
|
||||||
|
const modelNames = {
|
||||||
|
'pls': 'PLS回归',
|
||||||
|
'xgboost': 'XGBoost',
|
||||||
|
'lightgbm': 'LightGBM',
|
||||||
|
'gbm': 'GBM',
|
||||||
|
'rf': 'Random Forest'
|
||||||
|
}
|
||||||
|
return modelNames[modelType] || modelType
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style scoped>
|
||||||
.predict-page {
|
.predict-page {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.predict-card {
|
.predict-card {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
h2 {
|
.prediction-results {
|
||||||
text-align: center;
|
margin-top: 20px;
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.prediction-result {
|
.ml-prediction, .pls-prediction {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background-color: #f5f7fa;
|
background-color: #f5f7fa;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-descriptions {
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -6,52 +6,49 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 训练配置 -->
|
<!-- 训练配置 -->
|
||||||
<el-form :model="formData" label-width="120px">
|
<el-form :model="trainingConfig" label-width="120px">
|
||||||
<el-form-item label="装备类型" required>
|
<el-form-item label="装备类型">
|
||||||
<el-select v-model="formData.type" @change="handleTypeChange">
|
<el-select v-model="trainingConfig.type" placeholder="选择装备类型">
|
||||||
<el-option label="火箭炮" value="火箭炮"></el-option>
|
<el-option label="火箭炮" value="火箭炮" />
|
||||||
<el-option label="巡飞弹" value="巡飞弹"></el-option>
|
<el-option label="巡飞弹" value="巡飞弹" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<!-- 选择训练集 -->
|
<el-form-item label="训练数据集">
|
||||||
<el-form-item label="训练数据集" required>
|
<el-select v-model="trainingConfig.train_dataset_id" placeholder="选择训练数据集">
|
||||||
<el-select v-model="formData.train_dataset_id" placeholder="选择训练数据集">
|
|
||||||
<el-option
|
<el-option
|
||||||
v-for="dataset in trainingDatasets"
|
v-for="dataset in trainingDatasets"
|
||||||
:key="dataset.id"
|
:key="dataset.id"
|
||||||
:label="dataset.name"
|
:label="dataset.name"
|
||||||
:value="dataset.id"
|
:value="dataset.id"
|
||||||
></el-option>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<!-- 选择验证集 -->
|
|
||||||
<el-form-item label="验证数据集">
|
<el-form-item label="验证数据集">
|
||||||
<el-select v-model="formData.validation_dataset_id" placeholder="选择验证数据集" clearable>
|
<el-select v-model="trainingConfig.validation_dataset_id" placeholder="选择验证数据集">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="dataset in validationDatasets"
|
v-for="dataset in validationDatasets"
|
||||||
:key="dataset.id"
|
:key="dataset.id"
|
||||||
:label="dataset.name"
|
:label="dataset.name"
|
||||||
:value="dataset.id"
|
:value="dataset.id"
|
||||||
></el-option>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<!-- 模型选择 -->
|
<el-form-item label="选择模型">
|
||||||
<el-form-item label="训练模型" required>
|
<el-checkbox-group v-model="trainingConfig.models">
|
||||||
<el-checkbox-group v-model="formData.models">
|
<el-checkbox label="pls" disabled>PLS回归</el-checkbox>
|
||||||
<el-checkbox label="xgboost">XGBoost</el-checkbox>
|
<el-checkbox label="xgboost" checked>XGBoost</el-checkbox>
|
||||||
<el-checkbox label="lightgbm">LightGBM</el-checkbox>
|
<el-checkbox label="lightgbm" checked>LightGBM</el-checkbox>
|
||||||
<el-checkbox label="gbdt">GBDT</el-checkbox>
|
<el-checkbox label="gbm" checked>GBM</el-checkbox>
|
||||||
<el-checkbox label="rf">Random Forest</el-checkbox>
|
<el-checkbox label="rf" checked>Random Forest</el-checkbox>
|
||||||
</el-checkbox-group>
|
</el-checkbox-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<!-- 开始训练按钮 -->
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="startTraining" :loading="training">
|
<el-button type="primary" @click="startTraining" :loading="isTraining">
|
||||||
{{ training ? '训练中...' : '开始训练' }}
|
开始训练
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
@ -60,44 +57,56 @@
|
|||||||
<div v-if="trainingResult" class="training-result">
|
<div v-if="trainingResult" class="training-result">
|
||||||
<h3>训练结果</h3>
|
<h3>训练结果</h3>
|
||||||
|
|
||||||
<!-- 模型评估指标 -->
|
<!-- 最佳模型信息 -->
|
||||||
<el-table :data="modelMetrics" border style="width: 100%">
|
<div class="best-model-info" v-if="trainingResult.best_model">
|
||||||
<el-table-column prop="model" label="模型">
|
<h4>最佳模型: {{ getModelName(trainingResult.best_model.type) }}</h4>
|
||||||
|
<p>R²分数: {{ formatNumber(trainingResult.best_model.r2) }}</p>
|
||||||
|
<p>MAE: {{ formatNumber(trainingResult.best_model.mae) }} 元</p>
|
||||||
|
<p>RMSE: {{ formatNumber(trainingResult.best_model.rmse) }} 元</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 所有模型评估结果 -->
|
||||||
|
<el-table :data="modelResults" border style="width: 100%; margin-top: 20px;">
|
||||||
|
<el-table-column prop="model" label="模型" width="120">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ formatModelName(scope.row.model) }}
|
{{ getModelName(scope.row.model) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- 训练集评估 -->
|
||||||
<el-table-column label="训练集评估">
|
<el-table-column label="训练集评估">
|
||||||
<el-table-column prop="train.r2" label="R²分数">
|
<el-table-column prop="train.r2" label="R²分数" width="120">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ scope.row.train.r2.toFixed(4) }}
|
{{ formatNumber(scope.row.train.r2) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="train.mae" label="MAE (元)">
|
<el-table-column prop="train.mae" label="MAE (元)" width="150">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ scope.row.train.mae.toFixed(2) }}
|
{{ formatNumber(scope.row.train.mae) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="train.rmse" label="RMSE (元)">
|
<el-table-column prop="train.rmse" label="RMSE (元)" width="150">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ scope.row.train.rmse.toFixed(2) }}
|
{{ formatNumber(scope.row.train.rmse) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="验证集评估" v-if="formData.validation_dataset_id">
|
|
||||||
<el-table-column prop="validation.r2" label="R²分数">
|
<!-- 验证集评估 -->
|
||||||
|
<el-table-column label="验证集评估">
|
||||||
|
<el-table-column prop="validation.r2" label="R²分数" width="120">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ scope.row.validation.r2.toFixed(4) }}
|
{{ formatNumber(scope.row.validation.r2) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="validation.mae" label="MAE (元)">
|
<el-table-column prop="validation.mae" label="MAE (元)" width="150">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ scope.row.validation.mae.toFixed(2) }}
|
{{ formatNumber(scope.row.validation.mae) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="validation.rmse" label="RMSE (元)">
|
<el-table-column prop="validation.rmse" label="RMSE (元)" width="150">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ scope.row.validation.rmse.toFixed(2) }}
|
{{ formatNumber(scope.row.validation.rmse) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@ -106,148 +115,142 @@
|
|||||||
<!-- 特征重要性 -->
|
<!-- 特征重要性 -->
|
||||||
<div v-if="trainingResult.feature_importance" class="feature-importance">
|
<div v-if="trainingResult.feature_importance" class="feature-importance">
|
||||||
<h4>特征重要性</h4>
|
<h4>特征重要性</h4>
|
||||||
<el-table :data="featureImportanceData" border style="width: 100%">
|
<el-table
|
||||||
<el-table-column prop="feature" label="特征"></el-table-column>
|
:data="featureImportanceData"
|
||||||
<el-table-column prop="importance" label="重要性">
|
border
|
||||||
|
style="width: 100%; margin-top: 10px;"
|
||||||
|
>
|
||||||
|
<el-table-column prop="feature" label="特征" width="180" />
|
||||||
|
<el-table-column prop="importance" label="重要性" width="120">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-progress
|
{{ formatNumber(scope.row.importance) }}
|
||||||
:percentage="scope.row.importance * 100"
|
|
||||||
:format="format => format.toFixed(2) + '%'"
|
|
||||||
:color="getImportanceColor(scope.row.importance)"
|
|
||||||
></el-progress>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 最佳模型信息 -->
|
|
||||||
<div v-if="trainingResult.best_model" class="best-model">
|
|
||||||
<h4>最佳模型</h4>
|
|
||||||
<el-descriptions :column="2" border>
|
|
||||||
<el-descriptions-item label="模型类型">
|
|
||||||
{{ formatModelName(trainingResult.best_model.type) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="R²分数">
|
|
||||||
{{ trainingResult.best_model.r2.toFixed(4) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="MAE">
|
|
||||||
{{ formatMoney(trainingResult.best_model.mae) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="RMSE">
|
|
||||||
{{ formatMoney(trainingResult.best_model.rmse) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { API_BASE_URL } from '@/config'
|
import { API_BASE_URL } from '@/config'
|
||||||
|
|
||||||
// 响应式数据
|
// 训练配置
|
||||||
const formData = ref({
|
const trainingConfig = ref({
|
||||||
type: '',
|
type: '',
|
||||||
train_dataset_id: null,
|
train_dataset_id: null,
|
||||||
validation_dataset_id: null,
|
validation_dataset_id: null,
|
||||||
models: ['xgboost', 'lightgbm', 'gbdt', 'rf']
|
models: ['pls']
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 数据集列表
|
||||||
const trainingDatasets = ref([])
|
const trainingDatasets = ref([])
|
||||||
const validationDatasets = ref([])
|
const validationDatasets = ref([])
|
||||||
const training = ref(false)
|
|
||||||
|
// 训练状态和结果
|
||||||
|
const isTraining = ref(false)
|
||||||
const trainingResult = ref(null)
|
const trainingResult = ref(null)
|
||||||
|
|
||||||
// 加载数据集列表
|
// 加载数据集
|
||||||
const loadDatasets = async (type) => {
|
const loadDatasets = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${API_BASE_URL}/datasets`, {
|
// 加载训练数据集
|
||||||
params: {
|
const trainResponse = await axios.get(
|
||||||
equipment_type: type,
|
`${API_BASE_URL}/datasets`,
|
||||||
purpose: '训练' // 默认加载训练集
|
{ params: { equipment_type: trainingConfig.value.type, purpose: '训练' } }
|
||||||
}
|
)
|
||||||
})
|
trainingDatasets.value = trainResponse.data
|
||||||
trainingDatasets.value = response.data
|
|
||||||
|
// 加载验证数据集
|
||||||
|
const valResponse = await axios.get(
|
||||||
|
`${API_BASE_URL}/datasets`,
|
||||||
|
{ params: { equipment_type: trainingConfig.value.type, purpose: '验证' } }
|
||||||
|
)
|
||||||
|
validationDatasets.value = valResponse.data
|
||||||
|
|
||||||
// 加载验证集
|
|
||||||
const validationResponse = await axios.get(`${API_BASE_URL}/datasets`, {
|
|
||||||
params: {
|
|
||||||
equipment_type: type,
|
|
||||||
purpose: '验证'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
validationDatasets.value = validationResponse.data
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('获取数据集列表失败')
|
ElMessage.error('加载数据集失败')
|
||||||
|
console.error('Error loading datasets:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理装备类型变化
|
// 监听装备类型变化
|
||||||
const handleTypeChange = () => {
|
watch(() => trainingConfig.value.type, (newType) => {
|
||||||
formData.value.train_dataset_id = null
|
if (newType) {
|
||||||
formData.value.validation_dataset_id = null
|
loadDatasets()
|
||||||
loadDatasets(formData.value.type)
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
// 开始训练
|
// 开始训练
|
||||||
const startTraining = async () => {
|
const startTraining = async () => {
|
||||||
try {
|
try {
|
||||||
// 验证输入
|
// 验证配置
|
||||||
if (!formData.value.type) {
|
if (!trainingConfig.value.type) {
|
||||||
throw new Error('请选择装备类型')
|
ElMessage.warning('请选择装备类型')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if (!formData.value.train_dataset_id) {
|
if (!trainingConfig.value.train_dataset_id) {
|
||||||
throw new Error('请选择训练数据集')
|
ElMessage.warning('请选择训练数据集')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if (formData.value.models.length === 0) {
|
if (!trainingConfig.value.validation_dataset_id) {
|
||||||
throw new Error('请至少选择一个训练模型')
|
ElMessage.warning('请选择验证数据集')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (trainingConfig.value.models.length === 0) {
|
||||||
|
ElMessage.warning('请至少选择一个模型')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
training.value = true
|
isTraining.value = true
|
||||||
|
|
||||||
// 发送训练请求
|
// 发送训练请求
|
||||||
const response = await axios.post(`${API_BASE_URL}/train`, {
|
const response = await axios.post(`${API_BASE_URL}/train`, trainingConfig.value)
|
||||||
type: formData.value.type,
|
|
||||||
train_dataset_id: formData.value.train_dataset_id,
|
if (response.data.error) {
|
||||||
validation_dataset_id: formData.value.validation_dataset_id,
|
throw new Error(response.data.error)
|
||||||
models: formData.value.models
|
}
|
||||||
})
|
|
||||||
|
|
||||||
trainingResult.value = response.data
|
trainingResult.value = response.data
|
||||||
ElMessage.success('训练完成')
|
ElMessage.success('训练完成')
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Training error:', error)
|
|
||||||
ElMessage.error(error.message || '训练失败')
|
ElMessage.error(error.message || '训练失败')
|
||||||
|
console.error('Training error:', error)
|
||||||
} finally {
|
} finally {
|
||||||
training.value = false
|
isTraining.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化模型名称
|
// 格式化数字
|
||||||
const formatModelName = (name) => {
|
const formatNumber = (value) => {
|
||||||
const nameMap = {
|
if (value === null || value === undefined) return '-'
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
if (Math.abs(value) >= 1000) {
|
||||||
|
return value.toLocaleString('zh-CN', { maximumFractionDigits: 2 })
|
||||||
|
}
|
||||||
|
return value.toFixed(4)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取模型中文名称
|
||||||
|
const getModelName = (modelType) => {
|
||||||
|
const modelNames = {
|
||||||
'xgboost': 'XGBoost',
|
'xgboost': 'XGBoost',
|
||||||
'lightgbm': 'LightGBM',
|
'lightgbm': 'LightGBM',
|
||||||
'gbdt': 'GBDT',
|
'gbm': 'GBM',
|
||||||
'rf': 'Random Forest'
|
'rf': 'Random Forest'
|
||||||
}
|
}
|
||||||
return nameMap[name] || name
|
return modelNames[modelType] || modelType
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取重要性颜色
|
// 处理模型结果数据
|
||||||
const getImportanceColor = (value) => {
|
const modelResults = computed(() => {
|
||||||
if (value >= 0.5) return '#67C23A' // 高重要性
|
|
||||||
if (value >= 0.2) return '#E6A23C' // 中等重要性
|
|
||||||
return '#F56C6C' // 低重要性
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算模型评估指标数据
|
|
||||||
const modelMetrics = computed(() => {
|
|
||||||
if (!trainingResult.value?.metrics) return []
|
if (!trainingResult.value?.metrics) return []
|
||||||
|
|
||||||
return Object.entries(trainingResult.value.metrics).map(([model, metrics]) => ({
|
return Object.entries(trainingResult.value.metrics).map(([model, metrics]) => ({
|
||||||
@ -257,15 +260,74 @@ const modelMetrics = computed(() => {
|
|||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
// 格式化金额
|
// 特征名称映射
|
||||||
const formatMoney = (value) => {
|
const featureNameMap = {
|
||||||
if (value === null || value === undefined) return '-'
|
// 通用参数
|
||||||
return `${value.toFixed(2)} 元 (预测误差)`
|
'length_m': '总长(m)',
|
||||||
|
'width_m': '宽度(m)',
|
||||||
|
'height_m': '高度(m)',
|
||||||
|
'weight_kg': '重量(kg)',
|
||||||
|
'max_range_km': '最大射程(km)',
|
||||||
|
|
||||||
|
// 火箭炮特有参数
|
||||||
|
'firing_angle_horizontal': '方向射界(度)',
|
||||||
|
'firing_angle_vertical': '高低射界(度)',
|
||||||
|
'rocket_length_m': '火箭弹长度(m)',
|
||||||
|
'rocket_diameter_mm': '口径(mm)',
|
||||||
|
'rocket_weight_kg': '火箭弹重量(kg)',
|
||||||
|
'rate_of_fire': '射速(发/分)',
|
||||||
|
'combat_weight_kg': '战斗重量(kg)',
|
||||||
|
'speed_kmh': '速度(km/h)',
|
||||||
|
'min_range_km': '最小射程(km)',
|
||||||
|
'power_hp': '功率(hp)',
|
||||||
|
|
||||||
|
// 火箭炮衍生特征
|
||||||
|
'fire_density': '火力密度',
|
||||||
|
'mobility_index': '机动性指标',
|
||||||
|
'range_ratio': '射程比',
|
||||||
|
'power_weight_ratio': '功重比',
|
||||||
|
'volume_density': '体积密度',
|
||||||
|
|
||||||
|
// 巡飞弹特有参数
|
||||||
|
'wingspan_m': '翼展(m)',
|
||||||
|
'warhead_weight_kg': '战斗部重量(kg)',
|
||||||
|
'max_speed_ms': '最大速度(m/s)',
|
||||||
|
'cruise_speed_kmh': '巡航速度(km/h)',
|
||||||
|
'flight_time_min': '巡飞时间(min)',
|
||||||
|
'folded_length_mm': '折叠长度(mm)',
|
||||||
|
'folded_width_mm': '折叠宽度(mm)',
|
||||||
|
'folded_height_mm': '折叠高度(mm)',
|
||||||
|
|
||||||
|
// 巡飞弹衍生特征
|
||||||
|
'warhead_ratio': '战斗部比重',
|
||||||
|
'speed_ratio': '速度比',
|
||||||
|
'range_time_ratio': '射程时间比',
|
||||||
|
'aspect_ratio': '展弦比'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理特征重要性数据
|
||||||
|
const featureImportanceData = computed(() => {
|
||||||
|
if (!trainingResult.value?.feature_importance || !trainingResult.value?.feature_names) return []
|
||||||
|
|
||||||
|
// 创建特征重要性数组
|
||||||
|
const data = trainingResult.value.feature_importance
|
||||||
|
.map((importance, index) => ({
|
||||||
|
feature: featureNameMap[trainingResult.value.feature_names[index]] || trainingResult.value.feature_names[index],
|
||||||
|
importance
|
||||||
|
}))
|
||||||
|
// 过滤掉重要性为0的特征
|
||||||
|
.filter(item => item.importance > 0)
|
||||||
|
// 按重要性降序排序
|
||||||
|
.sort((a, b) => b.importance - a.importance)
|
||||||
|
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 可以在这里加载默认数据
|
if (trainingConfig.value.type) {
|
||||||
|
loadDatasets()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -274,21 +336,35 @@ onMounted(() => {
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
|
||||||
.training-card {
|
.training-card {
|
||||||
max-width: 800px;
|
.training-result {
|
||||||
margin: 0 auto;
|
margin-top: 20px;
|
||||||
}
|
|
||||||
|
|
||||||
.training-result {
|
.best-model-info {
|
||||||
margin-top: 20px;
|
background-color: #f5f7fa;
|
||||||
padding: 20px;
|
padding: 15px;
|
||||||
background-color: #f5f7fa;
|
border-radius: 4px;
|
||||||
border-radius: 4px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3, h4 {
|
.feature-importance {
|
||||||
margin: 20px 0;
|
margin-top: 20px;
|
||||||
padding-left: 10px;
|
|
||||||
border-left: 4px solid #409EFF;
|
.importance-bar {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.importance-value {
|
||||||
|
background-color: #409eff;
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: right;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
66
run.py
66
run.py
@ -1,61 +1,13 @@
|
|||||||
import os
|
from src.app import create_app
|
||||||
import logging
|
import logging
|
||||||
from src.app import app
|
|
||||||
|
|
||||||
# 确保必要的目录存在
|
# 创建应用实例
|
||||||
def ensure_directories():
|
app = create_app()
|
||||||
"""
|
|
||||||
确保所有必要的目录都存在
|
|
||||||
"""
|
|
||||||
directories = [
|
|
||||||
'logs',
|
|
||||||
'data',
|
|
||||||
'models',
|
|
||||||
'uploads'
|
|
||||||
]
|
|
||||||
|
|
||||||
for directory in directories:
|
if __name__ == '__main__':
|
||||||
os.makedirs(directory, exist_ok=True)
|
# 设置日志
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logging.info('=== Server Starting ===')
|
||||||
|
logging.info('Initializing directories...')
|
||||||
|
|
||||||
# 配置日志
|
app.run(host='0.0.0.0', port=5001, debug=True)
|
||||||
def setup_logging():
|
|
||||||
"""
|
|
||||||
配置日志系统
|
|
||||||
"""
|
|
||||||
logging.basicConfig(
|
|
||||||
filename='logs/server.log',
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
|
||||||
)
|
|
||||||
|
|
||||||
# 同时输出到控制台
|
|
||||||
console_handler = logging.StreamHandler()
|
|
||||||
console_handler.setLevel(logging.INFO)
|
|
||||||
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
|
||||||
console_handler.setFormatter(formatter)
|
|
||||||
logging.getLogger('').addHandler(console_handler)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
try:
|
|
||||||
# 初始化目录
|
|
||||||
ensure_directories()
|
|
||||||
|
|
||||||
# 设置日志
|
|
||||||
setup_logging()
|
|
||||||
|
|
||||||
# 记录启动信息
|
|
||||||
logging.info("=== Server Starting ===")
|
|
||||||
logging.info("Initializing directories...")
|
|
||||||
logging.info("Setting up logging system...")
|
|
||||||
|
|
||||||
# 启动服务器
|
|
||||||
app.run(
|
|
||||||
host='localhost',
|
|
||||||
port=5001,
|
|
||||||
debug=True,
|
|
||||||
use_reloader=False # 禁用重载器以避免模型重复加载
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Server failed to start: {str(e)}")
|
|
||||||
raise
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
from flask import Flask, request, jsonify
|
from flask import Flask, request, jsonify
|
||||||
from .model_training import ModelTrainer
|
from .model_trainer import ModelTrainer
|
||||||
from .cost_prediction import CostPredictor
|
from .cost_prediction import CostPredictor
|
||||||
from .feature_analysis import FeatureAnalysis
|
from .feature_analysis import FeatureAnalysis
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|||||||
84
src/app.py
84
src/app.py
@ -1,68 +1,50 @@
|
|||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
from .routes import api_bp
|
from .routes import api_bp
|
||||||
|
from .logger import setup_logger
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 获取logger
|
||||||
|
logger = setup_logger(__name__)
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
"""
|
"""
|
||||||
创建并配置Flask应用
|
创建并配置Flask应用
|
||||||
"""
|
"""
|
||||||
app = Flask(__name__)
|
try:
|
||||||
|
# 创建必要的目录
|
||||||
|
os.makedirs('logs', exist_ok=True)
|
||||||
|
os.makedirs('data', exist_ok=True)
|
||||||
|
os.makedirs('models', exist_ok=True)
|
||||||
|
|
||||||
# 配置跨域
|
logger.info("=== Server Starting ===")
|
||||||
CORS(app)
|
logger.info("Initializing directories...")
|
||||||
|
|
||||||
# 配置日志
|
# 创建Flask应用
|
||||||
setup_logging()
|
app = Flask(__name__)
|
||||||
|
|
||||||
# 注册蓝图
|
# 配置CORS
|
||||||
app.register_blueprint(api_bp, url_prefix='/api')
|
CORS(app)
|
||||||
|
logger.info("CORS enabled")
|
||||||
|
|
||||||
# 错误处理
|
# 注册API蓝图
|
||||||
@app.errorhandler(404)
|
app.register_blueprint(api_bp, url_prefix='/api')
|
||||||
def not_found_error(error):
|
logger.info("API blueprint registered")
|
||||||
logging.error(f"404 error: {error}")
|
|
||||||
return {'error': 'Resource not found'}, 404
|
|
||||||
|
|
||||||
@app.errorhandler(500)
|
# 配置数据库连接
|
||||||
def internal_error(error):
|
app.config['MYSQL_HOST'] = 'localhost'
|
||||||
logging.error(f"500 error: {error}")
|
app.config['MYSQL_USER'] = 'root'
|
||||||
return {'error': 'Internal server error'}, 500
|
app.config['MYSQL_PASSWORD'] = '123456'
|
||||||
|
app.config['MYSQL_DB'] = 'equipment_cost_db'
|
||||||
|
|
||||||
@app.errorhandler(Exception)
|
logger.info("Starting server...")
|
||||||
def handle_exception(error):
|
|
||||||
logging.error(f"Unhandled exception: {error}", exc_info=True)
|
|
||||||
return {'error': str(error)}, 500
|
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
def setup_logging():
|
except Exception as e:
|
||||||
"""
|
logger.error(f"Error creating app: {str(e)}")
|
||||||
配置日志系统
|
raise
|
||||||
"""
|
|
||||||
# 确保日志目录存在
|
|
||||||
os.makedirs('logs', exist_ok=True)
|
|
||||||
|
|
||||||
# 配置日志格式
|
if __name__ == '__main__':
|
||||||
logging.basicConfig(
|
app = create_app()
|
||||||
filename='logs/api.log',
|
app.run(host='localhost', port=5001)
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
|
||||||
)
|
|
||||||
|
|
||||||
# 同时输出到控制台
|
|
||||||
console_handler = logging.StreamHandler()
|
|
||||||
console_handler.setLevel(logging.INFO)
|
|
||||||
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
|
||||||
console_handler.setFormatter(formatter)
|
|
||||||
logging.getLogger('').addHandler(console_handler)
|
|
||||||
|
|
||||||
app = create_app()
|
|
||||||
|
|
||||||
@app.route('/health')
|
|
||||||
def health_check():
|
|
||||||
"""
|
|
||||||
健康检查端点
|
|
||||||
"""
|
|
||||||
return {'status': 'ok'}
|
|
||||||
@ -10,6 +10,9 @@ from .feature_analysis import FeatureAnalysis
|
|||||||
import logging
|
import logging
|
||||||
from src.model_trainer import ModelTrainer
|
from src.model_trainer import ModelTrainer
|
||||||
from src.database import get_db_connection
|
from src.database import get_db_connection
|
||||||
|
from .logger import setup_logger
|
||||||
|
|
||||||
|
logger = setup_logger(__name__)
|
||||||
|
|
||||||
class CostPredictor:
|
class CostPredictor:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -33,7 +36,7 @@ class CostPredictor:
|
|||||||
|
|
||||||
def load_model(self):
|
def load_model(self):
|
||||||
"""
|
"""
|
||||||
加载预训练模型和标准化器
|
加载预训练型和标准化器
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
model_dir = 'models'
|
model_dir = 'models'
|
||||||
@ -142,12 +145,13 @@ class CostPredictor:
|
|||||||
|
|
||||||
def predict(self, data):
|
def predict(self, data):
|
||||||
"""
|
"""
|
||||||
预测成本
|
使用训练好的最优模型进行预测
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
logger.info(f"Starting prediction for {data.get('type')}")
|
||||||
equipment_type = data.get('type')
|
equipment_type = data.get('type')
|
||||||
|
|
||||||
# 加载模型
|
# 加载已训练的最优模型
|
||||||
trainer = ModelTrainer()
|
trainer = ModelTrainer()
|
||||||
if not trainer.load_model(equipment_type):
|
if not trainer.load_model(equipment_type):
|
||||||
raise ValueError(f"No trained model found for {equipment_type}")
|
raise ValueError(f"No trained model found for {equipment_type}")
|
||||||
@ -160,23 +164,22 @@ class CostPredictor:
|
|||||||
y_pred = trainer.predict(X)
|
y_pred = trainer.predict(X)
|
||||||
|
|
||||||
# 计算置信区间
|
# 计算置信区间
|
||||||
confidence_interval = self._calculate_confidence_interval(y_pred[0])
|
confidence_interval = trainer._calculate_confidence_interval(y_pred[0])
|
||||||
|
|
||||||
# 确保预测值和置信区间都是正数且合理的范围
|
# 获取模型类型
|
||||||
predicted_cost = max(1000, float(y_pred[0])) # 最小值设为1000元
|
model_type = trainer.get_model_type()
|
||||||
lower_bound = max(1000, float(confidence_interval[0]))
|
|
||||||
upper_bound = max(predicted_cost * 1.2, float(confidence_interval[1])) # 至少比预测值大20%
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'predicted_cost': predicted_cost,
|
'predicted_cost': float(y_pred[0]),
|
||||||
|
'model_type': model_type, # 返回使用的模型类型
|
||||||
'confidence_interval': {
|
'confidence_interval': {
|
||||||
'lower': lower_bound,
|
'lower': float(confidence_interval[0]),
|
||||||
'upper': upper_bound
|
'upper': float(confidence_interval[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Prediction error: {str(e)}")
|
logger.error(f"Prediction error: {str(e)}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _calculate_confidence_interval(self, prediction, confidence=0.95):
|
def _calculate_confidence_interval(self, prediction, confidence=0.95):
|
||||||
@ -216,3 +219,38 @@ class CostPredictor:
|
|||||||
'rmse': float(np.sqrt(mean_squared_error(y_true, y_pred))),
|
'rmse': float(np.sqrt(mean_squared_error(y_true, y_pred))),
|
||||||
'r2': float(r2_score(y_true, y_pred))
|
'r2': float(r2_score(y_true, y_pred))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def predict_pls(self, data):
|
||||||
|
"""
|
||||||
|
使用 PLS 模型预测成本
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info(f"Starting PLS prediction for {data.get('type')}")
|
||||||
|
equipment_type = data.get('type')
|
||||||
|
|
||||||
|
# 加载 PLS 模型
|
||||||
|
trainer = ModelTrainer()
|
||||||
|
if not trainer.load_model(equipment_type, model_type='pls'): # 指定加载 PLS 模型
|
||||||
|
raise ValueError(f"No trained PLS model found for {equipment_type}")
|
||||||
|
|
||||||
|
# 准备特征数据
|
||||||
|
features = self.feature_analyzer.get_equipment_specific_features(equipment_type)
|
||||||
|
X = np.array([[data.get(feature) for feature in features]])
|
||||||
|
|
||||||
|
# 预测
|
||||||
|
y_pred = trainer.predict(X)
|
||||||
|
|
||||||
|
# 计算置信区间
|
||||||
|
confidence_interval = trainer._calculate_confidence_interval(y_pred[0])
|
||||||
|
|
||||||
|
return {
|
||||||
|
'predicted_cost': float(y_pred[0]),
|
||||||
|
'confidence_interval': {
|
||||||
|
'lower': float(confidence_interval[0]),
|
||||||
|
'upper': float(confidence_interval[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"PLS prediction error: {str(e)}")
|
||||||
|
raise
|
||||||
@ -3,6 +3,9 @@ import openpyxl
|
|||||||
from openpyxl.styles import PatternFill, Font, Alignment
|
from openpyxl.styles import PatternFill, Font, Alignment
|
||||||
from openpyxl.worksheet.datavalidation import DataValidation
|
from openpyxl.worksheet.datavalidation import DataValidation
|
||||||
import os
|
import os
|
||||||
|
from .logger import setup_logger
|
||||||
|
|
||||||
|
logger = setup_logger(__name__)
|
||||||
|
|
||||||
def create_excel_template():
|
def create_excel_template():
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -13,6 +13,9 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
from src.database.db_connection import get_db_connection
|
from src.database.db_connection import get_db_connection
|
||||||
from sklearn.metrics import mean_absolute_error, mean_squared_error
|
from sklearn.metrics import mean_absolute_error, mean_squared_error
|
||||||
|
from .logger import setup_logger
|
||||||
|
|
||||||
|
logger = setup_logger(__name__)
|
||||||
|
|
||||||
class DataPreparation:
|
class DataPreparation:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -25,13 +28,13 @@ class DataPreparation:
|
|||||||
准备训练数据
|
准备训练数据
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logging.info(f"Preparing training data for {equipment_type}")
|
logger.info(f"Preparing training data for {equipment_type}")
|
||||||
logging.info(f"Raw data size: {len(equipment_data)}")
|
logger.info(f"Raw data size: {len(equipment_data)}")
|
||||||
|
|
||||||
# 如果输入已经是 numpy 数组,直接返回
|
# 如果输入已经是 numpy 数组,直接返回
|
||||||
if isinstance(equipment_data, np.ndarray):
|
if isinstance(equipment_data, np.ndarray):
|
||||||
X = equipment_data
|
X = equipment_data
|
||||||
logging.info(f"Input is already numpy array with shape: {X.shape}")
|
logger.info(f"Input is already numpy array with shape: {X.shape}")
|
||||||
|
|
||||||
# 处理无效值
|
# 处理无效值
|
||||||
X = np.nan_to_num(X, nan=0.0, posinf=0.0, neginf=0.0)
|
X = np.nan_to_num(X, nan=0.0, posinf=0.0, neginf=0.0)
|
||||||
@ -65,9 +68,9 @@ class DataPreparation:
|
|||||||
if cost > 0: # 只使用正数成本值
|
if cost > 0: # 只使用正数成本值
|
||||||
targets.append(cost)
|
targets.append(cost)
|
||||||
else:
|
else:
|
||||||
logging.warning(f"Skipping non-positive cost value: {cost}")
|
logger.warning(f"Skipping non-positive cost value: {cost}")
|
||||||
except (ValueError, TypeError, KeyError):
|
except (ValueError, TypeError, KeyError):
|
||||||
logging.error(f"Invalid cost value: {item.get('actual_cost')}")
|
logger.error(f"Invalid cost value: {item.get('actual_cost')}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 转换为numpy数组
|
# 转换为numpy数组
|
||||||
@ -75,19 +78,25 @@ class DataPreparation:
|
|||||||
y = np.array(targets, dtype=float)
|
y = np.array(targets, dtype=float)
|
||||||
|
|
||||||
# 记录原始数据范围
|
# 记录原始数据范围
|
||||||
logging.info(f"Original X range: min={X.min()}, max={X.max()}")
|
logger.info(f"Raw X range: min={X.min()}, max={X.max()}")
|
||||||
logging.info(f"Original y range: min={y.min()}, max={y.max()}")
|
logger.info(f"Raw y range: min={y.min()}, max={y.max()}")
|
||||||
|
|
||||||
# 处理无效值
|
|
||||||
X = np.nan_to_num(X, nan=0.0, posinf=0.0, neginf=0.0)
|
|
||||||
|
|
||||||
# 标准化特征和目标值
|
# 标准化特征和目标值
|
||||||
X_scaled = self.feature_scaler.fit_transform(X)
|
X_scaled = self.feature_scaler.fit_transform(X)
|
||||||
y_scaled = self.target_scaler.fit_transform(y.reshape(-1, 1)).ravel()
|
y_scaled = self.target_scaler.fit_transform(y.reshape(-1, 1)).ravel()
|
||||||
|
|
||||||
# 记录标准化后的数据范围
|
# 记录标准化后的数据范围
|
||||||
logging.info(f"Scaled X range: min={X_scaled.min()}, max={X_scaled.max()}")
|
logger.info(f"Scaled X range: min={X_scaled.min()}, max={X_scaled.max()}")
|
||||||
logging.info(f"Scaled y range: min={y_scaled.min()}, max={y_scaled.max()}")
|
logger.info(f"Scaled y range: min={y_scaled.min()}, max={y_scaled.max()}")
|
||||||
|
|
||||||
|
# 记录标准化器参数
|
||||||
|
logger.info("Feature scaler params:")
|
||||||
|
logger.info(f"Mean: {self.feature_scaler.mean_}")
|
||||||
|
logger.info(f"Scale: {self.feature_scaler.scale_}")
|
||||||
|
|
||||||
|
logger.info("Target scaler params:")
|
||||||
|
logger.info(f"Mean: {self.target_scaler.mean_}")
|
||||||
|
logger.info(f"Scale: {self.target_scaler.scale_}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'X': X_scaled,
|
'X': X_scaled,
|
||||||
@ -98,7 +107,7 @@ class DataPreparation:
|
|||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error in data preparation: {str(e)}")
|
logger.error(f"Error in data preparation: {str(e)}")
|
||||||
raise Exception(f"Training error: {str(e)}")
|
raise Exception(f"Training error: {str(e)}")
|
||||||
|
|
||||||
def prepare_validation_data(self, validation_data, equipment_type, feature_names=None, scalers=None):
|
def prepare_validation_data(self, validation_data, equipment_type, feature_names=None, scalers=None):
|
||||||
@ -106,13 +115,13 @@ class DataPreparation:
|
|||||||
准备验证数据
|
准备验证数据
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logging.info(f"Preparing validation data for {equipment_type}")
|
logger.info(f"Preparing validation data for {equipment_type}")
|
||||||
logging.info(f"Raw validation data size: {len(validation_data)}")
|
logger.info(f"Raw validation data size: {len(validation_data)}")
|
||||||
|
|
||||||
# 如果输入已经是 numpy 数组,直接使用
|
# 如果输入已经是 numpy 数组,直接使用
|
||||||
if isinstance(validation_data, np.ndarray):
|
if isinstance(validation_data, np.ndarray):
|
||||||
X = validation_data
|
X = validation_data
|
||||||
logging.info(f"Input is already numpy array with shape: {X.shape}")
|
logger.info(f"Input is already numpy array with shape: {X.shape}")
|
||||||
|
|
||||||
# 处理无效值
|
# 处理无效值
|
||||||
X = np.nan_to_num(X, nan=0.0, posinf=0.0, neginf=0.0)
|
X = np.nan_to_num(X, nan=0.0, posinf=0.0, neginf=0.0)
|
||||||
@ -123,9 +132,9 @@ class DataPreparation:
|
|||||||
else:
|
else:
|
||||||
X_scaled = X
|
X_scaled = X
|
||||||
|
|
||||||
logging.info(f"Preprocessed data shape: {X_scaled.shape}")
|
logger.info(f"Preprocessed data shape: {X_scaled.shape}")
|
||||||
logging.info(f"Validation features shape: {X_scaled.shape}")
|
logger.info(f"Validation features shape: {X_scaled.shape}")
|
||||||
logging.info(f"Validation features type: {X_scaled.dtype}")
|
logger.info(f"Validation features type: {X_scaled.dtype}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'X': X_scaled,
|
'X': X_scaled,
|
||||||
@ -153,13 +162,13 @@ class DataPreparation:
|
|||||||
# 提取目标值(成本)并验证范围
|
# 提取目标值(成本)并验证范围
|
||||||
try:
|
try:
|
||||||
cost = float(item['actual_cost'])
|
cost = float(item['actual_cost'])
|
||||||
logging.info(f"Raw cost value: {cost}")
|
logger.info(f"Raw cost value: {cost}")
|
||||||
if cost > 0: # 只使用正数成本值
|
if cost > 0: # 只使用正数成本值
|
||||||
targets.append(cost)
|
targets.append(cost)
|
||||||
else:
|
else:
|
||||||
logging.warning(f"Skipping non-positive cost value: {cost}")
|
logger.warning(f"Skipping non-positive cost value: {cost}")
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
logging.error(f"Invalid cost value: {item.get('actual_cost')}")
|
logger.error(f"Invalid cost value: {item.get('actual_cost')}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 转换为numpy数组
|
# 转换为numpy数组
|
||||||
@ -167,8 +176,8 @@ class DataPreparation:
|
|||||||
y = np.array(targets, dtype=float)
|
y = np.array(targets, dtype=float)
|
||||||
|
|
||||||
# 记录数据范围
|
# 记录数据范围
|
||||||
logging.info(f"Features range: min={X.min()}, max={X.max()}")
|
logger.info(f"Features range: min={X.min()}, max={X.max()}")
|
||||||
logging.info(f"Targets range: min={y.min()}, max={y.max()}")
|
logger.info(f"Targets range: min={y.min()}, max={y.max()}")
|
||||||
|
|
||||||
# 处理无效值
|
# 处理无效值
|
||||||
X = np.nan_to_num(X, nan=0.0, posinf=0.0, neginf=0.0)
|
X = np.nan_to_num(X, nan=0.0, posinf=0.0, neginf=0.0)
|
||||||
@ -184,13 +193,23 @@ class DataPreparation:
|
|||||||
X_scaled = X
|
X_scaled = X
|
||||||
y_scaled = y
|
y_scaled = y
|
||||||
|
|
||||||
logging.info(f"Preprocessed data shape: {X_scaled.shape}")
|
logger.info(f"Preprocessed data shape: {X_scaled.shape}")
|
||||||
logging.info(f"Validation features shape: {X_scaled.shape}")
|
logger.info(f"Validation features shape: {X_scaled.shape}")
|
||||||
logging.info(f"Validation features type: {X_scaled.dtype}")
|
logger.info(f"Validation features type: {X_scaled.dtype}")
|
||||||
|
|
||||||
# 记录标准化后的数据范围
|
# 记录标准化后的数据范围
|
||||||
logging.info(f"Scaled validation X range: min={X_scaled.min()}, max={X_scaled.max()}")
|
logger.info(f"Scaled validation X range: min={X_scaled.min()}, max={X_scaled.max()}")
|
||||||
logging.info(f"Scaled validation y range: min={y_scaled.min()}, max={y_scaled.max()}")
|
logger.info(f"Scaled validation y range: min={y_scaled.min()}, max={y_scaled.max()}")
|
||||||
|
|
||||||
|
# 确保特征维度一致
|
||||||
|
if not feature_names:
|
||||||
|
feature_names = self.feature_analyzer.get_equipment_specific_features(equipment_type)
|
||||||
|
|
||||||
|
logger.info(f"Expected features: {len(feature_names)}")
|
||||||
|
logger.info(f"Actual features: {X_scaled.shape[1]}")
|
||||||
|
|
||||||
|
if X_scaled.shape[1] != len(feature_names):
|
||||||
|
raise ValueError(f"Feature dimension mismatch: expected {len(feature_names)}, got {X_scaled.shape[1]}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'X': X_scaled,
|
'X': X_scaled,
|
||||||
@ -198,9 +217,9 @@ class DataPreparation:
|
|||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error in validation data preparation: {str(e)}")
|
logger.error(f"Error in validation data preparation: {str(e)}")
|
||||||
logging.error(f"Feature names: {feature_names}")
|
logger.error(f"Feature names: {feature_names}")
|
||||||
logging.error(f"Equipment type: {equipment_type}")
|
logger.error(f"Equipment type: {equipment_type}")
|
||||||
raise Exception(f"Validation error: {str(e)}")
|
raise Exception(f"Validation error: {str(e)}")
|
||||||
|
|
||||||
def calculate_derived_features(self, data, equipment_type):
|
def calculate_derived_features(self, data, equipment_type):
|
||||||
@ -210,5 +229,5 @@ class DataPreparation:
|
|||||||
try:
|
try:
|
||||||
return self.feature_analyzer.calculate_derived_features(data, equipment_type)
|
return self.feature_analyzer.calculate_derived_features(data, equipment_type)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error calculating derived features: {str(e)}")
|
logger.error(f"Error calculating derived features: {str(e)}")
|
||||||
raise Exception(f"Feature calculation error: {str(e)}")
|
raise Exception(f"Feature calculation error: {str(e)}")
|
||||||
@ -1,28 +1,37 @@
|
|||||||
import mysql.connector
|
import mysql.connector
|
||||||
from mysql.connector import Error
|
from mysql.connector import Error
|
||||||
import logging
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from ..logger import setup_logger
|
||||||
|
|
||||||
# 数据库配置
|
# 获取logger
|
||||||
DB_CONFIG = {
|
logger = setup_logger(__name__)
|
||||||
'host': 'localhost',
|
|
||||||
'user': 'root',
|
# 加载环境变量
|
||||||
'password': '123456',
|
load_dotenv()
|
||||||
'database': 'equipment_cost_db'
|
|
||||||
}
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def get_db_connection():
|
def get_db_connection():
|
||||||
"""
|
"""
|
||||||
数据库连接上下文管理器
|
数据库连接上下文管理器
|
||||||
"""
|
"""
|
||||||
conn = None
|
connection = None
|
||||||
try:
|
try:
|
||||||
conn = mysql.connector.connect(**DB_CONFIG)
|
connection = mysql.connector.connect(
|
||||||
yield conn
|
host=os.getenv('MYSQL_HOST', 'localhost'),
|
||||||
|
user=os.getenv('MYSQL_USER', 'root'),
|
||||||
|
password=os.getenv('MYSQL_PASSWORD', '123456'),
|
||||||
|
database=os.getenv('MYSQL_DATABASE', 'equipment_cost_db')
|
||||||
|
)
|
||||||
|
logger.info("Database connection established")
|
||||||
|
yield connection
|
||||||
|
|
||||||
except Error as e:
|
except Error as e:
|
||||||
logging.error(f"Error connecting to MySQL: {str(e)}")
|
logger.error(f"Error connecting to MySQL: {str(e)}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if conn and conn.is_connected():
|
if connection and connection.is_connected():
|
||||||
conn.close()
|
connection.close()
|
||||||
|
logger.info("Database connection closed")
|
||||||
@ -5,6 +5,9 @@ from sklearn.preprocessing import StandardScaler
|
|||||||
from sklearn.ensemble import RandomForestRegressor
|
from sklearn.ensemble import RandomForestRegressor
|
||||||
from sklearn.metrics import r2_score
|
from sklearn.metrics import r2_score
|
||||||
import logging
|
import logging
|
||||||
|
from .logger import setup_logger
|
||||||
|
|
||||||
|
logger = setup_logger(__name__)
|
||||||
|
|
||||||
class FeatureAnalysis:
|
class FeatureAnalysis:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -182,7 +185,7 @@ class FeatureAnalysis:
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error calculating derived features: {str(e)}")
|
logger.error(f"Error calculating derived features: {str(e)}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def analyze_features(self, features, target, feature_names):
|
def analyze_features(self, features, target, feature_names):
|
||||||
@ -235,7 +238,7 @@ class FeatureAnalysis:
|
|||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error in feature analysis: {str(e)}")
|
logger.error(f"Error in feature analysis: {str(e)}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def preprocess_features(self, equipment_data, equipment_type):
|
def preprocess_features(self, equipment_data, equipment_type):
|
||||||
@ -258,9 +261,9 @@ class FeatureAnalysis:
|
|||||||
mean_value = df[col].mean()
|
mean_value = df[col].mean()
|
||||||
df[col] = df[col].fillna(mean_value)
|
df[col] = df[col].fillna(mean_value)
|
||||||
|
|
||||||
logging.info(f"Preprocessed data shape: {df.shape}")
|
logger.info(f"Preprocessed data shape: {df.shape}")
|
||||||
return df
|
return df
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error preprocessing features: {str(e)}")
|
logger.error(f"Error preprocessing features: {str(e)}")
|
||||||
raise Exception(f"Feature preprocessing error: {str(e)}")
|
raise Exception(f"Feature preprocessing error: {str(e)}")
|
||||||
@ -1,12 +1,8 @@
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import logging
|
from .logger import setup_logger
|
||||||
from src.database.db_connection import get_db_connection
|
from src.database.db_connection import get_db_connection
|
||||||
|
|
||||||
logging.basicConfig(
|
logger = setup_logger(__name__)
|
||||||
filename='logs/import.log',
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
|
||||||
)
|
|
||||||
|
|
||||||
def import_training_data(excel_file):
|
def import_training_data(excel_file):
|
||||||
"""
|
"""
|
||||||
@ -25,7 +21,7 @@ def import_training_data(excel_file):
|
|||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# 1. 先导入火箭炮数据
|
# 1. 先导入火箭炮数据
|
||||||
logging.info("开始导入火箭炮数据...")
|
logger.info("开始导入火箭炮数据...")
|
||||||
for _, row in rocket_df.iterrows():
|
for _, row in rocket_df.iterrows():
|
||||||
equipment_names.add(row['名称'])
|
equipment_names.add(row['名称'])
|
||||||
# 检查是否已存在相同名称的装备
|
# 检查是否已存在相同名称的装备
|
||||||
@ -36,7 +32,7 @@ def import_training_data(excel_file):
|
|||||||
|
|
||||||
existing_equipment = cursor.fetchone()
|
existing_equipment = cursor.fetchone()
|
||||||
if existing_equipment:
|
if existing_equipment:
|
||||||
logging.warning(f"火箭炮 '{row['名称']}' 已存在,跳过导入")
|
logger.warning(f"火箭炮 '{row['名称']}' 已存在,跳过导入")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 插入基本信息
|
# 插入基本信息
|
||||||
@ -96,15 +92,15 @@ def import_training_data(excel_file):
|
|||||||
VALUES (%s, %s)
|
VALUES (%s, %s)
|
||||||
""", (equipment_id, row['成本_元']))
|
""", (equipment_id, row['成本_元']))
|
||||||
|
|
||||||
logging.info("火箭炮数据导入完成")
|
logger.info("火箭炮数据导入完成")
|
||||||
|
|
||||||
# 2. 导入巡飞弹数据
|
# 2. 导入巡飞弹数据
|
||||||
logging.info("开始导入巡飞弹数据...")
|
logger.info("开始导入巡飞弹数据...")
|
||||||
for index, row in missile_df.iterrows():
|
for index, row in missile_df.iterrows():
|
||||||
# 记录每行数据的空值情况
|
# 记录每行数据的空值情况
|
||||||
null_values = row[row.isna()].index.tolist()
|
null_values = row[row.isna()].index.tolist()
|
||||||
if null_values:
|
if null_values:
|
||||||
logging.info(f"行 {index + 2} 中的空值字段: {null_values}")
|
logger.info(f"行 {index + 2} 中的空值字段: {null_values}")
|
||||||
|
|
||||||
equipment_names.add(row['名称'])
|
equipment_names.add(row['名称'])
|
||||||
# 检查是否已存在相同名称的装备
|
# 检查是否已存在相同名称的装备
|
||||||
@ -115,7 +111,7 @@ def import_training_data(excel_file):
|
|||||||
|
|
||||||
existing_equipment = cursor.fetchone()
|
existing_equipment = cursor.fetchone()
|
||||||
if existing_equipment:
|
if existing_equipment:
|
||||||
logging.warning(f"巡飞弹 '{row['名称']}' 已存在,跳过导入")
|
logger.warning(f"巡飞弹 '{row['名称']}' 已存在,跳过导入")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 插入基本信息
|
# 插入基本信息
|
||||||
@ -175,25 +171,25 @@ def import_training_data(excel_file):
|
|||||||
VALUES (%s, %s)
|
VALUES (%s, %s)
|
||||||
""", (equipment_id, float(row['成本_元'])))
|
""", (equipment_id, float(row['成本_元'])))
|
||||||
|
|
||||||
logging.info("巡飞弹数据导入完成")
|
logger.info("巡飞弹数据导入完成")
|
||||||
|
|
||||||
# 提交之前的更改并关闭原有游标
|
# 提交之前的更改并关闭原有游标
|
||||||
cursor.close()
|
cursor.close()
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
# 3. 导入特殊参数
|
# 3. 导入特殊参数
|
||||||
logging.info("开始导入特殊参数...")
|
logger.info("开始导入特殊参数...")
|
||||||
for index, row in special_df.iterrows():
|
for index, row in special_df.iterrows():
|
||||||
equipment_name = row['装备名称']
|
equipment_name = row['装备名称']
|
||||||
param_name = row['参数名称']
|
param_name = row['参数名称']
|
||||||
logging.info(f"处理第 {index + 1} 条记录: 装备='{equipment_name}', 参数='{param_name}'")
|
logger.info(f"处理第 {index + 1} 条记录: 装备='{equipment_name}', 参数='{param_name}'")
|
||||||
|
|
||||||
if equipment_name not in equipment_names:
|
if equipment_name not in equipment_names:
|
||||||
logging.warning(f"未找到装备: {equipment_name},请检查名称是否正确")
|
logger.warning(f"未找到装备: {equipment_name},请检查名称是否正确")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 获取装备ID - 使用新的游标
|
# 获取装备ID - 使用新的游标
|
||||||
logging.debug(f"查询装备ID: {equipment_name}")
|
logger.debug(f"查询装备ID: {equipment_name}")
|
||||||
with conn.cursor() as id_cursor:
|
with conn.cursor() as id_cursor:
|
||||||
id_cursor.execute("""
|
id_cursor.execute("""
|
||||||
SELECT id FROM equipment WHERE name = %s
|
SELECT id FROM equipment WHERE name = %s
|
||||||
@ -201,14 +197,14 @@ def import_training_data(excel_file):
|
|||||||
result = id_cursor.fetchone()
|
result = id_cursor.fetchone()
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
logging.warning(f"未找到装备: {equipment_name}")
|
logger.warning(f"未找到装备: {equipment_name}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
equipment_id = result[0]
|
equipment_id = result[0]
|
||||||
logging.debug(f"找到装备ID: {equipment_id}")
|
logger.debug(f"找到装备ID: {equipment_id}")
|
||||||
|
|
||||||
# 检查参数是否存在 - 使用新的游标
|
# 检查参数是否存在 - 使用新的游标
|
||||||
logging.debug(f"检查参数是否存在: equipment_id={equipment_id}, param_name='{param_name}'")
|
logger.debug(f"检查参数是否存在: equipment_id={equipment_id}, param_name='{param_name}'")
|
||||||
with conn.cursor() as check_cursor:
|
with conn.cursor() as check_cursor:
|
||||||
check_cursor.execute("""
|
check_cursor.execute("""
|
||||||
SELECT id FROM custom_params
|
SELECT id FROM custom_params
|
||||||
@ -217,7 +213,7 @@ def import_training_data(excel_file):
|
|||||||
exists = check_cursor.fetchone()
|
exists = check_cursor.fetchone()
|
||||||
|
|
||||||
if exists:
|
if exists:
|
||||||
logging.warning(f"装备 '{equipment_name}' 的参数 '{param_name}' 已存在,跳过导入")
|
logger.warning(f"装备 '{equipment_name}' 的参数 '{param_name}' 已存在,跳过导入")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 插入新的参数 - 使用新的游标
|
# 插入新的参数 - 使用新的游标
|
||||||
@ -225,7 +221,7 @@ def import_training_data(excel_file):
|
|||||||
param_unit = row['参数单位'] if pd.notna(row['参数单位']) else None
|
param_unit = row['参数单位'] if pd.notna(row['参数单位']) else None
|
||||||
param_desc = row['参数说明'] if pd.notna(row['参数说明']) else None
|
param_desc = row['参数说明'] if pd.notna(row['参数说明']) else None
|
||||||
|
|
||||||
logging.debug(f"插入新参数: value='{param_value}', unit='{param_unit}', desc='{param_desc}'")
|
logger.debug(f"插入新参数: value='{param_value}', unit='{param_unit}', desc='{param_desc}'")
|
||||||
with conn.cursor() as insert_cursor:
|
with conn.cursor() as insert_cursor:
|
||||||
insert_cursor.execute("""
|
insert_cursor.execute("""
|
||||||
INSERT INTO custom_params
|
INSERT INTO custom_params
|
||||||
@ -238,22 +234,22 @@ def import_training_data(excel_file):
|
|||||||
param_unit,
|
param_unit,
|
||||||
param_desc
|
param_desc
|
||||||
))
|
))
|
||||||
logging.debug(f"成功插入参数记录")
|
logger.debug(f"成功插入参数记录")
|
||||||
|
|
||||||
# 最终提交
|
# 最终提交
|
||||||
conn.commit()
|
conn.commit()
|
||||||
logging.info("特殊参数导入完成")
|
logger.info("特殊参数导入完成")
|
||||||
logging.info("所有数据导入成功")
|
logger.info("所有数据导入成功")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error importing data: {str(e)}")
|
logger.error(f"Error importing data: {str(e)}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
excel_file = 'data/equipment_data_20241108.xlsx'
|
excel_file = 'data/equipment_data_20241108.xlsx'
|
||||||
import_training_data(excel_file)
|
import_training_data(excel_file)
|
||||||
logging.info("All data imported successfully")
|
logger.info("All data imported successfully")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Import failed: {str(e)}")
|
logger.error(f"Import failed: {str(e)}")
|
||||||
33
src/logger.py
Normal file
33
src/logger.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
def setup_logger(name):
|
||||||
|
"""
|
||||||
|
创建并配置logger
|
||||||
|
"""
|
||||||
|
# 创建logger
|
||||||
|
logger = logging.getLogger(name)
|
||||||
|
|
||||||
|
# 如果logger已经有处理器,直接返回
|
||||||
|
if logger.handlers:
|
||||||
|
return logger
|
||||||
|
|
||||||
|
# 设置日志级别
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
# 确保日志目录存在
|
||||||
|
os.makedirs('logs', exist_ok=True)
|
||||||
|
|
||||||
|
# 创建文件处理器
|
||||||
|
file_handler = logging.FileHandler('logs/api.log')
|
||||||
|
file_handler.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
# 创建格式化器
|
||||||
|
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
|
file_handler.setFormatter(formatter)
|
||||||
|
|
||||||
|
# 添加处理器
|
||||||
|
logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
return logger
|
||||||
@ -14,39 +14,64 @@ from datetime import datetime
|
|||||||
import json
|
import json
|
||||||
from src.database import get_db_connection
|
from src.database import get_db_connection
|
||||||
from src.data_preparation import DataPreparation
|
from src.data_preparation import DataPreparation
|
||||||
|
from sklearn.cross_decomposition import PLSRegression
|
||||||
|
from .logger import setup_logger
|
||||||
|
|
||||||
|
logger = setup_logger(__name__)
|
||||||
|
|
||||||
class ModelTrainer:
|
class ModelTrainer:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
"""
|
||||||
|
初始化 ModelTrainer
|
||||||
|
"""
|
||||||
self.models = {
|
self.models = {
|
||||||
'xgboost': self._create_xgboost_model(),
|
'xgboost': self._create_xgboost_model(),
|
||||||
'lightgbm': self._create_lightgbm_model(),
|
'lightgbm': self._create_lightgbm_model(),
|
||||||
'gbdt': self._create_gbdt_model(),
|
'gbm': self._create_gbm_model(),
|
||||||
'rf': self._create_rf_model()
|
'rf': self._create_rf_model(),
|
||||||
|
'pls': self._create_pls_model()
|
||||||
}
|
}
|
||||||
self.best_model = None
|
self.best_model = None
|
||||||
self.imputer = SimpleImputer(strategy='mean')
|
self.imputer = SimpleImputer(strategy='mean')
|
||||||
self.feature_scaler = None
|
self.feature_scaler = None
|
||||||
self.target_scaler = None
|
self.target_scaler = None
|
||||||
|
self.equipment_type = None
|
||||||
|
self.feature_analyzer = FeatureAnalysis()
|
||||||
|
|
||||||
def fit_model(self, X_train, y_train, model_names, X_val=None, y_val=None, equipment_type=None):
|
def fit_model(self, X_train, y_train, model_names, X_val=None, y_val=None, equipment_type=None):
|
||||||
"""
|
"""
|
||||||
训练模型并返回评估结果
|
训练模型并返回评估结果
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 记录数据范围
|
self.equipment_type = equipment_type
|
||||||
logging.info(f"Training data range - X: min={X_train.min()}, max={X_train.max()}")
|
logger.info(f"Training data range - X: min={X_train.min()}, max={X_train.max()}")
|
||||||
logging.info(f"Training data range - y: min={y_train.min()}, max={y_train.max()}")
|
logger.info(f"Training data range - y: min={y_train.min()}, max={y_train.max()}")
|
||||||
|
|
||||||
results = {}
|
results = {}
|
||||||
best_score = -float('inf')
|
best_score = -float('inf')
|
||||||
best_model_info = None
|
best_model_info = None
|
||||||
|
|
||||||
|
# 首先训练 PLS 模型
|
||||||
|
logger.info("Training pls...")
|
||||||
|
pls_model = self.models['pls']
|
||||||
|
pls_model.fit(X_train, y_train)
|
||||||
|
pls_metrics = self._calculate_metrics(
|
||||||
|
pls_model,
|
||||||
|
X_train, y_train,
|
||||||
|
X_val, y_val
|
||||||
|
)
|
||||||
|
results['pls'] = pls_metrics
|
||||||
|
|
||||||
|
# 训练其他机器学习模型
|
||||||
for model_name in model_names:
|
for model_name in model_names:
|
||||||
if model_name not in self.models:
|
if model_name == 'pls': # 跳过 PLS 模型,因为已经训练过了
|
||||||
logging.warning(f"Unknown model: {model_name}")
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logging.info(f"Training {model_name}...")
|
if model_name not in self.models:
|
||||||
|
logger.warning(f"Unknown model: {model_name}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.info(f"Training {model_name}...")
|
||||||
model = self.models[model_name]
|
model = self.models[model_name]
|
||||||
|
|
||||||
# 训练模型
|
# 训练模型
|
||||||
@ -59,48 +84,30 @@ class ModelTrainer:
|
|||||||
X_val, y_val
|
X_val, y_val
|
||||||
)
|
)
|
||||||
|
|
||||||
# 更新最佳模型
|
results[model_name] = metrics
|
||||||
|
|
||||||
|
# 更新最佳模型(只在机器学习模型中比较)
|
||||||
if metrics['validation']['r2'] > best_score:
|
if metrics['validation']['r2'] > best_score:
|
||||||
best_score = metrics['validation']['r2']
|
best_score = metrics['validation']['r2']
|
||||||
self.best_model = model
|
|
||||||
best_model_info = {
|
best_model_info = {
|
||||||
'type': model_name,
|
'type': model_name,
|
||||||
'r2': float(metrics['validation']['r2']),
|
'r2': metrics['validation']['r2'],
|
||||||
'mae': float(metrics['validation']['mae']) if metrics['validation']['mae'] is not None else None,
|
'mae': metrics['validation']['mae'],
|
||||||
'rmse': float(metrics['validation']['rmse']) if metrics['validation']['rmse'] is not None else None
|
'rmse': metrics['validation']['rmse']
|
||||||
}
|
}
|
||||||
|
self.best_model = model
|
||||||
|
|
||||||
# 转换 numpy 数据类型为 Python 原生类型
|
# 保存最佳模型和 PLS 模型
|
||||||
results[model_name] = {
|
|
||||||
'train': {
|
|
||||||
'r2': float(metrics['train']['r2']),
|
|
||||||
'mae': float(metrics['train']['mae']),
|
|
||||||
'rmse': float(metrics['train']['rmse'])
|
|
||||||
},
|
|
||||||
'validation': {
|
|
||||||
'r2': float(metrics['validation']['r2']),
|
|
||||||
'mae': float(metrics['validation']['mae']) if metrics['validation']['mae'] is not None else None,
|
|
||||||
'rmse': float(metrics['validation']['rmse']) if metrics['validation']['rmse'] is not None else None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 保存最佳模型
|
|
||||||
if equipment_type and best_model_info:
|
if equipment_type and best_model_info:
|
||||||
self._save_best_model(equipment_type, best_model_info, X_train)
|
self._save_best_model(equipment_type, best_model_info, X_train, y_train, X_val, y_val)
|
||||||
|
|
||||||
# 转换特征重要性为列表
|
|
||||||
feature_importance = None
|
|
||||||
if self.best_model and hasattr(self.best_model, 'feature_importances_'):
|
|
||||||
feature_importance = [float(x) for x in self.best_model.feature_importances_]
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'metrics': results,
|
'metrics': results,
|
||||||
'best_model': best_model_info,
|
'best_model': best_model_info
|
||||||
'feature_importance': feature_importance
|
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error in model training: {str(e)}")
|
logger.error(f"Error in model training: {str(e)}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _calculate_metrics(self, model, X_train, y_train, X_val=None, y_val=None):
|
def _calculate_metrics(self, model, X_train, y_train, X_val=None, y_val=None):
|
||||||
@ -120,8 +127,8 @@ class ModelTrainer:
|
|||||||
y_train_orig = y_train
|
y_train_orig = y_train
|
||||||
|
|
||||||
# 记录预测范围
|
# 记录预测范围
|
||||||
logging.info(f"Train predictions range: min={train_pred.min()}, max={train_pred.max()}")
|
logger.info(f"Train predictions range: min={train_pred.min()}, max={train_pred.max()}")
|
||||||
logging.info(f"Train actual range: min={y_train_orig.min()}, max={y_train_orig.max()}")
|
logger.info(f"Train actual range: min={y_train_orig.min()}, max={y_train_orig.max()}")
|
||||||
|
|
||||||
train_metrics = {
|
train_metrics = {
|
||||||
'r2': r2_score(y_train_orig, train_pred),
|
'r2': r2_score(y_train_orig, train_pred),
|
||||||
@ -141,8 +148,8 @@ class ModelTrainer:
|
|||||||
y_val_orig = y_val
|
y_val_orig = y_val
|
||||||
|
|
||||||
# 记录预测范围
|
# 记录预测范围
|
||||||
logging.info(f"Validation predictions range: min={val_pred.min()}, max={val_pred.max()}")
|
logger.info(f"Validation predictions range: min={val_pred.min()}, max={val_pred.max()}")
|
||||||
logging.info(f"Validation actual range: min={y_val_orig.min()}, max={y_val_orig.max()}")
|
logger.info(f"Validation actual range: min={y_val_orig.min()}, max={y_val_orig.max()}")
|
||||||
|
|
||||||
val_metrics = {
|
val_metrics = {
|
||||||
'r2': r2_score(y_val_orig, val_pred),
|
'r2': r2_score(y_val_orig, val_pred),
|
||||||
@ -169,9 +176,9 @@ class ModelTrainer:
|
|||||||
"""
|
"""
|
||||||
return xgb.XGBRegressor(
|
return xgb.XGBRegressor(
|
||||||
n_estimators=50, # 减少树的数量
|
n_estimators=50, # 减少树的数量
|
||||||
learning_rate=0.05, # 减小学习率
|
learning_rate=0.05, # 学习率
|
||||||
max_depth=3, # 减小树的深度
|
max_depth=3, # 减小树的深
|
||||||
min_child_weight=3, # 增加最小子节点权重
|
min_child_weight=3, # 增加节点权重
|
||||||
subsample=0.7, # 减小样本采样比例
|
subsample=0.7, # 减小样本采样比例
|
||||||
colsample_bytree=0.7, # 减小特征采样比例
|
colsample_bytree=0.7, # 减小特征采样比例
|
||||||
reg_alpha=0.1, # L1 正则化
|
reg_alpha=0.1, # L1 正则化
|
||||||
@ -198,19 +205,18 @@ class ModelTrainer:
|
|||||||
verbose=-1
|
verbose=-1
|
||||||
)
|
)
|
||||||
|
|
||||||
def _create_gbdt_model(self):
|
def _create_gbm_model(self):
|
||||||
"""
|
"""
|
||||||
创建 GBDT 模型,增强正则化以减轻过拟合
|
创建 GBM 模型,增强正则化以减轻过拟合
|
||||||
"""
|
"""
|
||||||
return GradientBoostingRegressor(
|
return GradientBoostingRegressor(
|
||||||
n_estimators=20, # 减少树的数量
|
n_estimators=100,
|
||||||
learning_rate=0.01, # 减小学习率
|
learning_rate=0.1,
|
||||||
max_depth=2, # 减小树的深度
|
max_depth=3,
|
||||||
min_samples_split=4, # 增加分裂所需的最小样本数
|
|
||||||
min_samples_leaf=3, # 增加叶子节点最小样本数
|
|
||||||
subsample=0.5, # 减小样本采样比例
|
|
||||||
random_state=42,
|
random_state=42,
|
||||||
validation_fraction=0.2 # 使用部分训练数据作为验证集
|
subsample=0.8,
|
||||||
|
min_samples_split=3,
|
||||||
|
min_samples_leaf=2
|
||||||
)
|
)
|
||||||
|
|
||||||
def _create_rf_model(self):
|
def _create_rf_model(self):
|
||||||
@ -218,26 +224,34 @@ class ModelTrainer:
|
|||||||
创建随机森林模型,针对小样本数据调整参数
|
创建随机森林模型,针对小样本数据调整参数
|
||||||
"""
|
"""
|
||||||
return RandomForestRegressor(
|
return RandomForestRegressor(
|
||||||
n_estimators=100, # 增加树的数量
|
n_estimators=100,
|
||||||
max_depth=4, # 限制树的深度
|
max_depth=3,
|
||||||
min_samples_split=2, # 减小分需的最小样本数
|
random_state=42,
|
||||||
min_samples_leaf=1, # 减小叶子节点最小样本数
|
min_samples_split=3,
|
||||||
max_features='sqrt', # 特征采样
|
min_samples_leaf=2
|
||||||
bootstrap=True, # 使用 bootstrap 采样
|
|
||||||
oob_score=True, # 计算袋外分数
|
|
||||||
random_state=42
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _save_best_model(self, equipment_type, best_model_info, X_train):
|
def _create_pls_model(self):
|
||||||
"""
|
"""
|
||||||
保存最佳模型
|
创建 PLS 模型,优化参数配置
|
||||||
|
"""
|
||||||
|
return PLSRegression(
|
||||||
|
n_components=2, # 减少主成分数量,从5减到2
|
||||||
|
scale=True, # 保持数据标准化
|
||||||
|
max_iter=500, # 减少最大迭代次数,避免过拟合
|
||||||
|
tol=1e-6 # 降低收敛精度,避免过拟合
|
||||||
|
)
|
||||||
|
|
||||||
|
def _save_best_model(self, equipment_type, best_model_info, X_train, y_train, X_val=None, y_val=None):
|
||||||
|
"""
|
||||||
|
保存最佳模型和 PLS 模型
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
model_dir = 'models'
|
model_dir = 'models'
|
||||||
os.makedirs(model_dir, exist_ok=True)
|
os.makedirs(model_dir, exist_ok=True)
|
||||||
|
|
||||||
# 保存模型文件
|
# 1. 保存最佳机器学习模型
|
||||||
model_path = f'{model_dir}/{equipment_type}_{timestamp}'
|
model_path = f'{model_dir}/{equipment_type}_{timestamp}'
|
||||||
if isinstance(self.best_model, xgb.XGBRegressor):
|
if isinstance(self.best_model, xgb.XGBRegressor):
|
||||||
self.best_model.save_model(f'{model_path}.json')
|
self.best_model.save_model(f'{model_path}.json')
|
||||||
@ -246,128 +260,180 @@ class ModelTrainer:
|
|||||||
joblib.dump(self.best_model, f'{model_path}.joblib')
|
joblib.dump(self.best_model, f'{model_path}.joblib')
|
||||||
model_format = 'joblib'
|
model_format = 'joblib'
|
||||||
|
|
||||||
# 验证标准化器
|
# 2. 保存 PLS 模型
|
||||||
if not isinstance(self.feature_scaler, StandardScaler):
|
pls_model = self.models['pls']
|
||||||
raise ValueError("Invalid feature scaler")
|
pls_path = f'{model_dir}/{equipment_type}_{timestamp}_pls.joblib'
|
||||||
if not isinstance(self.target_scaler, StandardScaler):
|
joblib.dump(pls_model, pls_path)
|
||||||
raise ValueError("Invalid target scaler")
|
|
||||||
|
|
||||||
# 保存标准化器
|
# 3. 保存标准化器
|
||||||
scaler_path = f'{model_dir}/{equipment_type}_{timestamp}_scaler.joblib'
|
scaler_path = f'{model_dir}/{equipment_type}_{timestamp}_scaler.joblib'
|
||||||
joblib.dump({
|
joblib.dump({
|
||||||
'feature_scaler': self.feature_scaler,
|
'feature_scaler': self.feature_scaler,
|
||||||
'target_scaler': self.target_scaler
|
'target_scaler': self.target_scaler
|
||||||
}, scaler_path)
|
}, scaler_path)
|
||||||
|
|
||||||
logging.info(f"Saved model to {model_path}.{model_format}")
|
logger.info(f"Saved best model to {model_path}.{model_format}")
|
||||||
logging.info(f"Saved scalers to {scaler_path}")
|
logger.info(f"Saved PLS model to {pls_path}")
|
||||||
|
logger.info(f"Saved scalers to {scaler_path}")
|
||||||
|
|
||||||
# 更新数据库中的模型记录
|
# 4. 更新数据库中的模型记录
|
||||||
with get_db_connection() as conn:
|
with get_db_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# 将之前的激活模型设置为非激活
|
# 将所有模型设置为非激活
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
UPDATE trained_models
|
UPDATE trained_models
|
||||||
SET is_active = FALSE
|
SET is_active = FALSE
|
||||||
WHERE equipment_type = %s
|
WHERE equipment_type = %s
|
||||||
""", (equipment_type,))
|
""", (equipment_type,))
|
||||||
|
|
||||||
# 插入新的模型记录
|
# 获取 PLS 模型的评估指标
|
||||||
|
pls_metrics = self._calculate_metrics(
|
||||||
|
self.models['pls'],
|
||||||
|
X_train,
|
||||||
|
y_train,
|
||||||
|
X_val,
|
||||||
|
y_val
|
||||||
|
)
|
||||||
|
|
||||||
|
# 保存最佳机器学习模型记录
|
||||||
|
self.best_model.equipment_type = equipment_type # 设置装备类型
|
||||||
|
ml_feature_importance = self._get_feature_importance(self.best_model)
|
||||||
|
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
INSERT INTO trained_models (
|
INSERT INTO trained_models (
|
||||||
model_name, model_type, equipment_type, model_path,
|
model_name, model_type, equipment_type, model_path, scaler_path,
|
||||||
scaler_path, r2_score, mae, rmse, feature_importance,
|
r2_score, mae, rmse, feature_importance, training_data_size,
|
||||||
training_data_size, training_date, is_active, created_by
|
training_date, is_active, created_by
|
||||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), TRUE, 'system')
|
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), TRUE, %s)
|
||||||
""", (
|
""", (
|
||||||
f'{best_model_info["type"]}_{timestamp}',
|
f"{equipment_type}_{timestamp}", # model_name
|
||||||
best_model_info["type"],
|
best_model_info['type'], # model_type
|
||||||
equipment_type,
|
equipment_type, # equipment_type
|
||||||
f'{model_path}.{model_format}',
|
f"{model_path}.{model_format}", # model_path
|
||||||
scaler_path,
|
scaler_path, # scaler_path
|
||||||
best_model_info["r2"],
|
best_model_info['r2'], # r2_score
|
||||||
best_model_info["mae"],
|
best_model_info['mae'], # mae
|
||||||
best_model_info["rmse"],
|
best_model_info['rmse'], # rmse
|
||||||
json.dumps(self.feature_importance) if hasattr(self, 'feature_importance') else None,
|
json.dumps(ml_feature_importance), # feature_importance
|
||||||
len(X_train)
|
len(X_train), # training_data_size
|
||||||
|
'system' # created_by
|
||||||
|
))
|
||||||
|
|
||||||
|
# 保存 PLS 模型记录
|
||||||
|
pls_model.equipment_type = equipment_type # 设置装备类型
|
||||||
|
pls_feature_importance = self._get_feature_importance(pls_model)
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO trained_models (
|
||||||
|
model_name, model_type, equipment_type, model_path, scaler_path,
|
||||||
|
r2_score, mae, rmse, feature_importance, training_data_size,
|
||||||
|
training_date, is_active, created_by
|
||||||
|
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), TRUE, %s)
|
||||||
|
""", (
|
||||||
|
f"{equipment_type}_{timestamp}_pls", # model_name
|
||||||
|
'pls', # model_type
|
||||||
|
equipment_type, # equipment_type
|
||||||
|
pls_path, # model_path
|
||||||
|
scaler_path, # scaler_path
|
||||||
|
float(pls_metrics['validation']['r2']), # r2_score
|
||||||
|
float(pls_metrics['validation']['mae']), # mae
|
||||||
|
float(pls_metrics['validation']['rmse']), # rmse
|
||||||
|
json.dumps(pls_feature_importance), # feature_importance
|
||||||
|
len(X_train), # training_data_size
|
||||||
|
'system' # created_by
|
||||||
))
|
))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
logging.info(f"Best model saved: {model_path}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error saving best model: {str(e)}")
|
logger.error(f"Error saving models: {str(e)}")
|
||||||
return False
|
logger.error("Detailed traceback:", exc_info=True)
|
||||||
|
raise
|
||||||
|
|
||||||
def load_model(self, equipment_type):
|
def load_model(self, equipment_type, model_type='ml'):
|
||||||
"""
|
"""
|
||||||
加载已训练的模型
|
加载已训练的模型
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logging.info(f"Loading model for {equipment_type}")
|
logger.info(f"Loading {model_type} model for {equipment_type}")
|
||||||
|
|
||||||
# 从数据库获最新的激活模型
|
# 从数据库获取激活的模型
|
||||||
with get_db_connection() as conn:
|
with get_db_connection() as conn:
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute("""
|
|
||||||
SELECT * FROM trained_models
|
|
||||||
WHERE equipment_type = %s AND is_active = TRUE
|
|
||||||
ORDER BY training_date DESC LIMIT 1
|
|
||||||
""", (equipment_type,))
|
|
||||||
|
|
||||||
|
# 构建查询语句
|
||||||
|
if model_type == 'pls':
|
||||||
|
query = """
|
||||||
|
SELECT * FROM trained_models
|
||||||
|
WHERE equipment_type = %s
|
||||||
|
AND model_type = 'pls'
|
||||||
|
AND is_active = TRUE
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
params = (equipment_type,)
|
||||||
|
else:
|
||||||
|
query = """
|
||||||
|
SELECT * FROM trained_models
|
||||||
|
WHERE equipment_type = %s
|
||||||
|
AND model_type != 'pls'
|
||||||
|
AND is_active = TRUE
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
params = (equipment_type,)
|
||||||
|
|
||||||
|
# 记录查询信息
|
||||||
|
logger.info(f"Executing query: {query}")
|
||||||
|
logger.info(f"Query params: {params}")
|
||||||
|
|
||||||
|
cursor.execute(query, params)
|
||||||
model_record = cursor.fetchone()
|
model_record = cursor.fetchone()
|
||||||
if not model_record:
|
|
||||||
raise ValueError(f"No active model found for {equipment_type}")
|
|
||||||
|
|
||||||
logging.info(f"Found model: {model_record['model_name']}")
|
# 记录查询结果
|
||||||
logging.info(f"Model path: {model_record['model_path']}")
|
if model_record:
|
||||||
logging.info(f"Scaler path: {model_record['scaler_path']}")
|
logger.info(f"Found model record: {model_record}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"No active model found for type {model_type}")
|
||||||
|
return False
|
||||||
|
|
||||||
# 检查文件是否存在
|
# 检查文件是否存在
|
||||||
|
logger.info(f"Checking model file: {model_record['model_path']}")
|
||||||
|
logger.info(f"Checking scaler file: {model_record['scaler_path']}")
|
||||||
|
|
||||||
if not os.path.exists(model_record['model_path']):
|
if not os.path.exists(model_record['model_path']):
|
||||||
|
logger.error(f"Model file not found: {model_record['model_path']}")
|
||||||
raise FileNotFoundError(f"Model file not found: {model_record['model_path']}")
|
raise FileNotFoundError(f"Model file not found: {model_record['model_path']}")
|
||||||
|
|
||||||
if not os.path.exists(model_record['scaler_path']):
|
if not os.path.exists(model_record['scaler_path']):
|
||||||
|
logger.error(f"Scaler file not found: {model_record['scaler_path']}")
|
||||||
raise FileNotFoundError(f"Scaler file not found: {model_record['scaler_path']}")
|
raise FileNotFoundError(f"Scaler file not found: {model_record['scaler_path']}")
|
||||||
|
|
||||||
# 加载模型文件
|
# 加载模型文件
|
||||||
if model_record['model_type'] == 'xgboost':
|
logger.info(f"Loading model from {model_record['model_path']}")
|
||||||
self.best_model = xgb.XGBRegressor()
|
if model_type == 'pls':
|
||||||
self.best_model.load_model(model_record['model_path'])
|
|
||||||
else:
|
|
||||||
self.best_model = joblib.load(model_record['model_path'])
|
self.best_model = joblib.load(model_record['model_path'])
|
||||||
|
logger.info("Loaded PLS model")
|
||||||
|
else:
|
||||||
|
if model_record['model_type'] == 'xgboost':
|
||||||
|
self.best_model = xgb.XGBRegressor()
|
||||||
|
self.best_model.load_model(model_record['model_path'])
|
||||||
|
logger.info("Loaded XGBoost model")
|
||||||
|
else:
|
||||||
|
self.best_model = joblib.load(model_record['model_path'])
|
||||||
|
logger.info(f"Loaded {model_record['model_type']} model")
|
||||||
|
|
||||||
# 加载标准化器
|
# 加载标准化器
|
||||||
try:
|
logger.info(f"Loading scalers from {model_record['scaler_path']}")
|
||||||
scalers = joblib.load(model_record['scaler_path'])
|
scalers = joblib.load(model_record['scaler_path'])
|
||||||
logging.info(f"Loaded scalers: {scalers.keys()}")
|
self.feature_scaler = scalers['feature_scaler']
|
||||||
|
self.target_scaler = scalers['target_scaler']
|
||||||
if 'feature_scaler' not in scalers or 'target_scaler' not in scalers:
|
logger.info("Loaded scalers successfully")
|
||||||
raise ValueError("Missing scalers in saved file")
|
|
||||||
|
|
||||||
self.feature_scaler = scalers['feature_scaler']
|
|
||||||
self.target_scaler = scalers['target_scaler']
|
|
||||||
|
|
||||||
# 验证标准化器
|
|
||||||
if not hasattr(self.feature_scaler, 'transform') or not hasattr(self.target_scaler, 'transform'):
|
|
||||||
raise ValueError("Invalid scaler objects")
|
|
||||||
|
|
||||||
logging.info("Model and scalers loaded successfully")
|
|
||||||
logging.info(f"Feature scaler type: {type(self.feature_scaler)}")
|
|
||||||
logging.info(f"Target scaler type: {type(self.target_scaler)}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error loading scalers: {str(e)}")
|
|
||||||
logging.error(f"Scaler file content: {scalers if 'scalers' in locals() else 'Not loaded'}")
|
|
||||||
raise ValueError(f"Failed to load scalers: {str(e)}")
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error loading model: {str(e)}")
|
logger.error(f"Error loading model: {str(e)}")
|
||||||
logging.error("Detailed traceback:", exc_info=True)
|
logger.error(f"Detailed traceback:", exc_info=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def predict(self, features):
|
def predict(self, features):
|
||||||
@ -384,39 +450,163 @@ class ModelTrainer:
|
|||||||
if not self.target_scaler:
|
if not self.target_scaler:
|
||||||
raise ValueError("Target scaler not loaded")
|
raise ValueError("Target scaler not loaded")
|
||||||
|
|
||||||
logging.info("Starting prediction")
|
logger.info("Starting prediction")
|
||||||
logging.info(f"Input features shape: {features.shape}")
|
logger.info(f"Input features shape: {features.shape}")
|
||||||
logging.info(f"Input features: \n{features}")
|
logger.info(f"Input features: \n{features}")
|
||||||
|
|
||||||
# 处理缺失值
|
# 处理缺失值
|
||||||
features_filled = np.array(features, dtype=float)
|
features_filled = np.array(features, dtype=float)
|
||||||
features_filled[np.isnan(features_filled)] = 0
|
features_filled[np.isnan(features_filled)] = 0
|
||||||
features_filled = np.nan_to_num(features_filled, 0)
|
features_filled = np.nan_to_num(features_filled, 0)
|
||||||
|
|
||||||
logging.info(f"Filled features: \n{features_filled}")
|
logger.info(f"Filled features: \n{features_filled}")
|
||||||
|
|
||||||
# 标准化特征
|
# 标准化特征
|
||||||
X = self.feature_scaler.transform(features_filled)
|
X = self.feature_scaler.transform(features_filled)
|
||||||
logging.info(f"Transformed features shape: {X.shape}")
|
logger.info(f"Transformed features shape: {X.shape}")
|
||||||
logging.info(f"Transformed features: \n{X}")
|
logger.info(f"Transformed features: \n{X}")
|
||||||
|
|
||||||
# 预测
|
# 预测
|
||||||
y_pred_scaled = self.best_model.predict(X)
|
y_pred_scaled = self.best_model.predict(X)
|
||||||
logging.info(f"Scaled prediction shape: {y_pred_scaled.shape}")
|
logger.info(f"Scaled prediction shape: {y_pred_scaled.shape}")
|
||||||
logging.info(f"Scaled prediction: {y_pred_scaled}")
|
logger.info(f"Scaled prediction: {y_pred_scaled}")
|
||||||
|
|
||||||
# 反标准化
|
# <EFBFBD><EFBFBD>标准化
|
||||||
y_pred = self.target_scaler.inverse_transform(y_pred_scaled.reshape(-1, 1))
|
y_pred = self.target_scaler.inverse_transform(y_pred_scaled.reshape(-1, 1))
|
||||||
logging.info(f"Final prediction shape: {y_pred.shape}")
|
logger.info(f"Final prediction shape: {y_pred.shape}")
|
||||||
logging.info(f"Final prediction: {y_pred}")
|
logger.info(f"Final prediction: {y_pred}")
|
||||||
|
|
||||||
# 记录标准化器的参数
|
# 记录标准化器的参数
|
||||||
logging.info("Target scaler params:")
|
logger.info("Target scaler params:")
|
||||||
logging.info(f"Mean: {self.target_scaler.mean_}")
|
logger.info(f"Mean: {self.target_scaler.mean_}")
|
||||||
logging.info(f"Scale: {self.target_scaler.scale_}")
|
logger.info(f"Scale: {self.target_scaler.scale_}")
|
||||||
|
|
||||||
return y_pred.ravel()
|
return y_pred.ravel()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error in prediction: {str(e)}")
|
logger.error(f"Error in prediction: {str(e)}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def _get_feature_importance(self, model):
|
||||||
|
"""
|
||||||
|
获取特征重要性
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not model:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# 获取特征名称
|
||||||
|
feature_analyzer = FeatureAnalysis()
|
||||||
|
feature_names = feature_analyzer.get_equipment_specific_features(self.equipment_type)
|
||||||
|
|
||||||
|
# 获取特<E58F96><E789B9><EFBFBD>重要性
|
||||||
|
if hasattr(model, 'feature_importances_'):
|
||||||
|
importances = model.feature_importances_
|
||||||
|
elif hasattr(model, 'coef_'):
|
||||||
|
if len(model.coef_.shape) > 1: # 如果是二维数组
|
||||||
|
importances = np.abs(model.coef_[0]) # 取第一行
|
||||||
|
else:
|
||||||
|
importances = np.abs(model.coef_)
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# 创建特征重要性字典
|
||||||
|
importance_dict = {}
|
||||||
|
for name, importance in zip(feature_names, importances):
|
||||||
|
importance_dict[name] = float(importance) # 确保转换为 Python 标量
|
||||||
|
|
||||||
|
# 按重要性降序排序
|
||||||
|
sorted_dict = dict(sorted(
|
||||||
|
importance_dict.items(),
|
||||||
|
key=lambda x: x[1],
|
||||||
|
reverse=True
|
||||||
|
))
|
||||||
|
|
||||||
|
# 过滤掉重要性为0的特征
|
||||||
|
return {k: v for k, v in sorted_dict.items() if v > 0}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting feature importance: {str(e)}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _calculate_confidence_interval(self, prediction, confidence=0.95):
|
||||||
|
"""
|
||||||
|
计算预测值的置信区间
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 使用预测值的20%作为标准差(增加不确定性)
|
||||||
|
std = abs(prediction) * 0.2
|
||||||
|
|
||||||
|
# 计算置信区间
|
||||||
|
from scipy import stats
|
||||||
|
interval = stats.norm.interval(confidence, loc=prediction, scale=std)
|
||||||
|
|
||||||
|
# 确保区间值为正数且合理
|
||||||
|
lower = max(1000, interval[0]) # 最小值设为1000元
|
||||||
|
upper = max(prediction * 1.2, interval[1]) # 至少比预测值大20%
|
||||||
|
|
||||||
|
logger.info(f"Calculated confidence interval: [{lower:.2f}, {upper:.2f}]")
|
||||||
|
|
||||||
|
return [lower, upper]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error calculating confidence interval: {str(e)}")
|
||||||
|
# 如果计算失败,返回基于20%的简单区间
|
||||||
|
lower = max(1000, prediction * 0.8)
|
||||||
|
upper = prediction * 1.2
|
||||||
|
return [lower, upper]
|
||||||
|
|
||||||
|
def get_model_type(self):
|
||||||
|
"""
|
||||||
|
获取当前模型的类型
|
||||||
|
"""
|
||||||
|
if isinstance(self.best_model, xgb.XGBRegressor):
|
||||||
|
return 'xgboost'
|
||||||
|
elif isinstance(self.best_model, lgb.LGBMRegressor):
|
||||||
|
return 'lightgbm'
|
||||||
|
elif isinstance(self.best_model, GradientBoostingRegressor):
|
||||||
|
return 'gbm'
|
||||||
|
elif isinstance(self.best_model, RandomForestRegressor):
|
||||||
|
return 'rf'
|
||||||
|
else:
|
||||||
|
return 'unknown'
|
||||||
|
|
||||||
|
def _get_pls_feature_importance(self):
|
||||||
|
"""
|
||||||
|
获取 PLS 模型的特征重要性
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not self.models['pls']:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# 获取特征名称
|
||||||
|
feature_analyzer = FeatureAnalysis()
|
||||||
|
feature_names = feature_analyzer.get_equipment_specific_features(self.equipment_type)
|
||||||
|
|
||||||
|
# 获取 PLS 模型的系数作为特征重要性
|
||||||
|
pls_model = self.models['pls']
|
||||||
|
if hasattr(pls_model, 'coef_'):
|
||||||
|
# 使用绝对值作为重要性指标
|
||||||
|
importances = np.abs(pls_model.coef_.ravel()) # 使用 ravel() 展平数组
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# 创建特征重要性字典
|
||||||
|
importance_dict = {}
|
||||||
|
for name, importance in zip(feature_names, importances):
|
||||||
|
importance_dict[name] = float(importance) # 确保转换为 Python 标量
|
||||||
|
|
||||||
|
# 按重要性降序排序
|
||||||
|
sorted_dict = dict(sorted(
|
||||||
|
importance_dict.items(),
|
||||||
|
key=lambda x: x[1],
|
||||||
|
reverse=True
|
||||||
|
))
|
||||||
|
|
||||||
|
# 过滤掉重要性为0的特征
|
||||||
|
return {k: v for k, v in sorted_dict.items() if v > 0}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting PLS feature importance: {str(e)}")
|
||||||
|
logger.error("Detailed traceback:", exc_info=True)
|
||||||
|
return {}
|
||||||
@ -1,313 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from sklearn.cross_decomposition import PLSRegression
|
|
||||||
from sklearn.preprocessing import StandardScaler
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
import logging
|
|
||||||
from sklearn.metrics import r2_score, mean_absolute_error
|
|
||||||
from sklearn.model_selection import LeaveOneOut
|
|
||||||
import os
|
|
||||||
from datetime import datetime
|
|
||||||
import joblib
|
|
||||||
from src.database.db_connection import get_db_connection
|
|
||||||
|
|
||||||
class PLSPredictor:
|
|
||||||
def __init__(self, n_components=2):
|
|
||||||
"""
|
|
||||||
初始化PLS回归模型
|
|
||||||
"""
|
|
||||||
self.model = PLSRegression(
|
|
||||||
n_components=n_components,
|
|
||||||
scale=True,
|
|
||||||
max_iter=500,
|
|
||||||
tol=1e-6
|
|
||||||
)
|
|
||||||
self.scaler_X = StandardScaler()
|
|
||||||
self.scaler_y = StandardScaler()
|
|
||||||
self.feature_names = None
|
|
||||||
self.model_path = None
|
|
||||||
|
|
||||||
# 尝试加载已训练的模型
|
|
||||||
self.load_model()
|
|
||||||
|
|
||||||
# 初始化示例数据
|
|
||||||
self._initialize_scalers()
|
|
||||||
|
|
||||||
def _initialize_scalers(self):
|
|
||||||
"""
|
|
||||||
使用示例数据初始化标准化器
|
|
||||||
"""
|
|
||||||
# 创建示例数据
|
|
||||||
example_data = pd.DataFrame({
|
|
||||||
'length_m': [0.56, 0.58, 0.54],
|
|
||||||
'width_m': [0.15, 0.16, 0.14],
|
|
||||||
'height_m': [0.20, 0.21, 0.19],
|
|
||||||
'weight_kg': [2.72, 2.85, 2.60],
|
|
||||||
'max_range_km': [24, 26, 22],
|
|
||||||
'max_speed_kmh': [160.93, 170, 155],
|
|
||||||
'cruise_speed_kmh': [96.56, 100, 93],
|
|
||||||
'flight_time_min': [15, 16, 14],
|
|
||||||
'folded_length_mm': [560, 580, 540],
|
|
||||||
'folded_width_mm': [150, 160, 140],
|
|
||||||
'folded_height_mm': [200, 210, 190]
|
|
||||||
})
|
|
||||||
|
|
||||||
# 初始化特征标准化器
|
|
||||||
self.scaler_X.fit(example_data)
|
|
||||||
|
|
||||||
# 初始化目标变量标准化器
|
|
||||||
example_costs = np.array([[1000000], [1100000], [900000]])
|
|
||||||
self.scaler_y.fit(example_costs)
|
|
||||||
|
|
||||||
def predict(self, features):
|
|
||||||
"""
|
|
||||||
使用PLS模型进行预测
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# 转换输入数据为DataFrame
|
|
||||||
if not isinstance(features, pd.DataFrame):
|
|
||||||
features = pd.DataFrame([features])
|
|
||||||
|
|
||||||
# 选择数值特征
|
|
||||||
numeric_features = features.select_dtypes(include=[np.number]).columns
|
|
||||||
X = features[numeric_features]
|
|
||||||
|
|
||||||
# 标准化特征
|
|
||||||
X_scaled = self.scaler_X.transform(X)
|
|
||||||
|
|
||||||
# 预测
|
|
||||||
y_pred_scaled = self.model.predict(X_scaled)
|
|
||||||
y_pred = self.scaler_y.inverse_transform(y_pred_scaled)
|
|
||||||
|
|
||||||
# 计算置信区间
|
|
||||||
ci = self._calculate_confidence_intervals(y_pred)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'predicted_cost': float(abs(y_pred[0][0])),
|
|
||||||
'confidence_interval': {
|
|
||||||
'lower': float(abs(ci['lower'])),
|
|
||||||
'upper': float(abs(ci['upper']))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error in PLS prediction: {str(e)}")
|
|
||||||
raise Exception(f"PLS prediction error: {str(e)}")
|
|
||||||
|
|
||||||
def fit(self, X, y):
|
|
||||||
"""
|
|
||||||
训练PLS模型
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
logging.info("=== PLS Training Start ===")
|
|
||||||
|
|
||||||
# 1. 检查输入数据
|
|
||||||
logging.info(f"Input X type: {type(X)}, shape: {X.shape if hasattr(X, 'shape') else 'no shape'}")
|
|
||||||
logging.info(f"Input y type: {type(y)}, shape: {y.shape if hasattr(y, 'shape') else 'no shape'}")
|
|
||||||
logging.info(f"X data:\n{X}")
|
|
||||||
logging.info(f"y data:\n{y}")
|
|
||||||
|
|
||||||
# 2. 转换为numpy数组
|
|
||||||
if isinstance(X, pd.DataFrame):
|
|
||||||
# 保存特征名称
|
|
||||||
self.feature_names = X.columns.tolist()
|
|
||||||
X = X.values
|
|
||||||
X = np.array(X, dtype=float)
|
|
||||||
y = np.array(y, dtype=float)
|
|
||||||
|
|
||||||
# 3. 标准化数据
|
|
||||||
logging.info("Standardizing data...")
|
|
||||||
X_scaled = self.scaler_X.fit_transform(X)
|
|
||||||
y_scaled = self.scaler_y.fit_transform(y.reshape(-1, 1))
|
|
||||||
logging.info(f"X_scaled shape: {X_scaled.shape}")
|
|
||||||
logging.info(f"y_scaled shape: {y_scaled.shape}")
|
|
||||||
|
|
||||||
# 4. 训练模型
|
|
||||||
logging.info("Training PLS model...")
|
|
||||||
self.model.fit(X_scaled, y_scaled.ravel())
|
|
||||||
logging.info("PLS model training completed")
|
|
||||||
|
|
||||||
# 5. 计算R²分数
|
|
||||||
logging.info("Calculating R² score...")
|
|
||||||
y_pred = self.model.predict(X_scaled)
|
|
||||||
y_pred = self.scaler_y.inverse_transform(y_pred.reshape(-1, 1))
|
|
||||||
r2 = r2_score(y.reshape(-1, 1), y_pred)
|
|
||||||
logging.info(f"R² score: {r2}")
|
|
||||||
|
|
||||||
result = {
|
|
||||||
'r2_score': float(r2),
|
|
||||||
'n_components': int(self.model.n_components),
|
|
||||||
'feature_importance': None
|
|
||||||
}
|
|
||||||
logging.info(f"Final result: {result}")
|
|
||||||
logging.info("=== PLS Training End ===")
|
|
||||||
|
|
||||||
# 保存训练好的模型
|
|
||||||
equipment_type = 'missile' # 或者从参数中获取
|
|
||||||
self.save_model(equipment_type)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error in PLS training: {str(e)}")
|
|
||||||
logging.error(f"Error traceback:", exc_info=True)
|
|
||||||
raise Exception(f"PLS training error: {str(e)}")
|
|
||||||
|
|
||||||
def _calculate_confidence_intervals(self, predictions, confidence=0.95):
|
|
||||||
"""
|
|
||||||
计算预测值的置信区间
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# 使用 bootstrap 方法计算置信区间
|
|
||||||
n_predictions = 1000
|
|
||||||
bootstrap_predictions = []
|
|
||||||
|
|
||||||
for _ in range(n_predictions):
|
|
||||||
# 添加随机噪声
|
|
||||||
noise = np.random.normal(0, predictions.mean() * 0.05, predictions.shape)
|
|
||||||
noisy_pred = predictions + noise
|
|
||||||
bootstrap_predictions.append(noisy_pred)
|
|
||||||
|
|
||||||
bootstrap_predictions = np.array(bootstrap_predictions).flatten()
|
|
||||||
|
|
||||||
# 计算置信区间
|
|
||||||
lower = np.percentile(bootstrap_predictions, ((1 - confidence) / 2) * 100)
|
|
||||||
upper = np.percentile(bootstrap_predictions, (1 - (1 - confidence) / 2) * 100)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'lower': float(lower),
|
|
||||||
'upper': float(upper)
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error calculating confidence intervals: {str(e)}")
|
|
||||||
# 如果计算失败,返回基于10%标准差的区间
|
|
||||||
mean_pred = np.mean(predictions)
|
|
||||||
return {
|
|
||||||
'lower': float(mean_pred * 0.9),
|
|
||||||
'upper': float(mean_pred * 1.1)
|
|
||||||
}
|
|
||||||
|
|
||||||
def _get_feature_importance(self):
|
|
||||||
"""
|
|
||||||
计算特征重要性
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if not hasattr(self.model, 'x_weights_'):
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# 获取 VIP 分数
|
|
||||||
t = self.model.x_scores_
|
|
||||||
w = self.model.x_weights_
|
|
||||||
q = self.model.y_loadings_
|
|
||||||
|
|
||||||
# 计算每个特征的 VIP 分数
|
|
||||||
m, p = w.shape
|
|
||||||
vips = np.zeros((p,))
|
|
||||||
|
|
||||||
s = np.diag(t.T @ t @ q.T @ q).reshape(m, -1)
|
|
||||||
total_s = np.sum(s)
|
|
||||||
|
|
||||||
for i in range(p):
|
|
||||||
weight = np.array([(w[j,i] / np.linalg.norm(w[:,i]))**2 for j in range(m)])
|
|
||||||
vips[i] = np.sqrt(p*(s.T @ weight)/total_s)
|
|
||||||
|
|
||||||
# 创建特征重要性字典
|
|
||||||
feature_importance = {}
|
|
||||||
for i, score in enumerate(vips):
|
|
||||||
feature_name = f"feature_{i}" if self.feature_names is None else self.feature_names[i]
|
|
||||||
feature_importance[feature_name] = float(score)
|
|
||||||
|
|
||||||
# 按重要性排序
|
|
||||||
return dict(sorted(feature_importance.items(), key=lambda x: x[1], reverse=True))
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error calculating feature importance: {str(e)}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def save_model(self, equipment_type):
|
|
||||||
"""
|
|
||||||
保存模型和标准化器
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
||||||
model_dir = 'models'
|
|
||||||
os.makedirs(model_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# 保存模型文件
|
|
||||||
model_path = f'{model_dir}/pls_{equipment_type}_{timestamp}'
|
|
||||||
joblib.dump({
|
|
||||||
'model': self.model,
|
|
||||||
'scaler_X': self.scaler_X,
|
|
||||||
'scaler_y': self.scaler_y,
|
|
||||||
'feature_names': self.feature_names
|
|
||||||
}, f'{model_path}.joblib')
|
|
||||||
|
|
||||||
# 更新数据库中的模型记录
|
|
||||||
with get_db_connection() as conn:
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
# 将之前的激活模型设置为非激活
|
|
||||||
cursor.execute("""
|
|
||||||
UPDATE trained_models
|
|
||||||
SET is_active = FALSE
|
|
||||||
WHERE equipment_type = %s AND model_type = 'pls'
|
|
||||||
""", (equipment_type,))
|
|
||||||
|
|
||||||
# 插入新的模型记录
|
|
||||||
cursor.execute("""
|
|
||||||
INSERT INTO trained_models (
|
|
||||||
model_name, model_type, equipment_type, model_path,
|
|
||||||
r2_score, training_date, is_active, created_by
|
|
||||||
) VALUES (%s, %s, %s, %s, %s, NOW(), TRUE, 'system')
|
|
||||||
""", (
|
|
||||||
f'PLS_{timestamp}',
|
|
||||||
'pls',
|
|
||||||
equipment_type,
|
|
||||||
f'{model_path}.joblib',
|
|
||||||
float(self.r2_score_)
|
|
||||||
))
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
self.model_path = f'{model_path}.joblib'
|
|
||||||
logging.info(f"Model saved to {self.model_path}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error saving model: {str(e)}")
|
|
||||||
raise Exception(f"Failed to save model: {str(e)}")
|
|
||||||
|
|
||||||
def load_model(self):
|
|
||||||
"""
|
|
||||||
加载最新的激活模型
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
with get_db_connection() as conn:
|
|
||||||
cursor = conn.cursor(dictionary=True)
|
|
||||||
|
|
||||||
# 获取最新的激活模型
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT * FROM trained_models
|
|
||||||
WHERE model_type = 'pls' AND is_active = TRUE
|
|
||||||
ORDER BY training_date DESC LIMIT 1
|
|
||||||
""")
|
|
||||||
|
|
||||||
model_record = cursor.fetchone()
|
|
||||||
|
|
||||||
if model_record and os.path.exists(model_record['model_path']):
|
|
||||||
# 加载模型文件
|
|
||||||
saved_data = joblib.load(model_record['model_path'])
|
|
||||||
self.model = saved_data['model']
|
|
||||||
self.scaler_X = saved_data['scaler_X']
|
|
||||||
self.scaler_y = saved_data['scaler_y']
|
|
||||||
self.feature_names = saved_data['feature_names']
|
|
||||||
self.model_path = model_record['model_path']
|
|
||||||
|
|
||||||
logging.info(f"Loaded model from {self.model_path}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error loading model: {str(e)}")
|
|
||||||
return False
|
|
||||||
364
src/routes.py
364
src/routes.py
@ -8,22 +8,18 @@ import numpy as np
|
|||||||
import mysql.connector
|
import mysql.connector
|
||||||
from sklearn.metrics import mean_absolute_error
|
from sklearn.metrics import mean_absolute_error
|
||||||
from .create_template import create_excel_template
|
from .create_template import create_excel_template
|
||||||
from .pls_regression import PLSPredictor
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from .data_preparation import DataPreparation
|
from .data_preparation import DataPreparation
|
||||||
from .model_trainer import ModelTrainer
|
from .model_trainer import ModelTrainer
|
||||||
|
from .logger import setup_logger
|
||||||
|
|
||||||
# 创建蓝图
|
# 创建蓝图
|
||||||
api_bp = Blueprint('api', __name__)
|
api_bp = Blueprint('api', __name__)
|
||||||
|
|
||||||
# 配置日志
|
# 获取logger
|
||||||
logging.basicConfig(
|
logger = setup_logger(__name__)
|
||||||
filename='logs/api.log',
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
|
||||||
)
|
|
||||||
|
|
||||||
@api_bp.route('/', methods=['GET'])
|
@api_bp.route('/', methods=['GET'])
|
||||||
def index():
|
def index():
|
||||||
@ -65,44 +61,43 @@ def predict_cost():
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
|
logger.info(f"Received prediction request for equipment type: {data.get('type')}")
|
||||||
# 记录请求
|
|
||||||
logging.info(f"Received prediction request for equipment type: {data.get('type', 'unknown')}")
|
|
||||||
logging.debug(f"Request data: {data}") # 添加详细的请求数据日志
|
|
||||||
|
|
||||||
# 验证装备类型
|
# 验证装备类型
|
||||||
if 'type' not in data:
|
if 'type' not in data:
|
||||||
return jsonify({'error': 'Equipment type is required'}), 400
|
return jsonify({'error': 'Equipment type is required'}), 400
|
||||||
|
|
||||||
# 根据装备类型验证必要参数
|
# 预测成本
|
||||||
required_params = get_required_params(data['type'])
|
|
||||||
|
|
||||||
for param in required_params:
|
|
||||||
if param not in data:
|
|
||||||
return jsonify({'error': f'Missing parameter: {param}'}), 400
|
|
||||||
|
|
||||||
# 预<><E9A284><EFBFBD>成本
|
|
||||||
predictor = CostPredictor()
|
predictor = CostPredictor()
|
||||||
result = predictor.predict(data)
|
result = predictor.predict(data)
|
||||||
|
|
||||||
# 记录预测结果
|
# 获取当前使用的模型信息
|
||||||
logging.info(f"Prediction completed: {result['predicted_cost']}")
|
with get_db_connection() as conn:
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT model_type, model_name, r2_score, mae, rmse
|
||||||
|
FROM trained_models
|
||||||
|
WHERE equipment_type = %s AND model_type != 'pls' AND is_active = TRUE
|
||||||
|
LIMIT 1
|
||||||
|
""", (data['type'],))
|
||||||
|
model_info = cursor.fetchone()
|
||||||
|
|
||||||
# 确保返回的数据格式正确
|
# 在结果中添加模型信息
|
||||||
response = {
|
result.update({
|
||||||
'predicted_cost': float(result['predicted_cost']),
|
'model_info': {
|
||||||
'confidence_interval': {
|
'type': model_info['model_type'],
|
||||||
'lower': float(result['confidence_interval']['lower']),
|
'name': model_info['model_name'],
|
||||||
'upper': float(result['confidence_interval']['upper'])
|
'r2_score': float(model_info['r2_score']),
|
||||||
|
'mae': float(model_info['mae']),
|
||||||
|
'rmse': float(model_info['rmse'])
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
logging.info(f"Sending response: {response}")
|
logger.info(f"Prediction completed: {result}")
|
||||||
return jsonify(response)
|
return jsonify(result)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error in prediction: {str(e)}")
|
logger.error(f"Error in prediction: {str(e)}")
|
||||||
logging.exception("Detailed error traceback:")
|
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@api_bp.route('/analyze-features', methods=['POST'])
|
@api_bp.route('/analyze-features', methods=['POST'])
|
||||||
@ -114,10 +109,10 @@ def analyze_features():
|
|||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
dataset_id = data.get('dataset_id')
|
dataset_id = data.get('dataset_id')
|
||||||
|
|
||||||
logging.info(f"Starting feature analysis for dataset {dataset_id}")
|
logger.info(f"Starting feature analysis for dataset {dataset_id}")
|
||||||
|
|
||||||
if not dataset_id:
|
if not dataset_id:
|
||||||
logging.warning("No dataset_id provided")
|
logger.warning("No dataset_id provided")
|
||||||
return jsonify({'error': '请选择数据集'}), 400
|
return jsonify({'error': '请选择数据集'}), 400
|
||||||
|
|
||||||
with get_db_connection() as conn:
|
with get_db_connection() as conn:
|
||||||
@ -136,10 +131,10 @@ def analyze_features():
|
|||||||
dataset = cursor.fetchone()
|
dataset = cursor.fetchone()
|
||||||
|
|
||||||
if not dataset:
|
if not dataset:
|
||||||
logging.warning(f"Dataset {dataset_id} not found")
|
logger.warning(f"Dataset {dataset_id} not found")
|
||||||
return jsonify({'error': '数据集不存在'}), 404
|
return jsonify({'error': '数据集不存在'}), 404
|
||||||
|
|
||||||
logging.info(f"Dataset info: {dataset}")
|
logger.info(f"Dataset info: {dataset}")
|
||||||
|
|
||||||
# 创建特征分析实例
|
# 创建特征分析实例
|
||||||
from src.feature_analysis import FeatureAnalysis
|
from src.feature_analysis import FeatureAnalysis
|
||||||
@ -147,7 +142,7 @@ def analyze_features():
|
|||||||
|
|
||||||
# 获取特征列表
|
# 获取特征列表
|
||||||
feature_names = analyzer.get_equipment_specific_features(dataset['equipment_type'])
|
feature_names = analyzer.get_equipment_specific_features(dataset['equipment_type'])
|
||||||
logging.info(f"Feature names: {feature_names}")
|
logger.info(f"Feature names: {feature_names}")
|
||||||
|
|
||||||
# 获取数据集中的装备数据
|
# 获取数据集中的装备数据
|
||||||
if dataset['equipment_type'] == '火箭炮':
|
if dataset['equipment_type'] == '火箭炮':
|
||||||
@ -174,10 +169,10 @@ def analyze_features():
|
|||||||
""", (dataset_id,))
|
""", (dataset_id,))
|
||||||
|
|
||||||
equipment_data = cursor.fetchall()
|
equipment_data = cursor.fetchall()
|
||||||
logging.info(f"Found {len(equipment_data)} equipment records")
|
logger.info(f"Found {len(equipment_data)} equipment records")
|
||||||
|
|
||||||
if not equipment_data:
|
if not equipment_data:
|
||||||
logging.warning("No valid equipment data found in dataset")
|
logger.warning("No valid equipment data found in dataset")
|
||||||
return jsonify({'error': '数据集没有有效的成本数据'}), 400
|
return jsonify({'error': '数据集没有有效的成本数据'}), 400
|
||||||
|
|
||||||
# 统计每个特征的缺失率
|
# 统计每个特征的缺失率
|
||||||
@ -186,11 +181,11 @@ def analyze_features():
|
|||||||
missing_count = sum(1 for item in equipment_data if item.get(name) is None)
|
missing_count = sum(1 for item in equipment_data if item.get(name) is None)
|
||||||
missing_rate = missing_count / len(equipment_data)
|
missing_rate = missing_count / len(equipment_data)
|
||||||
missing_rates[name] = missing_rate
|
missing_rates[name] = missing_rate
|
||||||
logging.info(f"Feature {name} missing rate: {missing_rate:.2%}")
|
logger.info(f"Feature {name} missing rate: {missing_rate:.2%}")
|
||||||
|
|
||||||
# 过滤掉缺失率过高的特征
|
# 过滤掉缺失率过高的特征
|
||||||
valid_features = [name for name in feature_names if missing_rates[name] < 0.7]
|
valid_features = [name for name in feature_names if missing_rates[name] < 0.7]
|
||||||
logging.info(f"Valid features after filtering: {valid_features}")
|
logger.info(f"Valid features after filtering: {valid_features}")
|
||||||
|
|
||||||
if len(valid_features) < 3: # 至少需要3个特征
|
if len(valid_features) < 3: # 至少需要3个特征
|
||||||
return jsonify({'error': '有效特征数量不足'}), 400
|
return jsonify({'error': '有效特征数量不足'}), 400
|
||||||
@ -200,7 +195,7 @@ def analyze_features():
|
|||||||
for name in valid_features:
|
for name in valid_features:
|
||||||
values = [float(item[name]) for item in equipment_data if item.get(name) is not None]
|
values = [float(item[name]) for item in equipment_data if item.get(name) is not None]
|
||||||
feature_means[name] = sum(values) / len(values) if values else 0
|
feature_means[name] = sum(values) / len(values) if values else 0
|
||||||
logging.info(f"Feature {name} mean value: {feature_means[name]:.2f}")
|
logger.info(f"Feature {name} mean value: {feature_means[name]:.2f}")
|
||||||
|
|
||||||
# 准备特征和目标值
|
# 准备特征和目标值
|
||||||
features = []
|
features = []
|
||||||
@ -215,32 +210,32 @@ def analyze_features():
|
|||||||
# 确保数值类型转换正确
|
# 确保数值类型转换正确
|
||||||
feature_values.append(float(value) if value is not None else feature_means[name])
|
feature_values.append(float(value) if value is not None else feature_means[name])
|
||||||
except (ValueError, TypeError) as e:
|
except (ValueError, TypeError) as e:
|
||||||
logging.error(f"Error converting value for feature {name}: {value}")
|
logger.error(f"Error converting value for feature {name}: {value}")
|
||||||
logging.error(f"Error details: {str(e)}")
|
logger.error(f"Error details: {str(e)}")
|
||||||
return jsonify({'error': f'特征 {name} 的值 {value} 无法转换为数值'}), 400
|
return jsonify({'error': f'特征 {name} 的值 {value} 无法转换为数值'}), 400
|
||||||
features.append(feature_values)
|
features.append(feature_values)
|
||||||
|
|
||||||
# 确保成本值是数值类型
|
# 确保成本值是值类型
|
||||||
try:
|
try:
|
||||||
target.append(float(item['actual_cost']))
|
target.append(float(item['actual_cost']))
|
||||||
except (ValueError, TypeError) as e:
|
except (ValueError, TypeError) as e:
|
||||||
logging.error(f"Error converting actual_cost: {item['actual_cost']}")
|
logger.error(f"Error converting actual_cost: {item['actual_cost']}")
|
||||||
logging.error(f"Error details: {str(e)}")
|
logger.error(f"Error details: {str(e)}")
|
||||||
return jsonify({'error': '成本值无法换为数值'}), 400
|
return jsonify({'error': '成本值无法换为数值'}), 400
|
||||||
|
|
||||||
logging.info(f"Prepared {len(features)} feature vectors")
|
logger.info(f"Prepared {len(features)} feature vectors")
|
||||||
logging.info(f"First feature vector: {features[0] if features else None}")
|
logger.info(f"First feature vector: {features[0] if features else None}")
|
||||||
logging.info(f"First target value: {target[0] if target else None}")
|
logger.info(f"First target value: {target[0] if target else None}")
|
||||||
|
|
||||||
# 调用特征分析方法
|
# 调用特征分析方法
|
||||||
result = analyzer.analyze_features(features, target, valid_features)
|
result = analyzer.analyze_features(features, target, valid_features)
|
||||||
logging.info("Analysis completed successfully")
|
logger.info("Analysis completed successfully")
|
||||||
|
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error analyzing features: {str(e)}")
|
logger.error(f"Error analyzing features: {str(e)}")
|
||||||
logging.error("Detailed traceback:", exc_info=True)
|
logger.error("Detailed traceback:", exc_info=True)
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@api_bp.route('/train', methods=['POST'])
|
@api_bp.route('/train', methods=['POST'])
|
||||||
@ -250,15 +245,15 @@ def train_model():
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
|
logger.info(f"Starting model training for {data.get('type')}")
|
||||||
equipment_type = data.get('type')
|
equipment_type = data.get('type')
|
||||||
train_dataset_id = data.get('train_dataset_id')
|
train_dataset_id = data.get('train_dataset_id')
|
||||||
validation_dataset_id = data.get('validation_dataset_id')
|
validation_dataset_id = data.get('validation_dataset_id')
|
||||||
models = data.get('models', [])
|
models = data.get('models', [])
|
||||||
|
|
||||||
logging.info(f"Starting model training for {equipment_type}")
|
logger.info(f"Training dataset: {train_dataset_id}")
|
||||||
logging.info(f"Training dataset: {train_dataset_id}")
|
logger.info(f"Validation dataset: {validation_dataset_id}")
|
||||||
logging.info(f"Validation dataset: {validation_dataset_id}")
|
logger.info(f"Selected models: {models}")
|
||||||
logging.info(f"Selected models: {models}")
|
|
||||||
|
|
||||||
# 获取训练数据
|
# 获取训练数据
|
||||||
with get_db_connection() as conn:
|
with get_db_connection() as conn:
|
||||||
@ -357,8 +352,8 @@ def train_model():
|
|||||||
return jsonify(training_result)
|
return jsonify(training_result)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error in model training: {str(e)}")
|
logger.error(f"Error in model training: {str(e)}")
|
||||||
logging.error("Detailed traceback:", exc_info=True)
|
logger.error("Detailed traceback:", exc_info=True)
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@api_bp.route('/evaluate', methods=['POST'])
|
@api_bp.route('/evaluate', methods=['POST'])
|
||||||
@ -368,7 +363,7 @@ def evaluate_model():
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
logging.info("Received model evaluation request")
|
logger.info("Received model evaluation request")
|
||||||
|
|
||||||
if 'test_data' not in data:
|
if 'test_data' not in data:
|
||||||
return jsonify({'error': 'Test data is required'}), 400
|
return jsonify({'error': 'Test data is required'}), 400
|
||||||
@ -379,11 +374,11 @@ def evaluate_model():
|
|||||||
data['test_data']['predicted']
|
data['test_data']['predicted']
|
||||||
)
|
)
|
||||||
|
|
||||||
logging.info("Model evaluation completed")
|
logger.info("Model evaluation completed")
|
||||||
return jsonify(evaluation_result)
|
return jsonify(evaluation_result)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error in model evaluation: {str(e)}")
|
logger.error(f"Error in model evaluation: {str(e)}")
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
def get_required_params(equipment_type):
|
def get_required_params(equipment_type):
|
||||||
@ -424,7 +419,7 @@ def not_found(error):
|
|||||||
|
|
||||||
@api_bp.errorhandler(500)
|
@api_bp.errorhandler(500)
|
||||||
def internal_error(error):
|
def internal_error(error):
|
||||||
logging.error(f"Internal server error: {str(error)}")
|
logger.error(f"Internal server error: {str(error)}")
|
||||||
return jsonify({'error': 'Internal server error'}), 500
|
return jsonify({'error': 'Internal server error'}), 500
|
||||||
|
|
||||||
@api_bp.route('/data', methods=['GET'])
|
@api_bp.route('/data', methods=['GET'])
|
||||||
@ -446,10 +441,10 @@ def get_equipment_data():
|
|||||||
LIMIT 5
|
LIMIT 5
|
||||||
""")
|
""")
|
||||||
test_params = cursor.fetchall()
|
test_params = cursor.fetchall()
|
||||||
logging.info(f"Test custom params: {test_params}")
|
logger.info(f"Test custom params: {test_params}")
|
||||||
|
|
||||||
# 获取火箭炮数据
|
# 获取火箭炮数据
|
||||||
logging.info("Fetching rocket artillery data...")
|
logger.info("Fetching rocket artillery data...")
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT
|
SELECT
|
||||||
e.id,
|
e.id,
|
||||||
@ -503,13 +498,13 @@ def get_equipment_data():
|
|||||||
WHERE e.type = '火箭炮'
|
WHERE e.type = '火箭炮'
|
||||||
""")
|
""")
|
||||||
rocket_artillery = cursor.fetchall()
|
rocket_artillery = cursor.fetchall()
|
||||||
logging.info(f"Found {len(rocket_artillery)} rocket artillery records")
|
logger.info(f"Found {len(rocket_artillery)} rocket artillery records")
|
||||||
if rocket_artillery:
|
if rocket_artillery:
|
||||||
logging.info(f"First rocket artillery: {rocket_artillery[0]['name']}")
|
logger.info(f"First rocket artillery: {rocket_artillery[0]['name']}")
|
||||||
logging.info(f"First rocket custom_params: {rocket_artillery[0].get('custom_params')}")
|
logger.info(f"First rocket custom_params: {rocket_artillery[0].get('custom_params')}")
|
||||||
|
|
||||||
# 获取巡飞弹数据
|
# 获取巡飞弹数据
|
||||||
logging.info("Fetching missile data...")
|
logger.info("Fetching missile data...")
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT
|
SELECT
|
||||||
e.id,
|
e.id,
|
||||||
@ -560,28 +555,28 @@ def get_equipment_data():
|
|||||||
WHERE e.type = '巡飞弹'
|
WHERE e.type = '巡飞弹'
|
||||||
""")
|
""")
|
||||||
loitering_munition = cursor.fetchall()
|
loitering_munition = cursor.fetchall()
|
||||||
logging.info(f"Found {len(loitering_munition)} missile records")
|
logger.info(f"Found {len(loitering_munition)} missile records")
|
||||||
if loitering_munition:
|
if loitering_munition:
|
||||||
logging.info(f"First missile: {loitering_munition[0]['name']}")
|
logger.info(f"First missile: {loitering_munition[0]['name']}")
|
||||||
logging.info(f"First missile custom_params: {loitering_munition[0].get('custom_params')}")
|
logger.info(f"First missile custom_params: {loitering_munition[0].get('custom_params')}")
|
||||||
|
|
||||||
# 处理 custom_params,<EFBFBD><EFBFBD><EFBFBD>保不为 NULL
|
# 处理 custom_params,保为 NULL
|
||||||
for item in rocket_artillery + loitering_munition:
|
for item in rocket_artillery + loitering_munition:
|
||||||
if item['custom_params'] is None:
|
if item['custom_params'] is None:
|
||||||
item['custom_params'] = []
|
item['custom_params'] = []
|
||||||
logging.debug(f"Set empty custom_params for equipment {item['id']}")
|
logger.debug(f"Set empty custom_params for equipment {item['id']}")
|
||||||
else:
|
else:
|
||||||
logging.debug(f"Equipment {item['id']} has {len(item['custom_params'])} custom params")
|
logger.debug(f"Equipment {item['id']} has {len(item['custom_params'])} custom params")
|
||||||
|
|
||||||
logging.info("Data fetching completed")
|
logger.info("Data fetching completed")
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'rocket_artillery': rocket_artillery,
|
'rocket_artillery': rocket_artillery,
|
||||||
'loitering_munition': loitering_munition
|
'loitering_munition': loitering_munition
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error getting equipment data: {str(e)}")
|
logger.error(f"Error getting equipment data: {str(e)}")
|
||||||
logging.error("Detailed traceback:", exc_info=True)
|
logger.error("Detailed traceback:", exc_info=True)
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@api_bp.route('/data/<int:id>', methods=['DELETE'])
|
@api_bp.route('/data/<int:id>', methods=['DELETE'])
|
||||||
@ -607,7 +602,7 @@ def delete_equipment(id):
|
|||||||
return jsonify({'status': 'success'})
|
return jsonify({'status': 'success'})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error deleting equipment: {str(e)}")
|
logger.error(f"Error deleting equipment: {str(e)}")
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@api_bp.route('/data/template', methods=['GET'])
|
@api_bp.route('/data/template', methods=['GET'])
|
||||||
@ -633,7 +628,7 @@ def download_template():
|
|||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error creating template: {str(e)}")
|
logger.error(f"Error creating template: {str(e)}")
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
def get_db_connection():
|
def get_db_connection():
|
||||||
@ -654,115 +649,60 @@ def pls_predict():
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
|
logger.info(f"Received PLS prediction request for equipment type: {data.get('type')}")
|
||||||
# 记录请求
|
|
||||||
logging.info(f"Received PLS prediction request for equipment type: {data.get('type', 'unknown')}")
|
|
||||||
logging.debug(f"Request data: {data}")
|
|
||||||
|
|
||||||
# 验证装备类型
|
# 验证装备类型
|
||||||
if 'type' not in data:
|
if 'type' not in data:
|
||||||
return jsonify({'error': 'Equipment type is required'}), 400
|
return jsonify({'error': 'Equipment type is required'}), 400
|
||||||
|
|
||||||
# 创建PLS预测器
|
# 使用 ModelTrainer 中的 PLS 模型进行预测
|
||||||
predictor = PLSPredictor()
|
trainer = ModelTrainer()
|
||||||
result = predictor.predict(data)
|
if not trainer.load_model(data['type'], model_type='pls'): # 指定加载 PLS 模型
|
||||||
|
return jsonify({'error': '未找到可用的模型'}), 404
|
||||||
|
|
||||||
|
# 准备特征数据
|
||||||
|
feature_analyzer = FeatureAnalysis()
|
||||||
|
features = feature_analyzer.get_equipment_specific_features(data['type'])
|
||||||
|
X = np.array([[data.get(feature) for feature in features]])
|
||||||
|
|
||||||
|
# 预测
|
||||||
|
result = trainer.predict(X)
|
||||||
|
|
||||||
|
# 计算置信区间
|
||||||
|
confidence_interval = trainer._calculate_confidence_interval(result[0])
|
||||||
|
|
||||||
|
# 获取模型信息
|
||||||
|
with get_db_connection() as conn:
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT model_type, model_name, r2_score, mae, rmse
|
||||||
|
FROM trained_models
|
||||||
|
WHERE equipment_type = %s AND model_type = 'pls' AND is_active = TRUE
|
||||||
|
LIMIT 1
|
||||||
|
""", (data['type'],))
|
||||||
|
model_info = cursor.fetchone()
|
||||||
|
|
||||||
# 确保返回的数据可以序列化为JSON
|
# 确保返回的数据可以序列化为JSON
|
||||||
response = {
|
response = {
|
||||||
'predicted_cost': float(result['predicted_cost']),
|
'predicted_cost': float(result[0]),
|
||||||
|
'model_info': {
|
||||||
|
'type': model_info['model_type'],
|
||||||
|
'name': model_info['model_name'],
|
||||||
|
'r2_score': model_info['r2_score'],
|
||||||
|
'mae': model_info['mae'],
|
||||||
|
'rmse': model_info['rmse']
|
||||||
|
},
|
||||||
'confidence_interval': {
|
'confidence_interval': {
|
||||||
'lower': float(result['confidence_interval']['lower']),
|
'lower': float(confidence_interval[0]),
|
||||||
'upper': float(result['confidence_interval']['upper'])
|
'upper': float(confidence_interval[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# 如果有特征重要性数据也进行转换
|
logger.info(f"PLS prediction completed: {response}")
|
||||||
if 'feature_importance' in result:
|
|
||||||
response['feature_importance'] = {
|
|
||||||
k: float(v) for k, v in result['feature_importance'].items()
|
|
||||||
}
|
|
||||||
|
|
||||||
logging.info(f"PLS prediction completed: {response}")
|
|
||||||
return jsonify(response)
|
return jsonify(response)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error in PLS prediction: {str(e)}")
|
logger.error(f"Error in PLS prediction: {str(e)}")
|
||||||
return jsonify({'error': str(e)}), 500
|
|
||||||
|
|
||||||
@api_bp.route('/pls/train', methods=['POST'])
|
|
||||||
def pls_train():
|
|
||||||
"""
|
|
||||||
PLS模型训练接口
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# 检查请求类型
|
|
||||||
if request.content_type and 'multipart/form-data' in request.content_type:
|
|
||||||
# 处理文件上传
|
|
||||||
if 'file' not in request.files:
|
|
||||||
return jsonify({'error': '没有上传文件'}), 400
|
|
||||||
|
|
||||||
file = request.files['file']
|
|
||||||
if not file.filename.endswith(('.xls', '.xlsx')):
|
|
||||||
return jsonify({'error': '请上传Excel文件'}), 400
|
|
||||||
|
|
||||||
# 读取Excel文件
|
|
||||||
df = pd.read_excel(file, sheet_name='火箭炮基本参数')
|
|
||||||
logging.info(f"Excel data columns: {df.columns}")
|
|
||||||
|
|
||||||
# 获取数值列
|
|
||||||
numeric_features = df.select_dtypes(include=[np.number]).columns.tolist()
|
|
||||||
|
|
||||||
# 检查是否存在成本列
|
|
||||||
cost_column = None
|
|
||||||
for col in df.columns:
|
|
||||||
if '成本' in col or 'cost' in col.lower():
|
|
||||||
cost_column = col
|
|
||||||
numeric_features.remove(col)
|
|
||||||
break
|
|
||||||
|
|
||||||
if not cost_column:
|
|
||||||
raise ValueError("Excel文件中未找到成本列")
|
|
||||||
|
|
||||||
# 准备训练数据
|
|
||||||
X = df[numeric_features].values
|
|
||||||
y = df[cost_column].values
|
|
||||||
|
|
||||||
else:
|
|
||||||
# 处理JSON数
|
|
||||||
data = request.get_json()
|
|
||||||
logging.info(f"Received PLS training data: {data}")
|
|
||||||
|
|
||||||
# 将训练数据转换为DataFrame
|
|
||||||
training_data = pd.DataFrame(data['training_data'])
|
|
||||||
|
|
||||||
# 取值列
|
|
||||||
numeric_features = training_data.select_dtypes(include=[np.number]).columns.tolist()
|
|
||||||
|
|
||||||
# 准备特征矩阵X和目标变量y
|
|
||||||
X = training_data[numeric_features].values
|
|
||||||
y = np.array(data['actual_costs'])
|
|
||||||
|
|
||||||
logging.info(f"X shape: {X.shape}")
|
|
||||||
logging.info(f"y shape: {y.shape}")
|
|
||||||
|
|
||||||
# 创建并训练PLS预测器
|
|
||||||
predictor = PLSPredictor()
|
|
||||||
result = predictor.fit(X, y)
|
|
||||||
|
|
||||||
# 确保返回的数据可以序列化为JSON
|
|
||||||
response = {
|
|
||||||
'r2_score': float(result['r2_score']),
|
|
||||||
'n_components': int(result['n_components']),
|
|
||||||
'feature_importance': {
|
|
||||||
str(k): float(v) for k, v in result['feature_importance'].items()
|
|
||||||
} if result['feature_importance'] else {}
|
|
||||||
}
|
|
||||||
|
|
||||||
logging.info(f"Training completed: {response}")
|
|
||||||
return jsonify(response)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error in PLS training: {str(e)}")
|
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@api_bp.route('/data/import', methods=['POST'])
|
@api_bp.route('/data/import', methods=['POST'])
|
||||||
@ -794,7 +734,7 @@ def import_data():
|
|||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error importing data: {str(e)}")
|
logger.error(f"Error importing data: {str(e)}")
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@api_bp.route('/data/<int:id>', methods=['PUT'])
|
@api_bp.route('/data/<int:id>', methods=['PUT'])
|
||||||
@ -804,8 +744,8 @@ def update_equipment(id):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
logging.info(f"Updating equipment ID: {id}")
|
logger.info(f"Updating equipment ID: {id}")
|
||||||
logging.info(f"Update data: {data}")
|
logger.info(f"Update data: {data}")
|
||||||
|
|
||||||
with get_db_connection() as conn:
|
with get_db_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
@ -816,7 +756,7 @@ def update_equipment(id):
|
|||||||
SET name = %s, manufacturer = %s
|
SET name = %s, manufacturer = %s
|
||||||
WHERE id = %s
|
WHERE id = %s
|
||||||
""", (data['name'], data['manufacturer'], id))
|
""", (data['name'], data['manufacturer'], id))
|
||||||
logging.info("Basic info updated")
|
logger.info("Basic info updated")
|
||||||
|
|
||||||
# 更新通用参数
|
# 更新通用参数
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
@ -828,7 +768,7 @@ def update_equipment(id):
|
|||||||
data['length_m'], data['width_m'], data['height_m'],
|
data['length_m'], data['width_m'], data['height_m'],
|
||||||
data['weight_kg'], data['max_range_km'], id
|
data['weight_kg'], data['max_range_km'], id
|
||||||
))
|
))
|
||||||
logging.info("Common params updated")
|
logger.info("Common params updated")
|
||||||
|
|
||||||
# 根据备类型更新特有参数
|
# 根据备类型更新特有参数
|
||||||
if data['type'] == '火箭炮':
|
if data['type'] == '火箭炮':
|
||||||
@ -843,7 +783,7 @@ def update_equipment(id):
|
|||||||
data['rocket_length_m'], data['rocket_diameter_mm'],
|
data['rocket_length_m'], data['rocket_diameter_mm'],
|
||||||
data['rocket_weight_kg'], data['rate_of_fire'], id
|
data['rocket_weight_kg'], data['rate_of_fire'], id
|
||||||
))
|
))
|
||||||
logging.info("Rocket artillery params updated")
|
logger.info("Rocket artillery params updated")
|
||||||
else:
|
else:
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
UPDATE loitering_munition_params
|
UPDATE loitering_munition_params
|
||||||
@ -858,7 +798,7 @@ def update_equipment(id):
|
|||||||
data['launch_mode'], data['folded_length_mm'],
|
data['launch_mode'], data['folded_length_mm'],
|
||||||
data['folded_width_mm'], data['folded_height_mm'], id
|
data['folded_width_mm'], data['folded_height_mm'], id
|
||||||
))
|
))
|
||||||
logging.info("Missile params updated")
|
logger.info("Missile params updated")
|
||||||
|
|
||||||
# 更新成本数据
|
# 更新成本数据
|
||||||
if 'actual_cost' in data:
|
if 'actual_cost' in data:
|
||||||
@ -867,27 +807,27 @@ def update_equipment(id):
|
|||||||
SET actual_cost = %s
|
SET actual_cost = %s
|
||||||
WHERE equipment_id = %s
|
WHERE equipment_id = %s
|
||||||
""", (data['actual_cost'], id))
|
""", (data['actual_cost'], id))
|
||||||
logging.info("Cost data updated")
|
logger.info("Cost data updated")
|
||||||
|
|
||||||
# 更新特殊参数
|
# 更新特殊参数
|
||||||
if 'custom_params' in data and data['custom_params']:
|
if 'custom_params' in data and data['custom_params']:
|
||||||
logging.info(f"Updating custom params: {data['custom_params']}")
|
logger.info(f"Updating custom params: {data['custom_params']}")
|
||||||
for param in data['custom_params']:
|
for param in data['custom_params']:
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
UPDATE custom_params
|
UPDATE custom_params
|
||||||
SET param_value = %s
|
SET param_value = %s
|
||||||
WHERE id = %s AND equipment_id = %s
|
WHERE id = %s AND equipment_id = %s
|
||||||
""", (param['param_value'], param['id'], id))
|
""", (param['param_value'], param['id'], id))
|
||||||
logging.info("Custom params updated")
|
logger.info("Custom params updated")
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
logging.info("All updates committed successfully")
|
logger.info("All updates committed successfully")
|
||||||
|
|
||||||
return jsonify({'success': True})
|
return jsonify({'success': True})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error updating equipment: {str(e)}")
|
logger.error(f"Error updating equipment: {str(e)}")
|
||||||
logging.error("Detailed traceback:", exc_info=True)
|
logger.error("Detailed traceback:", exc_info=True)
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@api_bp.route('/data/details/<int:id>', methods=['GET'])
|
@api_bp.route('/data/details/<int:id>', methods=['GET'])
|
||||||
@ -896,7 +836,7 @@ def get_equipment_details(id):
|
|||||||
获取装备详数据
|
获取装备详数据
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logging.info(f"Getting details for equipment ID: {id}")
|
logger.info(f"Getting details for equipment ID: {id}")
|
||||||
|
|
||||||
with get_db_connection() as conn:
|
with get_db_connection() as conn:
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
@ -906,11 +846,11 @@ def get_equipment_details(id):
|
|||||||
equipment = cursor.fetchone()
|
equipment = cursor.fetchone()
|
||||||
|
|
||||||
if not equipment:
|
if not equipment:
|
||||||
logging.warning(f"Equipment not found: {id}")
|
logger.warning(f"Equipment not found: {id}")
|
||||||
return jsonify({'error': 'Equipment not found'}), 404
|
return jsonify({'error': 'Equipment not found'}), 404
|
||||||
|
|
||||||
equipment_type = equipment['type']
|
equipment_type = equipment['type']
|
||||||
logging.info(f"Equipment type: {equipment_type}")
|
logger.info(f"Equipment type: {equipment_type}")
|
||||||
|
|
||||||
# 根据装备类型选择查询
|
# 根据装备类型选择查询
|
||||||
if equipment_type == '火箭炮':
|
if equipment_type == '火箭炮':
|
||||||
@ -984,13 +924,13 @@ def get_equipment_details(id):
|
|||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
logging.info(f"Found equipment details: {result['name']}")
|
logger.info(f"Found equipment details: {result['name']}")
|
||||||
logging.info(f"Custom params: {result.get('custom_params')}")
|
logger.info(f"Custom params: {result.get('custom_params')}")
|
||||||
|
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error getting equipment details: {str(e)}")
|
logger.error(f"Error getting equipment details: {str(e)}")
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
# 添加数据集相关的路由
|
# 添加数据集相关的路由
|
||||||
@ -1022,7 +962,7 @@ def get_datasets():
|
|||||||
|
|
||||||
return jsonify(datasets)
|
return jsonify(datasets)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error getting datasets: {str(e)}")
|
logger.error(f"Error getting datasets: {str(e)}")
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@api_bp.route('/datasets/<int:id>', methods=['GET'])
|
@api_bp.route('/datasets/<int:id>', methods=['GET'])
|
||||||
@ -1076,7 +1016,7 @@ def get_dataset(id):
|
|||||||
dataset['equipment'] = equipment
|
dataset['equipment'] = equipment
|
||||||
return jsonify(dataset)
|
return jsonify(dataset)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error getting dataset: {str(e)}")
|
logger.error(f"Error getting dataset: {str(e)}")
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@api_bp.route('/datasets', methods=['POST'])
|
@api_bp.route('/datasets', methods=['POST'])
|
||||||
@ -1108,7 +1048,7 @@ def create_dataset():
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
return jsonify({'id': dataset_id, 'message': '数据集创建成功'})
|
return jsonify({'id': dataset_id, 'message': '数据集创建成功'})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error creating dataset: {str(e)}")
|
logger.error(f"Error creating dataset: {str(e)}")
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@api_bp.route('/datasets/<int:id>', methods=['PUT'])
|
@api_bp.route('/datasets/<int:id>', methods=['PUT'])
|
||||||
@ -1142,7 +1082,7 @@ def update_dataset(id):
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
return jsonify({'success': True})
|
return jsonify({'success': True})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error updating dataset: {str(e)}")
|
logger.error(f"Error updating dataset: {str(e)}")
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@api_bp.route('/datasets/<int:id>', methods=['DELETE'])
|
@api_bp.route('/datasets/<int:id>', methods=['DELETE'])
|
||||||
@ -1163,13 +1103,13 @@ def delete_dataset(id):
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
return jsonify({'success': True})
|
return jsonify({'success': True})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error deleting dataset: {str(e)}")
|
logger.error(f"Error deleting dataset: {str(e)}")
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@api_bp.route('/models/<equipment_type>/latest', methods=['GET'])
|
@api_bp.route('/models/<equipment_type>/latest', methods=['GET'])
|
||||||
def get_latest_model(equipment_type):
|
def get_latest_model(equipment_type):
|
||||||
"""
|
"""
|
||||||
获取最新训练的<EFBFBD><EFBFBD><EFBFBD>型信息
|
获取最新训练的型信息
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with get_db_connection() as conn:
|
with get_db_connection() as conn:
|
||||||
@ -1184,7 +1124,7 @@ def get_latest_model(equipment_type):
|
|||||||
return jsonify(model)
|
return jsonify(model)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error getting latest model: {str(e)}")
|
logger.error(f"Error getting latest model: {str(e)}")
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@api_bp.route('/models', methods=['GET'])
|
@api_bp.route('/models', methods=['GET'])
|
||||||
@ -1218,7 +1158,7 @@ def get_models():
|
|||||||
return jsonify(models)
|
return jsonify(models)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error getting models: {str(e)}")
|
logger.error(f"Error getting models: {str(e)}")
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@api_bp.route('/models/<int:id>/activate', methods=['POST'])
|
@api_bp.route('/models/<int:id>/activate', methods=['POST'])
|
||||||
@ -1258,7 +1198,7 @@ def activate_model(id):
|
|||||||
return jsonify({'success': True})
|
return jsonify({'success': True})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error activating model: {str(e)}")
|
logger.error(f"Error activating model: {str(e)}")
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@api_bp.route('/models/<int:id>', methods=['DELETE'])
|
@api_bp.route('/models/<int:id>', methods=['DELETE'])
|
||||||
@ -1294,5 +1234,23 @@ def delete_model(id):
|
|||||||
return jsonify({'success': True})
|
return jsonify({'success': True})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error deleting model: {str(e)}")
|
logger.error(f"Error deleting model: {str(e)}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@api_bp.route('/predict/all', methods=['POST'])
|
||||||
|
def predict_all():
|
||||||
|
"""
|
||||||
|
获取所有机器学习模型的预测结果
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
logger.info(f"Received prediction request for all models, equipment type: {data.get('type')}")
|
||||||
|
|
||||||
|
predictor = CostPredictor()
|
||||||
|
results = predictor.predict_all(data)
|
||||||
|
|
||||||
|
return jsonify(results)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in prediction: {str(e)}")
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
28
src/run.py
28
src/run.py
@ -1,28 +0,0 @@
|
|||||||
import os
|
|
||||||
import logging
|
|
||||||
from src.app import app
|
|
||||||
|
|
||||||
# 确保必要的目录存在
|
|
||||||
os.makedirs('logs', exist_ok=True)
|
|
||||||
os.makedirs('models', exist_ok=True)
|
|
||||||
os.makedirs('data', exist_ok=True)
|
|
||||||
|
|
||||||
# 配置日志
|
|
||||||
logging.basicConfig(
|
|
||||||
filename='logs/server.log',
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
|
||||||
)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
try:
|
|
||||||
logging.info("Starting server...")
|
|
||||||
app.run(
|
|
||||||
host='localhost',
|
|
||||||
port=5001,
|
|
||||||
debug=True, # 启用调试模式
|
|
||||||
use_reloader=True # 启用自动重载
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Server failed to start: {str(e)}")
|
|
||||||
raise
|
|
||||||
Loading…
Reference in New Issue
Block a user