feat: 添加 JD-R 理论分析模块与 SHAP 可解释性分析功能

- 后端新增 JD-R(工作要求-资源)理论维度数据生成,包含工作要求、工作资源、
    个人资源、中介变量共 16 个新特征列
  - 新增 JD-R 分析服务与 API(维度统计、倦怠投入分析、双路径中介分析、
    分组轮廓、风险分布)
  - 新增 SHAP 可解释性分析模块(全局重要性、局部解释、特征交互、依赖图)
  - 预测服务增加风险分类模型加载与概率预测能力
  - 前端新增 JD-R 分析页面(JDRAnalysis.vue),含雷达图、散点图、路径分析等可视化
  - 预测页面增加风险概率展示与 SHAP 特征解释
  - 路由与导航菜单同步更新
This commit is contained in:
shuo
2026-04-04 07:15:46 +08:00
parent eab1a62ffb
commit e8235bf3ca
30 changed files with 6302 additions and 10 deletions

View File

@@ -32,6 +32,8 @@ MODEL_INFO = {
class PredictService:
def __init__(self):
self.models = {}
self.classifiers = {}
self.classification_metrics = {}
self.scaler = None
self.feature_names = None
self.selected_features = None
@@ -94,6 +96,21 @@ class PredictService:
if valid_metrics:
self.default_model = max(valid_metrics.items(), key=lambda item: item[1]['r2'])[0]
# 加载风险分类模型
for name in ['random_forest', 'gradient_boosting', 'lightgbm', 'xgboost']:
path = os.path.join(config.MODELS_DIR, f'risk_{name}_classifier.pkl')
if os.path.exists(path):
try:
self.classifiers[name] = joblib.load(path)
except Exception:
pass
cls_metrics_path = os.path.join(config.MODELS_DIR, 'classification_metrics.pkl')
if os.path.exists(cls_metrics_path):
try:
self.classification_metrics = joblib.load(cls_metrics_path)
except Exception:
pass
def get_available_models(self):
self._ensure_models_loaded()
models = []
@@ -131,10 +148,15 @@ class PredictService:
risk_level, risk_label = self._get_risk_level(predicted_hours)
confidence = max(0.5, self.model_metrics.get(model_type, {}).get('r2', 0.82))
# 风险分类概率
risk_probability = self._get_risk_probability(features, model_type)
return {
'predicted_hours': round(predicted_hours, 2),
'risk_level': risk_level,
'risk_label': risk_label,
'risk_probability': risk_probability,
'confidence': round(confidence, 2),
'model_used': model_type,
'model_name_cn': MODEL_INFO.get(model_type, {}).get('name_cn', model_type),
@@ -198,11 +220,65 @@ class PredictService:
'predicted_hours': round(max(0.5, base_hours), 2),
'risk_level': risk_level,
'risk_label': risk_label,
'risk_probability': {'low': 0.0, 'medium': 1.0, 'high': 0.0},
'confidence': 0.72,
'model_used': 'default',
'model_name_cn': '默认规则',
}
def _get_risk_probability(self, features, model_type):
"""获取分类器预测的风险概率"""
classifier = self.classifiers.get(model_type)
if classifier is None:
classifier = self.classifiers.get('random_forest')
if classifier is None:
return {'low': 0.0, 'medium': 1.0, 'high': 0.0}
try:
proba = classifier.predict_proba([features])[0]
classes = list(classifier.classes_)
result = {'low': 0.0, 'medium': 0.0, 'high': 0.0}
label_map = {0: 'low', 1: 'medium', 2: 'high'}
for idx, cls in enumerate(classes):
if cls in label_map:
result[label_map[cls]] = round(float(proba[idx]), 4)
return result
except Exception:
return {'low': 0.0, 'medium': 1.0, 'high': 0.0}
def predict_risk_classification(self, data, model_type=None):
"""使用分类模型直接预测风险等级"""
self._ensure_models_loaded()
model_type = model_type or self.default_model
classifier = self.classifiers.get(model_type)
if classifier is None:
classifier = self.classifiers.get('random_forest')
if classifier is None or self.scaler is None:
return None
features = self._prepare_features(data)
try:
pred_class = int(classifier.predict([features])[0])
proba = classifier.predict_proba([features])[0]
label_map = {0: 'low', 1: 'medium', 2: 'high'}
risk_labels_map = {'low': '低风险', 'medium': '中风险', 'high': '高风险'}
risk_level = label_map.get(pred_class, 'medium')
classes = list(classifier.classes_)
probabilities = {'low': 0.0, 'medium': 0.0, 'high': 0.0}
for idx, cls in enumerate(classes):
if cls in label_map:
probabilities[label_map[cls]] = round(float(proba[idx]), 4)
return {
'risk_level': risk_level,
'risk_label': risk_labels_map[risk_level],
'risk_probability': probabilities,
'model_used': model_type,
'classification_metrics': self.classification_metrics.get(model_type, {}),
}
except Exception:
return None
def get_model_info(self):
self._ensure_models_loaded()
return {