commit a39d8b2fd22000c804925ba64265e924147670fb Author: shenjianZ Date: Sun Mar 8 14:48:26 2026 +0800 feat: 初始化员工缺勤分析系统项目 搭建完整的前后端分离架构,实现数据概览、预测分析、聚类分析等核心功能模块 详细版: feat: 初始化员工缺勤分析系统项目 - 后端:基于 Flask 搭建 RESTful API,包含数据概览、特征分析、预测模型、聚类分析四大模块 - 前端:基于 Vue.js 构建单页应用,实现 Dashboard、预测、聚类、因子分析等页面 - 模型:集成随机森林、XGBoost、LightGBM、Stacking 等多种机器学习模型 - 文档:完成需求规格说明、系统架构设计、接口设计、数据设计、UI原型设计等文档 diff --git a/README.md b/README.md new file mode 100644 index 0000000..e3cbb9b --- /dev/null +++ b/README.md @@ -0,0 +1,253 @@ +# 基于多维特征挖掘的员工缺勤分析与预测系统 + +## 项目简介 + +本系统基于 UCI Absenteeism 数据集,利用机器学习算法对员工考勤数据进行深度分析,挖掘影响缺勤的多维度特征,构建缺勤预测模型,为企业人力资源管理提供科学、客观的决策支持。 + +## 功能特性 + +### F01 数据概览与全局统计 +- 基础统计指标展示(样本总数、员工总数、缺勤总时长等) +- 月度缺勤趋势分析 +- 星期分布分析 +- 缺勤原因分布分析 +- 季节分布分析 + +### F02 多维特征挖掘与影响因素分析 +- 特征重要性排序(基于随机森林) +- 相关性热力图分析 +- 群体对比分析(饮酒/吸烟/学历/子女等维度) + +### F03 员工缺勤风险预测 +- 单次缺勤预测 +- 风险等级评估(低/中/高) +- 模型性能展示(R²、MSE、RMSE、MAE) + +### F04 员工画像与群体聚类 +- K-Means 聚类结果展示 +- 员工群体雷达图 +- 聚类散点图可视化 + +## 技术栈 + +### 后端 +- Python 3.11 +- Flask 2.3.3 +- scikit-learn 1.3.0 +- XGBoost 1.7.6 +- LightGBM 4.1.0 +- pandas 2.0.3 +- numpy 1.24.3 + +### 前端 +- Vue 3.4 +- Element Plus 2.4 +- ECharts 5.4 +- Axios 1.6 +- Vue Router 4.2 +- Vite 5.0 + +## 环境要求 + +| 项目 | 要求 | +|------|------| +| 操作系统 | Windows 10/11、Linux、macOS | +| Python | 3.11 | +| Node.js | 16.0+ | +| Conda | Anaconda 或 Miniconda | +| pnpm | 8.0+ | + +## 安装部署 + +### 1. 克隆项目 + +```bash +git clone +cd forsetsystem +``` + +### 2. 后端环境配置 + +#### 创建 Conda 环境 + +```powershell +conda create -n forsetenv python=3.11 -y +conda activate forsetenv +``` + +#### 安装机器学习库(使用 conda-forge) + +```powershell +conda install -c conda-forge pandas=2.0.3 numpy=1.24.3 scikit-learn=1.3.0 xgboost=1.7.6 lightgbm=4.1.0 joblib=1.3.1 -y +``` + +#### 安装 Web 框架 + +```powershell +pip install Flask==2.3.3 Flask-CORS==4.0.0 python-dotenv==1.0.0 +``` + +#### 验证安装 + +```powershell +python -c "import pandas,numpy,sklearn,xgboost,lightgbm,flask;print('All libraries installed successfully')" +``` + +#### 训练模型 + +```powershell +cd backend +python core/train_model.py +``` + +### 3. 前端环境配置 + +```bash +cd frontend +pnpm install +``` + +## 运行说明 + +### 启动后端服务 + +```powershell +conda activate forsetenv +cd backend +python app.py +``` + +后端服务运行在 http://localhost:5000 + +### 启动前端服务 + +```bash +cd frontend +pnpm dev +``` + +前端服务运行在 http://localhost:5173 + +### 访问系统 + +打开浏览器访问 http://localhost:5173 + +## 项目结构 + +``` +forsetsystem/ +├── backend/ # 后端项目 +│ ├── api/ # API 接口层 +│ │ ├── overview_routes.py # 数据概览接口 +│ │ ├── analysis_routes.py # 影响因素分析接口 +│ │ ├── predict_routes.py # 预测接口 +│ │ └── cluster_routes.py # 聚类接口 +│ ├── services/ # 业务逻辑层 +│ ├── core/ # 核心算法层 +│ │ ├── preprocessing.py # 数据预处理 +│ │ ├── feature_mining.py # 特征挖掘 +│ │ ├── train_model.py # 模型训练 +│ │ └── clustering.py # 聚类分析 +│ ├── data/ # 数据存储 +│ ├── models/ # 模型存储 +│ ├── utils/ # 工具函数 +│ ├── app.py # 应用入口 +│ ├── config.py # 配置文件 +│ └── requirements.txt # 依赖清单 +│ +├── frontend/ # 前端项目 +│ ├── src/ +│ │ ├── api/ # API 调用 +│ │ ├── views/ # 页面组件 +│ │ ├── router/ # 路由配置 +│ │ ├── App.vue # 根组件 +│ │ └── main.js # 入口文件 +│ ├── index.html +│ ├── package.json +│ └── vite.config.js +│ +├── data/ # 原始数据 +│ └── Absenteeism_at_work.csv +│ +├── docs/ # 项目文档 +│ ├── 00_需求规格说明书.md +│ ├── 01_系统架构设计.md +│ ├── 02_接口设计文档.md +│ ├── 03_数据设计文档.md +│ └── 04_UI原型设计.md +│ +└── README.md +``` + +## API 接口 + +### 数据概览模块 +| 接口 | 方法 | 说明 | +|------|------|------| +| /api/overview/stats | GET | 基础统计指标 | +| /api/overview/trend | GET | 月度缺勤趋势 | +| /api/overview/weekday | GET | 星期分布 | +| /api/overview/reasons | GET | 缺勤原因分布 | +| /api/overview/seasons | GET | 季节分布 | + +### 影响因素分析模块 +| 接口 | 方法 | 说明 | +|------|------|------| +| /api/analysis/importance | GET | 特征重要性 | +| /api/analysis/correlation | GET | 相关性矩阵 | +| /api/analysis/compare | GET | 群体对比分析 | + +### 预测模块 +| 接口 | 方法 | 说明 | +|------|------|------| +| /api/predict/single | POST | 单次预测 | +| /api/predict/model-info | GET | 模型信息 | + +### 聚类模块 +| 接口 | 方法 | 说明 | +|------|------|------| +| /api/cluster/result | GET | 聚类结果 | +| /api/cluster/profile | GET | 群体画像 | +| /api/cluster/scatter | GET | 散点数据 | + +## 作者信息 + +- **作者**:张硕 +- **学校**:河南农业大学软件学院 +- **项目类型**:本科毕业设计 +- **完成时间**:2026年3月 + +## 后续改进计划 + +### 模型优化 +- [ ] 引入深度学习模型(如 LSTM)处理时序特征 +- [ ] 增加模型解释性分析(SHAP 值可视化) +- [ ] 实现模型自动调参(Optuna/Hyperopt) +- [ ] 支持多模型集成预测 + +### 功能扩展 +- [ ] 增加用户认证与权限管理 +- [ ] 支持自定义数据集上传与分析 +- [ ] 增加数据导出功能(Excel/PDF 报告) +- [ ] 实现预测结果的批量导出 +- [ ] 增加数据可视化大屏展示 + +### 技术改进 +- [ ] 后端迁移至 FastAPI 提升性能 +- [ ] 引入 Redis 缓存常用查询结果 +- [ ] 使用 Docker 容器化部署 +- [ ] 增加 CI/CD 自动化测试与部署 +- [ ] 前端状态管理迁移至 Pinia + +### 数据层面 +- [ ] 支持数据库存储(MySQL/PostgreSQL) +- [ ] 实现数据增量更新机制 +- [ ] 增加数据质量检测与清洗功能 + +## 参考资料 + +- [UCI Machine Learning Repository - Absenteeism at work Data Set](https://archive.ics.uci.edu/ml/datasets/Absenteeism+at+work) +- [Flask 官方文档](https://flask.palletsprojects.com/) +- [Vue 3 官方文档](https://vuejs.org/) +- [Element Plus 组件库](https://element-plus.org/) +- [ECharts 图表库](https://echarts.apache.org/) diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..005f2d3 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,69 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual Environment +venv/ +env/ +ENV/ +.venv + +# Environment variables +.env +.env.local +.env.*.local + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Testing +.pytest_cache/ +.coverage +htmlcov/ +.tox/ +.nox/ + +# Logs +*.log +logs/ + +# OS +.DS_Store +Thumbs.db + +# Model files (optional - uncomment if needed) +# *.pkl +# *.joblib +# *.h5 +# *.model + +# Jupyter Notebook +.ipynb_checkpoints + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json +models \ No newline at end of file diff --git a/backend/api/__init__.py b/backend/api/__init__.py new file mode 100644 index 0000000..4bd9fd3 --- /dev/null +++ b/backend/api/__init__.py @@ -0,0 +1,11 @@ +from .overview_routes import overview_bp +from .analysis_routes import analysis_bp +from .predict_routes import predict_bp +from .cluster_routes import cluster_bp + + +def register_blueprints(app): + app.register_blueprint(overview_bp) + app.register_blueprint(analysis_bp) + app.register_blueprint(predict_bp) + app.register_blueprint(cluster_bp) diff --git a/backend/api/analysis_routes.py b/backend/api/analysis_routes.py new file mode 100644 index 0000000..4946b62 --- /dev/null +++ b/backend/api/analysis_routes.py @@ -0,0 +1,64 @@ +from flask import Blueprint, jsonify, request + +from services.analysis_service import analysis_service + +analysis_bp = Blueprint('analysis', __name__, url_prefix='/api/analysis') + + +@analysis_bp.route('/importance', methods=['GET']) +def get_importance(): + try: + model_type = request.args.get('model', 'rf') + result = analysis_service.get_feature_importance(model_type) + return jsonify({ + 'code': 200, + 'message': 'success', + 'data': result + }) + except Exception as e: + return jsonify({ + 'code': 500, + 'message': str(e), + 'data': None + }), 500 + + +@analysis_bp.route('/correlation', methods=['GET']) +def get_correlation(): + try: + result = analysis_service.get_correlation() + return jsonify({ + 'code': 200, + 'message': 'success', + 'data': result + }) + except Exception as e: + return jsonify({ + 'code': 500, + 'message': str(e), + 'data': None + }), 500 + + +@analysis_bp.route('/compare', methods=['GET']) +def get_compare(): + try: + dimension = request.args.get('dimension', 'drinker') + result = analysis_service.get_group_comparison(dimension) + return jsonify({ + 'code': 200, + 'message': 'success', + 'data': result + }) + except ValueError as e: + return jsonify({ + 'code': 400, + 'message': str(e), + 'data': None + }), 400 + except Exception as e: + return jsonify({ + 'code': 500, + 'message': str(e), + 'data': None + }), 500 diff --git a/backend/api/cluster_routes.py b/backend/api/cluster_routes.py new file mode 100644 index 0000000..df6d24e --- /dev/null +++ b/backend/api/cluster_routes.py @@ -0,0 +1,68 @@ +from flask import Blueprint, jsonify, request + +from services.cluster_service import cluster_service + +cluster_bp = Blueprint('cluster', __name__, url_prefix='/api/cluster') + + +@cluster_bp.route('/result', methods=['GET']) +def get_result(): + try: + n_clusters = request.args.get('n_clusters', 3, type=int) + n_clusters = max(2, min(10, n_clusters)) + + result = cluster_service.get_cluster_result(n_clusters) + return jsonify({ + 'code': 200, + 'message': 'success', + 'data': result + }) + except Exception as e: + return jsonify({ + 'code': 500, + 'message': str(e), + 'data': None + }), 500 + + +@cluster_bp.route('/profile', methods=['GET']) +def get_profile(): + try: + n_clusters = request.args.get('n_clusters', 3, type=int) + n_clusters = max(2, min(10, n_clusters)) + + result = cluster_service.get_cluster_profile(n_clusters) + return jsonify({ + 'code': 200, + 'message': 'success', + 'data': result + }) + except Exception as e: + return jsonify({ + 'code': 500, + 'message': str(e), + 'data': None + }), 500 + + +@cluster_bp.route('/scatter', methods=['GET']) +def get_scatter(): + try: + n_clusters = request.args.get('n_clusters', 3, type=int) + x_axis = request.args.get('x_axis', 'Age') + y_axis = request.args.get('y_axis', 'Absenteeism time in hours') + + n_clusters = max(2, min(10, n_clusters)) + + result = cluster_service.get_scatter_data(n_clusters, x_axis, y_axis) + return jsonify({ + 'code': 200, + 'message': 'success', + 'data': result + }) + except Exception as e: + return jsonify({ + 'code': 500, + 'message': str(e), + 'data': None + }), 500 diff --git a/backend/api/overview_routes.py b/backend/api/overview_routes.py new file mode 100644 index 0000000..3dba7a7 --- /dev/null +++ b/backend/api/overview_routes.py @@ -0,0 +1,90 @@ +from flask import Blueprint, jsonify + +from services.data_service import data_service + +overview_bp = Blueprint('overview', __name__, url_prefix='/api/overview') + + +@overview_bp.route('/stats', methods=['GET']) +def get_stats(): + try: + stats = data_service.get_basic_stats() + return jsonify({ + 'code': 200, + 'message': 'success', + 'data': stats + }) + except Exception as e: + return jsonify({ + 'code': 500, + 'message': str(e), + 'data': None + }), 500 + + +@overview_bp.route('/trend', methods=['GET']) +def get_trend(): + try: + trend = data_service.get_monthly_trend() + return jsonify({ + 'code': 200, + 'message': 'success', + 'data': trend + }) + except Exception as e: + return jsonify({ + 'code': 500, + 'message': str(e), + 'data': None + }), 500 + + +@overview_bp.route('/weekday', methods=['GET']) +def get_weekday(): + try: + weekday = data_service.get_weekday_distribution() + return jsonify({ + 'code': 200, + 'message': 'success', + 'data': weekday + }) + except Exception as e: + return jsonify({ + 'code': 500, + 'message': str(e), + 'data': None + }), 500 + + +@overview_bp.route('/reasons', methods=['GET']) +def get_reasons(): + try: + reasons = data_service.get_reason_distribution() + return jsonify({ + 'code': 200, + 'message': 'success', + 'data': reasons + }) + except Exception as e: + return jsonify({ + 'code': 500, + 'message': str(e), + 'data': None + }), 500 + + +@overview_bp.route('/seasons', methods=['GET']) +def get_seasons(): + try: + seasons = data_service.get_season_distribution() + return jsonify({ + 'code': 200, + 'message': 'success', + 'data': seasons + }) + except Exception as e: + return jsonify({ + 'code': 500, + 'message': str(e), + 'data': None + }), 500 diff --git a/backend/api/predict_routes.py b/backend/api/predict_routes.py new file mode 100644 index 0000000..6d87939 --- /dev/null +++ b/backend/api/predict_routes.py @@ -0,0 +1,102 @@ +from flask import Blueprint, jsonify, request + +from services.predict_service import predict_service + +predict_bp = Blueprint('predict', __name__, url_prefix='/api/predict') + + +@predict_bp.route('/single', methods=['POST']) +def predict_single(): + try: + data = request.get_json() + + if not data: + return jsonify({ + 'code': 400, + 'message': 'Request body is required', + 'data': None + }), 400 + + model_type = data.get('model_type') + + result = predict_service.predict_single(data, model_type) + + return jsonify({ + 'code': 200, + 'message': 'success', + 'data': result + }) + except Exception as e: + return jsonify({ + 'code': 500, + 'message': str(e), + 'data': None + }), 500 + + +@predict_bp.route('/compare', methods=['POST']) +def predict_compare(): + try: + data = request.get_json() + + if not data: + return jsonify({ + 'code': 400, + 'message': 'Request body is required', + 'data': None + }), 400 + + results = predict_service.predict_compare(data) + + return jsonify({ + 'code': 200, + 'message': 'success', + 'data': { + 'results': results, + 'total_models': len(results) + } + }) + except Exception as e: + return jsonify({ + 'code': 500, + 'message': str(e), + 'data': None + }), 500 + + +@predict_bp.route('/models', methods=['GET']) +def get_models(): + try: + models = predict_service.get_available_models() + + return jsonify({ + 'code': 200, + 'message': 'success', + 'data': { + 'models': models, + 'total': len(models) + } + }) + except Exception as e: + return jsonify({ + 'code': 500, + 'message': str(e), + 'data': None + }), 500 + + +@predict_bp.route('/model-info', methods=['GET']) +def get_model_info(): + try: + result = predict_service.get_model_info() + return jsonify({ + 'code': 200, + 'message': 'success', + 'data': result + }) + except Exception as e: + return jsonify({ + 'code': 500, + 'message': str(e), + 'data': None + }), 500 diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 0000000..de58b14 --- /dev/null +++ b/backend/app.py @@ -0,0 +1,68 @@ +from flask import Flask +from flask_cors import CORS + +from api import register_blueprints + + +def create_app(): + app = Flask(__name__) + + CORS(app) + + register_blueprints(app) + + @app.route('/') + def index(): + return { + 'code': 200, + 'message': 'Employee Absenteeism Analysis System API', + 'data': { + 'version': '1.0.0', + 'endpoints': { + 'overview': [ + '/api/overview/stats', + '/api/overview/trend', + '/api/overview/weekday', + '/api/overview/reasons', + '/api/overview/seasons' + ], + 'analysis': [ + '/api/analysis/importance', + '/api/analysis/correlation', + '/api/analysis/compare' + ], + 'predict': [ + '/api/predict/single', + '/api/predict/model-info' + ], + 'cluster': [ + '/api/cluster/result', + '/api/cluster/profile', + '/api/cluster/scatter' + ] + } + } + } + + @app.errorhandler(404) + def not_found(e): + return { + 'code': 404, + 'message': 'Resource not found', + 'data': None + }, 404 + + @app.errorhandler(500) + def server_error(e): + return { + 'code': 500, + 'message': 'Internal server error', + 'data': None + }, 500 + + return app + + +if __name__ == '__main__': + app = create_app() + app.run(host='0.0.0.0', port=5000, debug=True) diff --git a/backend/config.py b/backend/config.py new file mode 100644 index 0000000..af5e877 --- /dev/null +++ b/backend/config.py @@ -0,0 +1,148 @@ +import os + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + +DATA_DIR = os.path.join(BASE_DIR, 'data') +RAW_DATA_DIR = os.path.join(DATA_DIR, 'raw') +PROCESSED_DATA_DIR = os.path.join(DATA_DIR, 'processed') + +MODELS_DIR = os.path.join(BASE_DIR, 'models') + +RAW_DATA_PATH = os.path.join(RAW_DATA_DIR, 'Absenteeism_at_work.csv') +CLEAN_DATA_PATH = os.path.join(PROCESSED_DATA_DIR, 'clean_data.csv') + +RF_MODEL_PATH = os.path.join(MODELS_DIR, 'rf_model.pkl') +XGB_MODEL_PATH = os.path.join(MODELS_DIR, 'xgb_model.pkl') +KMEANS_MODEL_PATH = os.path.join(MODELS_DIR, 'kmeans_model.pkl') +SCALER_PATH = os.path.join(MODELS_DIR, 'scaler.pkl') +ENCODER_PATH = os.path.join(MODELS_DIR, 'encoder.pkl') + +CSV_SEPARATOR = ';' + +RANDOM_STATE = 42 +TEST_SIZE = 0.2 + +FEATURE_NAMES = [ + 'ID', + 'Reason for absence', + 'Month of absence', + 'Day of the week', + 'Seasons', + 'Transportation expense', + 'Distance from Residence to Work', + 'Service time', + 'Age', + 'Work load Average/day ', + 'Hit target', + 'Disciplinary failure', + 'Education', + 'Son', + 'Social drinker', + 'Social smoker', + 'Pet', + 'Weight', + 'Height', + 'Body mass index', + 'Absenteeism time in hours' +] + +CATEGORICAL_FEATURES = [ + 'Reason for absence', + 'Month of absence', + 'Day of the week', + 'Seasons', + 'Disciplinary failure', + 'Education', + 'Social drinker', + 'Social smoker' +] + +NUMERICAL_FEATURES = [ + 'Transportation expense', + 'Distance from Residence to Work', + 'Service time', + 'Age', + 'Work load Average/day ', + 'Hit target', + 'Son', + 'Pet', + 'Body mass index' +] + +REASON_NAMES = { + 0: '未知原因', + 1: '传染病', + 2: '肿瘤', + 3: '血液疾病', + 4: '内分泌疾病', + 5: '精神行为障碍', + 6: '神经系统疾病', + 7: '眼部疾病', + 8: '耳部疾病', + 9: '循环系统疾病', + 10: '呼吸系统疾病', + 11: '消化系统疾病', + 12: '皮肤疾病', + 13: '肌肉骨骼疾病', + 14: '泌尿生殖疾病', + 15: '妊娠相关', + 16: '围产期疾病', + 17: '先天性畸形', + 18: '症状体征', + 19: '损伤中毒', + 20: '外部原因', + 21: '健康因素', + 22: '医疗随访', + 23: '医疗咨询', + 24: '献血', + 25: '实验室检查', + 26: '无故缺勤', + 27: '理疗', + 28: '牙科咨询' +} + +WEEKDAY_NAMES = { + 2: '周一', + 3: '周二', + 4: '周三', + 5: '周四', + 6: '周五' +} + +SEASON_NAMES = { + 1: '夏季', + 2: '秋季', + 3: '冬季', + 4: '春季' +} + +EDUCATION_NAMES = { + 1: '高中', + 2: '本科', + 3: '研究生', + 4: '博士' +} + +FEATURE_NAME_CN = { + 'ID': '员工标识', + 'Reason for absence': '缺勤原因', + 'Month of absence': '缺勤月份', + 'Day of the week': '星期几', + 'Seasons': '季节', + 'Transportation expense': '交通费用', + 'Distance from Residence to Work': '通勤距离', + 'Service time': '工龄', + 'Age': '年龄', + 'Work load Average/day ': '日均工作负荷', + 'Hit target': '达标率', + 'Disciplinary failure': '违纪记录', + 'Education': '学历', + 'Son': '子女数量', + 'Social drinker': '饮酒习惯', + 'Social smoker': '吸烟习惯', + 'Pet': '宠物数量', + 'Weight': '体重', + 'Height': '身高', + 'Body mass index': 'BMI指数', + 'Absenteeism time in hours': '缺勤时长' +} diff --git a/backend/core/__init__.py b/backend/core/__init__.py new file mode 100644 index 0000000..24278e7 --- /dev/null +++ b/backend/core/__init__.py @@ -0,0 +1,4 @@ +from .preprocessing import DataPreprocessor, get_clean_data, save_clean_data +from .feature_mining import calculate_correlation, get_correlation_for_heatmap, group_comparison +from .train_model import OptimizedModelTrainer, train_and_save_models +from .clustering import KMeansAnalyzer, kmeans_analyzer diff --git a/backend/core/clustering.py b/backend/core/clustering.py new file mode 100644 index 0000000..55f4c26 --- /dev/null +++ b/backend/core/clustering.py @@ -0,0 +1,229 @@ +import pandas as pd +import numpy as np +from sklearn.cluster import KMeans +from sklearn.preprocessing import MinMaxScaler +import joblib +import os + +import config +from core.preprocessing import get_clean_data + + +class KMeansAnalyzer: + def __init__(self, n_clusters=3): + self.n_clusters = n_clusters + self.model = None + self.scaler = MinMaxScaler() + self.data = None + self.data_scaled = None + self.labels = None + + def _get_feature_columns(self, df): + df.columns = [col.strip() for col in df.columns] + + feature_map = { + 'Age': None, + 'Service time': None, + 'Work load Average/day': None, + 'Body mass index': None, + 'Absenteeism time in hours': None + } + + for key in feature_map: + if key in df.columns: + feature_map[key] = key + else: + for col in df.columns: + if key.replace(' ', '').lower() == col.replace(' ', '').lower(): + feature_map[key] = col + break + + actual_features = [v for v in feature_map.values() if v is not None] + return actual_features + + def fit(self, n_clusters=None): + if n_clusters: + self.n_clusters = n_clusters + + df = get_clean_data() + df = df.reset_index(drop=True) + + feature_cols = self._get_feature_columns(df) + + if not feature_cols: + feature_cols = ['Age', 'Service time', 'Body mass index', 'Absenteeism time in hours'] + feature_cols = [c for c in feature_cols if c in df.columns] + + self.data = df[feature_cols].values + + self.scaler = MinMaxScaler() + self.data_scaled = self.scaler.fit_transform(self.data) + + self.model = KMeans( + n_clusters=self.n_clusters, + random_state=config.RANDOM_STATE, + n_init=10 + ) + + self.labels = self.model.fit_predict(self.data_scaled) + + return self.model + + def get_cluster_results(self, n_clusters=3): + if self.model is None or self.n_clusters != n_clusters: + self.fit(n_clusters) + + centers = self.scaler.inverse_transform(self.model.cluster_centers_) + + unique, counts = np.unique(self.labels, return_counts=True) + total = len(self.labels) + + cluster_names = self._generate_cluster_names(centers) + + feature_cols = self._get_feature_columns(get_clean_data()) + + clusters = [] + for i, (cluster_id, count) in enumerate(zip(unique, counts)): + center_dict = {} + for j, fname in enumerate(feature_cols): + if j < len(centers[i]): + center_dict[fname] = round(centers[i][j], 2) + + clusters.append({ + 'id': int(cluster_id), + 'name': cluster_names.get(cluster_id, f'群体{cluster_id+1}'), + 'member_count': int(count), + 'percentage': round(count / total * 100, 1), + 'center': center_dict, + 'description': self._generate_description(cluster_names.get(cluster_id, '')) + }) + + return { + 'n_clusters': self.n_clusters, + 'clusters': clusters + } + + def get_cluster_profile(self, n_clusters=3): + if self.model is None or self.n_clusters != n_clusters: + self.fit(n_clusters) + + centers_scaled = self.model.cluster_centers_ + + df = get_clean_data() + df.columns = [col.strip() for col in df.columns] + feature_cols = self._get_feature_columns(df) + + dimensions = ['年龄', '工龄', '工作负荷', 'BMI', '缺勤倾向'][:len(feature_cols)] + + cluster_names = self._generate_cluster_names( + self.scaler.inverse_transform(centers_scaled) + ) + + clusters = [] + for i in range(self.n_clusters): + clusters.append({ + 'id': i, + 'name': cluster_names.get(i, f'群体{i+1}'), + 'values': [round(v, 2) for v in centers_scaled[i]] + }) + + return { + 'dimensions': dimensions, + 'dimension_keys': feature_cols, + 'clusters': clusters + } + + def get_scatter_data(self, n_clusters=3, x_axis='Age', y_axis='Absenteeism time in hours'): + if self.model is None or self.n_clusters != n_clusters: + self.fit(n_clusters) + + df = get_clean_data() + df = df.reset_index(drop=True) + df.columns = [col.strip() for col in df.columns] + + x_col = None + y_col = None + + for col in df.columns: + if x_axis.replace(' ', '').lower() in col.replace(' ', '').lower(): + x_col = col + if y_axis.replace(' ', '').lower() in col.replace(' ', '').lower(): + y_col = col + + if x_col is None: + x_col = df.columns[0] + if y_col is None: + y_col = df.columns[-1] + + points = [] + for idx in range(min(len(df), len(self.labels))): + row = df.iloc[idx] + points.append({ + 'employee_id': int(row['ID']), + 'x': float(row[x_col]), + 'y': float(row[y_col]), + 'cluster_id': int(self.labels[idx]) + }) + + cluster_colors = { + '0': '#67C23A', + '1': '#E6A23C', + '2': '#F56C6C', + '3': '#909399', + '4': '#409EFF' + } + + return { + 'x_axis': x_col, + 'x_axis_name': config.FEATURE_NAME_CN.get(x_col, x_col), + 'y_axis': y_col, + 'y_axis_name': config.FEATURE_NAME_CN.get(y_col, y_col), + 'points': points[:500], + 'cluster_colors': cluster_colors + } + + def _generate_cluster_names(self, centers): + names = {} + + for i, center in enumerate(centers): + if len(center) >= 5: + service_time = center[1] + work_load = center[2] + bmi = center[3] + absent = center[4] + else: + service_time = center[1] if len(center) > 1 else 0 + work_load = 0 + bmi = center[2] if len(center) > 2 else 0 + absent = center[3] if len(center) > 3 else 0 + + if service_time > 15 and absent < 3: + names[i] = '模范型员工' + elif work_load > 260 and absent > 5: + names[i] = '压力型员工' + elif bmi > 28: + names[i] = '生活习惯型员工' + else: + names[i] = f'群体{i+1}' + + return names + + def _generate_description(self, name): + descriptions = { + '模范型员工': '工龄长、工作稳定、缺勤率低', + '压力型员工': '工作负荷大、缺勤较多', + '生活习惯型员工': 'BMI偏高、需关注健康' + } + return descriptions.get(name, '常规员工群体') + + def save_model(self): + os.makedirs(config.MODELS_DIR, exist_ok=True) + joblib.dump(self.model, config.KMEANS_MODEL_PATH) + + def load_model(self): + if os.path.exists(config.KMEANS_MODEL_PATH): + self.model = joblib.load(config.KMEANS_MODEL_PATH) + self.n_clusters = self.model.n_clusters + + +kmeans_analyzer = KMeansAnalyzer() diff --git a/backend/core/feature_mining.py b/backend/core/feature_mining.py new file mode 100644 index 0000000..95eb8d3 --- /dev/null +++ b/backend/core/feature_mining.py @@ -0,0 +1,151 @@ +import pandas as pd +import numpy as np + +import config +from core.preprocessing import get_clean_data + + +def calculate_correlation(): + df = get_clean_data() + + numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist() + + if 'ID' in numeric_cols: + numeric_cols.remove('ID') + + corr_matrix = df[numeric_cols].corr() + + return corr_matrix + + +def get_correlation_for_heatmap(): + corr_matrix = calculate_correlation() + + key_features = [ + 'Age', + 'Service time', + 'Distance from Residence to Work', + 'Work load Average/day ', + 'Body mass index', + 'Absenteeism time in hours' + ] + + key_features = [f for f in key_features if f in corr_matrix.columns] + + sub_matrix = corr_matrix.loc[key_features, key_features] + + result = { + 'features': [config.FEATURE_NAME_CN.get(f, f) for f in key_features], + 'matrix': sub_matrix.values.round(2).tolist() + } + + return result + + +def calculate_feature_importance(model, feature_names): + if hasattr(model, 'feature_importances_'): + importance = model.feature_importances_ + else: + raise ValueError("Model does not have feature_importances_ attribute") + + importance_dict = dict(zip(feature_names, importance)) + + sorted_importance = sorted(importance_dict.items(), key=lambda x: x[1], reverse=True) + + return sorted_importance + + +def get_feature_importance_from_model(model_path, feature_names): + import joblib + + model = joblib.load(model_path) + return calculate_feature_importance(model, feature_names) + + +def group_comparison(dimension): + df = get_clean_data() + + dimension_map = { + 'drinker': ('Social drinker', {0: '不饮酒', 1: '饮酒'}), + 'smoker': ('Social smoker', {0: '不吸烟', 1: '吸烟'}), + 'education': ('Education', {1: '高中', 2: '本科', 3: '研究生', 4: '博士'}), + 'children': ('Son', {0: '无子女'}, lambda x: x > 0, '有子女'), + 'pet': ('Pet', {0: '无宠物'}, lambda x: x > 0, '有宠物') + } + + if dimension not in dimension_map: + raise ValueError(f"Invalid dimension: {dimension}") + + col, value_map = dimension_map[dimension][0], dimension_map[dimension][1] + + if dimension in ['children', 'pet']: + threshold_fn = dimension_map[dimension][2] + other_label = dimension_map[dimension][3] + + groups = [] + for val in [0]: + group_df = df[df[col] == val] + if len(group_df) > 0: + groups.append({ + 'name': value_map.get(val, str(val)), + 'value': val, + 'avg_hours': round(group_df['Absenteeism time in hours'].mean(), 2), + 'count': len(group_df), + 'percentage': round(len(group_df) / len(df) * 100, 1) + }) + + group_df = df[df[col].apply(threshold_fn)] + if len(group_df) > 0: + groups.append({ + 'name': other_label, + 'value': 1, + 'avg_hours': round(group_df['Absenteeism time in hours'].mean(), 2), + 'count': len(group_df), + 'percentage': round(len(group_df) / len(df) * 100, 1) + }) + else: + groups = [] + for val in sorted(df[col].unique()): + group_df = df[df[col] == val] + if len(group_df) > 0: + groups.append({ + 'name': value_map.get(val, str(val)), + 'value': int(val), + 'avg_hours': round(group_df['Absenteeism time in hours'].mean(), 2), + 'count': len(group_df), + 'percentage': round(len(group_df) / len(df) * 100, 1) + }) + + if len(groups) >= 2: + diff_value = abs(groups[0]['avg_hours'] - groups[1]['avg_hours']) + base = min(groups[0]['avg_hours'], groups[1]['avg_hours']) + diff_percentage = round(diff_value / base * 100, 1) if base > 0 else 0 + else: + diff_value = 0 + diff_percentage = 0 + + return { + 'dimension': dimension, + 'dimension_name': { + 'drinker': '饮酒习惯', + 'smoker': '吸烟习惯', + 'education': '学历', + 'children': '子女', + 'pet': '宠物' + }.get(dimension, dimension), + 'groups': groups, + 'difference': { + 'value': diff_value, + 'percentage': diff_percentage + } + } + + +if __name__ == '__main__': + print("Correlation matrix:") + corr = get_correlation_for_heatmap() + print(corr) + + print("\nGroup comparison (drinker):") + comp = group_comparison('drinker') + print(comp) diff --git a/backend/core/preprocessing.py b/backend/core/preprocessing.py new file mode 100644 index 0000000..f1eae64 --- /dev/null +++ b/backend/core/preprocessing.py @@ -0,0 +1,105 @@ +import pandas as pd +import numpy as np +from sklearn.preprocessing import StandardScaler +import joblib +import os + +import config + + +class DataPreprocessor: + def __init__(self): + self.scaler = StandardScaler() + self.is_fitted = False + self.feature_names = None + + def load_raw_data(self): + df = pd.read_csv(config.RAW_DATA_PATH, sep=config.CSV_SEPARATOR) + df.columns = df.columns.str.strip() + return df + + def clean_data(self, df): + df = df.copy() + + df = df.drop_duplicates() + + for col in df.columns: + if df[col].isnull().sum() > 0: + if df[col].dtype in ['int64', 'float64']: + df[col].fillna(df[col].median(), inplace=True) + else: + df[col].fillna(df[col].mode()[0], inplace=True) + + return df + + def fit_transform(self, df): + df = self.clean_data(df) + + if 'Absenteeism time in hours' in df.columns: + y = df['Absenteeism time in hours'].values + feature_df = df.drop(columns=['Absenteeism time in hours']) + else: + y = None + feature_df = df + + self.feature_names = list(feature_df.columns) + + X = feature_df.values + + X = self.scaler.fit_transform(X) + + self.is_fitted = True + + return X, y + + def transform(self, df): + if not self.is_fitted: + raise ValueError("Preprocessor has not been fitted yet.") + + df = self.clean_data(df) + + if 'Absenteeism time in hours' in df.columns: + feature_df = df.drop(columns=['Absenteeism time in hours']) + else: + feature_df = df + + X = feature_df.values + X = self.scaler.transform(X) + + return X + + def save_preprocessor(self): + os.makedirs(config.MODELS_DIR, exist_ok=True) + joblib.dump(self.scaler, config.SCALER_PATH) + joblib.dump(self.feature_names, os.path.join(config.MODELS_DIR, 'feature_names.pkl')) + + def load_preprocessor(self): + self.scaler = joblib.load(config.SCALER_PATH) + feature_names_path = os.path.join(config.MODELS_DIR, 'feature_names.pkl') + if os.path.exists(feature_names_path): + self.feature_names = joblib.load(feature_names_path) + self.is_fitted = True + + +def get_clean_data(): + preprocessor = DataPreprocessor() + df = preprocessor.load_raw_data() + df = preprocessor.clean_data(df) + return df + + +def save_clean_data(): + preprocessor = DataPreprocessor() + df = preprocessor.load_raw_data() + df = preprocessor.clean_data(df) + + os.makedirs(config.PROCESSED_DATA_DIR, exist_ok=True) + df.to_csv(config.CLEAN_DATA_PATH, index=False, sep=',') + + return df + + +if __name__ == '__main__': + df = save_clean_data() + print(f"Clean data saved. Shape: {df.shape}") + print(df.head()) diff --git a/backend/core/train_model.py b/backend/core/train_model.py new file mode 100644 index 0000000..bc76d75 --- /dev/null +++ b/backend/core/train_model.py @@ -0,0 +1,590 @@ +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import pandas as pd +import numpy as np +import time +from sklearn.ensemble import ( + RandomForestRegressor, + GradientBoostingRegressor, + ExtraTreesRegressor, + StackingRegressor +) +from sklearn.linear_model import Ridge +from sklearn.model_selection import train_test_split, RandomizedSearchCV +from sklearn.preprocessing import RobustScaler, LabelEncoder +from sklearn.feature_selection import SelectKBest, f_regression +from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error +import xgboost as xgb +import lightgbm as lgb +import joblib +import warnings +warnings.filterwarnings('ignore') + +import config +from core.preprocessing import get_clean_data + + +def print_training_log(model_name, start_time, best_score, best_params, n_iter, cv_folds): + elapsed = time.time() - start_time + print(f" {'─'*50}") + print(f" Model: {model_name}") + print(f" Time: {elapsed:.1f}s") + print(f" Best CV R2: {best_score:.4f}") + print(f" Best params:") + for k, v in best_params.items(): + print(f" - {k}: {v}") + print(f" Iterations: {n_iter}, CV folds: {cv_folds}") + print(f" {'─'*50}") + + +class DataAugmenter: + def __init__(self, noise_level=0.02, n_augment=2): + self.noise_level = noise_level + self.n_augment = n_augment + + def augment(self, df, target_col='Absenteeism time in hours'): + print(f"\nData Augmentation...") + print(f" Original size: {len(df)}") + + augmented_dfs = [df] + + numerical_cols = df.select_dtypes(include=[np.number]).columns.tolist() + if target_col in numerical_cols: + numerical_cols.remove(target_col) + + for i in range(self.n_augment): + df_aug = df.copy() + + for col in numerical_cols: + if col in df_aug.columns: + std_val = df_aug[col].std() + if std_val > 0: + noise = np.random.normal(0, self.noise_level * std_val, len(df_aug)) + df_aug[col] = df_aug[col] + noise + + augmented_dfs.append(df_aug) + + df_result = pd.concat(augmented_dfs, ignore_index=True) + print(f" Augmented size: {len(df_result)}") + + return df_result + + def smote_regression(self, df, target_col='Absenteeism time in hours'): + df = df.copy() + y = df[target_col].values + + bins = [0, 1, 4, 8, 100] + labels = ['zero', 'low', 'medium', 'high'] + df['_target_bin'] = pd.cut(y, bins=bins, labels=labels, include_lowest=True) + + bin_counts = df['_target_bin'].value_counts() + max_count = bin_counts.max() + + numerical_cols = df.select_dtypes(include=[np.number]).columns.tolist() + if target_col in numerical_cols: + numerical_cols.remove(target_col) + if '_target_bin' in numerical_cols: + numerical_cols.remove('_target_bin') + + augmented_rows = [] + for bin_label in labels: + bin_df = df[df['_target_bin'] == bin_label].drop(columns=['_target_bin']) + bin_size = len(bin_df) + + if bin_size < max_count and bin_size > 0: + n_samples_to_add = max_count - bin_size + + for _ in range(n_samples_to_add): + idx = np.random.choice(bin_df.index) + sample = bin_df.loc[idx].copy() + + for col in numerical_cols: + if col in sample.index: + std_val = bin_df[col].std() + if std_val > 0: + noise = np.random.normal(0, 0.02 * std_val) + sample[col] = sample[col] + noise + + augmented_rows.append(sample) + + if augmented_rows: + df_aug = pd.DataFrame(augmented_rows) + df_result = pd.concat([df.drop(columns=['_target_bin']), df_aug], ignore_index=True) + else: + df_result = df.drop(columns=['_target_bin']) + + print(f" After SMOTE-like augmentation: {len(df_result)}") + + return df_result + + +class OptimizedModelTrainer: + def __init__(self): + self.models = {} + self.scaler = RobustScaler() + self.feature_names = None + self.selected_features = None + self.label_encoders = {} + self.model_metrics = {} + self.augmenter = DataAugmenter(noise_level=0.02, n_augment=2) + + def analyze_data(self, df): + print("\n" + "="*60) + print("Data Analysis") + print("="*60) + + y = df['Absenteeism time in hours'] + + print(f"\nTarget variable statistics:") + print(f" Min: {y.min()}") + print(f" Max: {y.max()}") + print(f" Mean: {y.mean():.2f}") + print(f" Median: {y.median():.2f}") + print(f" Std: {y.std():.2f}") + print(f" Skewness: {y.skew():.2f}") + + print(f"\nTarget distribution:") + print(f" Zero values: {(y == 0).sum()} ({(y == 0).sum() / len(y) * 100:.1f}%)") + print(f" 1-8 hours: {((y > 0) & (y <= 8)).sum()} ({((y > 0) & (y <= 8)).sum() / len(y) * 100:.1f}%)") + print(f" >8 hours: {(y > 8).sum()} ({(y > 8).sum() / len(y) * 100:.1f}%)") + + return y + + def clip_outliers(self, df, columns, lower_pct=1, upper_pct=99): + df_clean = df.copy() + + for col in columns: + if col in df_clean.columns and df_clean[col].dtype in ['int64', 'float64']: + if col == 'Absenteeism time in hours': + continue + lower = df_clean[col].quantile(lower_pct / 100) + upper = df_clean[col].quantile(upper_pct / 100) + df_clean[col] = df_clean[col].clip(lower, upper) + + return df_clean + + def feature_engineering(self, df): + df = df.copy() + + df['workload_per_age'] = df['Work load Average/day'] / (df['Age'] + 1) + df['expense_per_distance'] = df['Transportation expense'] / (df['Distance from Residence to Work'] + 1) + df['age_service_ratio'] = df['Age'] / (df['Service time'] + 1) + + df['has_children'] = (df['Son'] > 0).astype(int) + df['has_pet'] = (df['Pet'] > 0).astype(int) + df['family_responsibility'] = df['Son'] + df['Pet'] + + df['health_risk'] = ((df['Social drinker'] == 1) | (df['Social smoker'] == 1) | (df['Body mass index'] > 30)).astype(int) + df['lifestyle_risk'] = df['Social drinker'].astype(int) + df['Social smoker'].astype(int) + + df['age_group'] = pd.cut(df['Age'], bins=[0, 30, 40, 50, 100], labels=[1, 2, 3, 4]) + df['service_group'] = pd.cut(df['Service time'], bins=[0, 5, 10, 20, 100], labels=[1, 2, 3, 4]) + df['bmi_category'] = pd.cut(df['Body mass index'], bins=[0, 18.5, 25, 30, 100], labels=[1, 2, 3, 4]) + + df['workload_category'] = pd.cut(df['Work load Average/day'], bins=[0, 200, 250, 300, 500], labels=[1, 2, 3, 4]) + df['commute_category'] = pd.cut(df['Distance from Residence to Work'], bins=[0, 10, 20, 50, 100], labels=[1, 2, 3, 4]) + + df['seasonal_risk'] = df['Seasons'].apply(lambda x: 1 if x in [1, 3] else 0) + df['weekday_risk'] = df['Day of the week'].apply(lambda x: 1 if x in [2, 6] else 0) + + df['hit_target_ratio'] = df['Hit target'] / 100 + df['experience_level'] = pd.cut(df['Service time'], bins=[0, 5, 10, 15, 100], labels=[1, 2, 3, 4]) + + df['age_workload_interaction'] = df['Age'] * df['Work load Average/day'] / 10000 + df['service_bmi_interaction'] = df['Service time'] * df['Body mass index'] / 100 + + return df + + def select_features(self, X, y, k=20): + print("\nFeature Selection...") + + selector = SelectKBest(score_func=f_regression, k=min(k, X.shape[1])) + selector.fit(X, y) + + scores = selector.scores_ + feature_scores = list(zip(self.feature_names, scores)) + feature_scores.sort(key=lambda x: x[1], reverse=True) + + print(f"\nTop {min(k, len(feature_scores))} features by F-score:") + for i, (name, score) in enumerate(feature_scores[:min(k, len(feature_scores))]): + cn = config.FEATURE_NAME_CN.get(name, name) + print(f" {i+1}. {cn}: {score:.2f}") + + selected_mask = selector.get_support() + self.selected_features = [f for f, s in zip(self.feature_names, selected_mask) if s] + + return selector.transform(X) + + def prepare_data(self): + df = get_clean_data() + df.columns = [col.strip() for col in df.columns] + + df = df.drop(columns=['ID']) + + cols_to_drop = ['Weight', 'Height', 'Reason for absence'] + for col in cols_to_drop: + if col in df.columns: + df = df.drop(columns=[col]) + print(" Removed features: Weight, Height, Reason for absence (data leakage risk)") + + self.analyze_data(df) + + print("\n" + "="*60) + print("Data Preprocessing") + print("="*60) + + numerical_cols = ['Age', 'Service time', 'Work load Average/day', + 'Transportation expense', 'Distance from Residence to Work', + 'Hit target', 'Body mass index'] + df = self.clip_outliers(df, numerical_cols) + print(" Outliers clipped (1st-99th percentile)") + + print("\n" + "="*60) + print("Data Augmentation") + print("="*60) + + df = self.augmenter.smote_regression(df) + df = self.augmenter.augment(df) + + print("\n" + "="*60) + print("Feature Engineering") + print("="*60) + + df = self.feature_engineering(df) + + y = df['Absenteeism time in hours'].values + X_df = df.drop(columns=['Absenteeism time in hours']) + + ordinal_cols = ['Month of absence', 'Day of the week', 'Seasons', + 'Disciplinary failure', 'Education', 'Social drinker', + 'Social smoker', 'age_group', 'service_group', + 'bmi_category', 'workload_category', 'commute_category', + 'experience_level'] + + for col in ordinal_cols: + if col in X_df.columns: + le = LabelEncoder() + X_df[col] = le.fit_transform(X_df[col].astype(str)) + self.label_encoders[col] = le + + self.feature_names = list(X_df.columns) + + X = X_df.values.astype(float) + + X = self.scaler.fit_transform(X) + + X = self.select_features(X, y, k=20) + + print(f"\nFinal feature count: {X.shape[1]}") + + X_train, X_test, y_train, y_test = train_test_split( + X, y, test_size=0.2, random_state=42 + ) + + return X_train, X_test, y_train, y_test + + def train_random_forest(self, X_train, y_train): + print("\n" + "="*60) + print("Training Random Forest") + print("="*60) + + start_time = time.time() + rf = RandomForestRegressor(random_state=42, n_jobs=-1) + + param_distributions = { + 'n_estimators': [200, 300, 400], + 'max_depth': [10, 15, 20, 25], + 'min_samples_split': [2, 5, 10], + 'min_samples_leaf': [1, 2, 4], + 'max_features': ['sqrt', 0.7] + } + + print(f" Searching {20*5} parameter combinations...") + random_search = RandomizedSearchCV( + rf, param_distributions, n_iter=20, cv=5, + scoring='r2', n_jobs=-1, random_state=42 + ) + random_search.fit(X_train, y_train) + + self.models['random_forest'] = random_search.best_estimator_ + print_training_log("Random Forest", start_time, random_search.best_score_, + random_search.best_params_, 20, 5) + + return random_search.best_estimator_ + + def train_xgboost(self, X_train, y_train): + print("\n" + "="*60) + print("Training XGBoost") + print("="*60) + + start_time = time.time() + xgb_model = xgb.XGBRegressor(random_state=42, n_jobs=-1) + + param_distributions = { + 'n_estimators': [200, 300, 400], + 'max_depth': [5, 7, 9], + 'learning_rate': [0.05, 0.1], + 'subsample': [0.7, 0.8], + 'colsample_bytree': [0.7, 0.8], + 'min_child_weight': [1, 3], + 'reg_alpha': [0, 0.1], + 'reg_lambda': [1, 1.5] + } + + print(f" Searching {20*5} parameter combinations...") + random_search = RandomizedSearchCV( + xgb_model, param_distributions, n_iter=20, cv=5, + scoring='r2', n_jobs=-1, random_state=42 + ) + random_search.fit(X_train, y_train) + + self.models['xgboost'] = random_search.best_estimator_ + print_training_log("XGBoost", start_time, random_search.best_score_, + random_search.best_params_, 20, 5) + + return random_search.best_estimator_ + + def train_lightgbm(self, X_train, y_train): + print("\n" + "="*60) + print("Training LightGBM") + print("="*60) + + start_time = time.time() + lgb_model = lgb.LGBMRegressor(random_state=42, n_jobs=-1, verbose=-1) + + param_distributions = { + 'n_estimators': [200, 300, 400], + 'max_depth': [7, 9, 11, -1], + 'learning_rate': [0.05, 0.1], + 'subsample': [0.7, 0.8], + 'colsample_bytree': [0.7, 0.8], + 'min_child_samples': [5, 10, 20], + 'reg_alpha': [0, 0.1], + 'reg_lambda': [1, 1.5], + 'num_leaves': [31, 50, 70] + } + + print(f" Searching {20*5} parameter combinations...") + random_search = RandomizedSearchCV( + lgb_model, param_distributions, n_iter=20, cv=5, + scoring='r2', n_jobs=-1, random_state=42 + ) + random_search.fit(X_train, y_train) + + self.models['lightgbm'] = random_search.best_estimator_ + print_training_log("LightGBM", start_time, random_search.best_score_, + random_search.best_params_, 20, 5) + + return random_search.best_estimator_ + + def train_gradient_boosting(self, X_train, y_train): + print("\n" + "="*60) + print("Training Gradient Boosting") + print("="*60) + + start_time = time.time() + gb = GradientBoostingRegressor(random_state=42) + + param_distributions = { + 'n_estimators': [200, 300], + 'max_depth': [5, 7, 9], + 'learning_rate': [0.05, 0.1], + 'subsample': [0.7, 0.8], + 'min_samples_split': [2, 5], + 'min_samples_leaf': [1, 2] + } + + print(f" Searching {15*5} parameter combinations...") + random_search = RandomizedSearchCV( + gb, param_distributions, n_iter=15, cv=5, + scoring='r2', n_jobs=-1, random_state=42 + ) + random_search.fit(X_train, y_train) + + self.models['gradient_boosting'] = random_search.best_estimator_ + print_training_log("Gradient Boosting", start_time, random_search.best_score_, + random_search.best_params_, 15, 5) + + return random_search.best_estimator_ + + def train_extra_trees(self, X_train, y_train): + print("\n" + "="*60) + print("Training Extra Trees") + print("="*60) + + start_time = time.time() + et = ExtraTreesRegressor(random_state=42, n_jobs=-1) + + param_distributions = { + 'n_estimators': [200, 300, 400], + 'max_depth': [10, 15, 20], + 'min_samples_split': [2, 5, 10], + 'min_samples_leaf': [1, 2, 4], + 'max_features': ['sqrt', 0.7] + } + + print(f" Searching {20*5} parameter combinations...") + random_search = RandomizedSearchCV( + et, param_distributions, n_iter=20, cv=5, + scoring='r2', n_jobs=-1, random_state=42 + ) + random_search.fit(X_train, y_train) + + self.models['extra_trees'] = random_search.best_estimator_ + print_training_log("Extra Trees", start_time, random_search.best_score_, + random_search.best_params_, 20, 5) + + return random_search.best_estimator_ + + def train_stacking(self, X_train, y_train): + print("\n" + "="*60) + print("Training Stacking Ensemble") + print("="*60) + + start_time = time.time() + base_estimators = [] + + if 'random_forest' in self.models: + base_estimators.append(('rf', self.models['random_forest'])) + if 'xgboost' in self.models: + base_estimators.append(('xgb', self.models['xgboost'])) + if 'lightgbm' in self.models: + base_estimators.append(('lgb', self.models['lightgbm'])) + if 'gradient_boosting' in self.models: + base_estimators.append(('gb', self.models['gradient_boosting'])) + + if len(base_estimators) < 2: + print(" Not enough base models for stacking") + return None + + print(f" Base estimators: {[name for name, _ in base_estimators]}") + print(f" Meta learner: Ridge") + print(f" CV folds: 5") + + stacking = StackingRegressor( + estimators=base_estimators, + final_estimator=Ridge(alpha=1.0), + cv=5, + n_jobs=-1 + ) + stacking.fit(X_train, y_train) + + self.models['stacking'] = stacking + elapsed = time.time() - start_time + print(f" {'─'*50}") + print(f" Stacking ensemble created in {elapsed:.1f}s") + print(f" {'─'*50}") + + return stacking + + def evaluate_model(self, model, X_test, y_test): + y_pred = model.predict(X_test) + + r2 = r2_score(y_test, y_pred) + mse = mean_squared_error(y_test, y_pred) + rmse = np.sqrt(mse) + mae = mean_absolute_error(y_test, y_pred) + + return { + 'r2': round(r2, 4), + 'mse': round(mse, 4), + 'rmse': round(rmse, 4), + 'mae': round(mae, 4) + } + + def save_models(self): + os.makedirs(config.MODELS_DIR, exist_ok=True) + + for name, model in self.models.items(): + if model is not None: + model_path = os.path.join(config.MODELS_DIR, f'{name}_model.pkl') + joblib.dump(model, model_path) + print(f" {name} saved") + + joblib.dump(self.scaler, config.SCALER_PATH) + joblib.dump(self.feature_names, os.path.join(config.MODELS_DIR, 'feature_names.pkl')) + joblib.dump(self.selected_features, os.path.join(config.MODELS_DIR, 'selected_features.pkl')) + joblib.dump(self.label_encoders, os.path.join(config.MODELS_DIR, 'label_encoders.pkl')) + joblib.dump(self.model_metrics, os.path.join(config.MODELS_DIR, 'model_metrics.pkl')) + print(" Scaler and feature info saved") + + def train_all(self): + total_start = time.time() + print("\n" + "="*60) + print("Optimized Model Training Started") + print("="*60) + print(f"Start time: {time.strftime('%Y-%m-%d %H:%M:%S')}") + + X_train, X_test, y_train, y_test = self.prepare_data() + + print(f"\nTrain size: {len(X_train)}, Test size: {len(X_test)}") + + print("\n" + "="*60) + print("Training Models with Hyperparameter Optimization") + print("="*60) + + self.train_random_forest(X_train, y_train) + self.train_extra_trees(X_train, y_train) + self.train_xgboost(X_train, y_train) + self.train_lightgbm(X_train, y_train) + self.train_gradient_boosting(X_train, y_train) + self.train_stacking(X_train, y_train) + + print("\n" + "="*60) + print("Evaluating Models on Test Set") + print("="*60) + + best_r2 = -float('inf') + best_model = None + + for name, model in self.models.items(): + if model is not None: + metrics = self.evaluate_model(model, X_test, y_test) + self.model_metrics[name] = metrics + + status = "Good" if metrics['r2'] > 0.5 else ("OK" if metrics['r2'] > 0.3 else "Poor") + status_icon = "✓" if status == "Good" else ("△" if status == "OK" else "✗") + print(f" {status_icon} {name:20s} - R2: {metrics['r2']:.4f}, RMSE: {metrics['rmse']:.4f}, MAE: {metrics['mae']:.4f}") + + if metrics['r2'] > best_r2: + best_r2 = metrics['r2'] + best_model = name + + print(f"\n ★ Best Model: {best_model} (R2 = {best_r2:.4f})") + + print("\n" + "="*60) + print("Saving Models") + print("="*60) + self.save_models() + + return self.model_metrics + + +def train_and_save_models(): + total_start = time.time() + trainer = OptimizedModelTrainer() + metrics = trainer.train_all() + total_elapsed = time.time() - total_start + + print("\n" + "="*60) + print("Training Complete!") + print("="*60) + print(f"Total training time: {total_elapsed:.1f}s ({total_elapsed/60:.1f} min)") + print(f"End time: {time.strftime('%Y-%m-%d %H:%M:%S')}") + + print("\n" + "-"*60) + print("Final Model Ranking (by R2)") + print("-"*60) + + sorted_metrics = sorted(metrics.items(), key=lambda x: x[1]['r2'], reverse=True) + for i, (name, m) in enumerate(sorted_metrics, 1): + medal = "🥇" if i == 1 else ("🥈" if i == 2 else ("🥉" if i == 3 else " ")) + print(f" {medal} {i}. {name:20s} - R2: {m['r2']:.4f}, RMSE: {m['rmse']:.4f}") + + return metrics + + +if __name__ == '__main__': + train_and_save_models() diff --git a/backend/data/raw/Absenteeism_at_work.csv b/backend/data/raw/Absenteeism_at_work.csv new file mode 100644 index 0000000..4760632 --- /dev/null +++ b/backend/data/raw/Absenteeism_at_work.csv @@ -0,0 +1,741 @@ +ID;Reason for absence;Month of absence;Day of the week;Seasons;Transportation expense;Distance from Residence to Work;Service time;Age;Work load Average/day ;Hit target;Disciplinary failure;Education;Son;Social drinker;Social smoker;Pet;Weight;Height;Body mass index;Absenteeism time in hours +11;26;7;3;1;289;36;13;33;239.554;97;0;1;2;1;0;1;90;172;30;4 +36;0;7;3;1;118;13;18;50;239.554;97;1;1;1;1;0;0;98;178;31;0 +3;23;7;4;1;179;51;18;38;239.554;97;0;1;0;1;0;0;89;170;31;2 +7;7;7;5;1;279;5;14;39;239.554;97;0;1;2;1;1;0;68;168;24;4 +11;23;7;5;1;289;36;13;33;239.554;97;0;1;2;1;0;1;90;172;30;2 +3;23;7;6;1;179;51;18;38;239.554;97;0;1;0;1;0;0;89;170;31;2 +10;22;7;6;1;361;52;3;28;239.554;97;0;1;1;1;0;4;80;172;27;8 +20;23;7;6;1;260;50;11;36;239.554;97;0;1;4;1;0;0;65;168;23;4 +14;19;7;2;1;155;12;14;34;239.554;97;0;1;2;1;0;0;95;196;25;40 +1;22;7;2;1;235;11;14;37;239.554;97;0;3;1;0;0;1;88;172;29;8 +20;1;7;2;1;260;50;11;36;239.554;97;0;1;4;1;0;0;65;168;23;8 +20;1;7;3;1;260;50;11;36;239.554;97;0;1;4;1;0;0;65;168;23;8 +20;11;7;4;1;260;50;11;36;239.554;97;0;1;4;1;0;0;65;168;23;8 +3;11;7;4;1;179;51;18;38;239.554;97;0;1;0;1;0;0;89;170;31;1 +3;23;7;4;1;179;51;18;38;239.554;97;0;1;0;1;0;0;89;170;31;4 +24;14;7;6;1;246;25;16;41;239.554;97;0;1;0;1;0;0;67;170;23;8 +3;23;7;6;1;179;51;18;38;239.554;97;0;1;0;1;0;0;89;170;31;2 +3;21;7;2;1;179;51;18;38;239.554;97;0;1;0;1;0;0;89;170;31;8 +6;11;7;5;1;189;29;13;33;239.554;97;0;1;2;0;0;2;69;167;25;8 +33;23;8;4;1;248;25;14;47;205.917;92;0;1;2;0;0;1;86;165;32;2 +18;10;8;4;1;330;16;4;28;205.917;92;0;2;0;0;0;0;84;182;25;8 +3;11;8;2;1;179;51;18;38;205.917;92;0;1;0;1;0;0;89;170;31;1 +10;13;8;2;1;361;52;3;28;205.917;92;0;1;1;1;0;4;80;172;27;40 +20;28;8;6;1;260;50;11;36;205.917;92;0;1;4;1;0;0;65;168;23;4 +11;18;8;2;1;289;36;13;33;205.917;92;0;1;2;1;0;1;90;172;30;8 +10;25;8;2;1;361;52;3;28;205.917;92;0;1;1;1;0;4;80;172;27;7 +11;23;8;3;1;289;36;13;33;205.917;92;0;1;2;1;0;1;90;172;30;1 +30;28;8;4;1;157;27;6;29;205.917;92;0;1;0;1;1;0;75;185;22;4 +11;18;8;4;1;289;36;13;33;205.917;92;0;1;2;1;0;1;90;172;30;8 +3;23;8;6;1;179;51;18;38;205.917;92;0;1;0;1;0;0;89;170;31;2 +3;18;8;2;1;179;51;18;38;205.917;92;0;1;0;1;0;0;89;170;31;8 +2;18;8;5;1;235;29;12;48;205.917;92;0;1;1;0;1;5;88;163;33;8 +1;23;8;5;1;235;11;14;37;205.917;92;0;3;1;0;0;1;88;172;29;4 +2;18;8;2;1;235;29;12;48;205.917;92;0;1;1;0;1;5;88;163;33;8 +3;23;8;2;1;179;51;18;38;205.917;92;0;1;0;1;0;0;89;170;31;2 +10;23;8;2;1;361;52;3;28;205.917;92;0;1;1;1;0;4;80;172;27;1 +11;24;8;3;1;289;36;13;33;205.917;92;0;1;2;1;0;1;90;172;30;8 +19;11;8;5;1;291;50;12;32;205.917;92;0;1;0;1;0;0;65;169;23;4 +2;28;8;6;1;235;29;12;48;205.917;92;0;1;1;0;1;5;88;163;33;8 +20;23;8;6;1;260;50;11;36;205.917;92;0;1;4;1;0;0;65;168;23;4 +27;23;9;3;1;184;42;7;27;241.476;92;0;1;0;0;0;0;58;167;21;2 +34;23;9;2;1;118;10;10;37;241.476;92;0;1;0;0;0;0;83;172;28;4 +3;23;9;3;1;179;51;18;38;241.476;92;0;1;0;1;0;0;89;170;31;4 +5;19;9;3;1;235;20;13;43;241.476;92;0;1;1;1;0;0;106;167;38;8 +14;23;9;4;1;155;12;14;34;241.476;92;0;1;2;1;0;0;95;196;25;2 +34;23;9;2;1;118;10;10;37;241.476;92;0;1;0;0;0;0;83;172;28;3 +3;23;9;3;1;179;51;18;38;241.476;92;0;1;0;1;0;0;89;170;31;3 +15;23;9;5;1;291;31;12;40;241.476;92;0;1;1;1;0;1;73;171;25;4 +20;22;9;6;1;260;50;11;36;241.476;92;0;1;4;1;0;0;65;168;23;8 +15;14;9;2;4;291;31;12;40;241.476;92;0;1;1;1;0;1;73;171;25;32 +20;0;9;2;4;260;50;11;36;241.476;92;1;1;4;1;0;0;65;168;23;0 +29;0;9;2;4;225;26;9;28;241.476;92;1;1;1;0;0;2;69;169;24;0 +28;23;9;3;4;225;26;9;28;241.476;92;0;1;1;0;0;2;69;169;24;2 +34;23;9;3;4;118;10;10;37;241.476;92;0;1;0;0;0;0;83;172;28;2 +11;0;9;3;4;289;36;13;33;241.476;92;1;1;2;1;0;1;90;172;30;0 +36;0;9;3;4;118;13;18;50;241.476;92;1;1;1;1;0;0;98;178;31;0 +28;18;9;4;4;225;26;9;28;241.476;92;0;1;1;0;0;2;69;169;24;3 +3;23;9;4;4;179;51;18;38;241.476;92;0;1;0;1;0;0;89;170;31;3 +13;0;9;4;4;369;17;12;31;241.476;92;1;1;3;1;0;0;70;169;25;0 +33;23;9;6;4;248;25;14;47;241.476;92;0;1;2;0;0;1;86;165;32;1 +3;23;9;6;4;179;51;18;38;241.476;92;0;1;0;1;0;0;89;170;31;3 +20;23;9;6;4;260;50;11;36;241.476;92;0;1;4;1;0;0;65;168;23;4 +3;23;10;3;4;179;51;18;38;253.465;93;0;1;0;1;0;0;89;170;31;3 +34;23;10;3;4;118;10;10;37;253.465;93;0;1;0;0;0;0;83;172;28;3 +36;0;10;4;4;118;13;18;50;253.465;93;1;1;1;1;0;0;98;178;31;0 +22;23;10;5;4;179;26;9;30;253.465;93;0;3;0;0;0;0;56;171;19;1 +3;23;10;6;4;179;51;18;38;253.465;93;0;1;0;1;0;0;89;170;31;3 +28;23;10;6;4;225;26;9;28;253.465;93;0;1;1;0;0;2;69;169;24;3 +34;23;10;3;4;118;10;10;37;253.465;93;0;1;0;0;0;0;83;172;28;3 +28;23;10;4;4;225;26;9;28;253.465;93;0;1;1;0;0;2;69;169;24;2 +33;23;10;4;4;248;25;14;47;253.465;93;0;1;2;0;0;1;86;165;32;2 +15;23;10;5;4;291;31;12;40;253.465;93;0;1;1;1;0;1;73;171;25;5 +3;23;10;4;4;179;51;18;38;253.465;93;0;1;0;1;0;0;89;170;31;8 +28;23;10;4;4;225;26;9;28;253.465;93;0;1;1;0;0;2;69;169;24;3 +20;19;10;5;4;260;50;11;36;253.465;93;0;1;4;1;0;0;65;168;23;16 +15;14;10;3;4;291;31;12;40;253.465;93;0;1;1;1;0;1;73;171;25;8 +28;28;10;3;4;225;26;9;28;253.465;93;0;1;1;0;0;2;69;169;24;2 +11;26;10;4;4;289;36;13;33;253.465;93;0;1;2;1;0;1;90;172;30;8 +10;23;10;6;4;361;52;3;28;253.465;93;0;1;1;1;0;4;80;172;27;1 +20;28;10;6;4;260;50;11;36;253.465;93;0;1;4;1;0;0;65;168;23;3 +3;23;11;5;4;179;51;18;38;306.345;93;0;1;0;1;0;0;89;170;31;1 +28;23;11;4;4;225;26;9;28;306.345;93;0;1;1;0;0;2;69;169;24;1 +3;13;11;5;4;179;51;18;38;306.345;93;0;1;0;1;0;0;89;170;31;8 +17;21;11;5;4;179;22;17;40;306.345;93;0;2;2;0;1;0;63;170;22;8 +15;23;11;5;4;291;31;12;40;306.345;93;0;1;1;1;0;1;73;171;25;5 +14;10;11;2;4;155;12;14;34;306.345;93;0;1;2;1;0;0;95;196;25;32 +6;22;11;2;4;189;29;13;33;306.345;93;0;1;2;0;0;2;69;167;25;8 +15;14;11;2;4;291;31;12;40;306.345;93;0;1;1;1;0;1;73;171;25;40 +28;23;11;4;4;225;26;9;28;306.345;93;0;1;1;0;0;2;69;169;24;1 +14;6;11;6;4;155;12;14;34;306.345;93;0;1;2;1;0;0;95;196;25;8 +28;23;11;4;4;225;26;9;28;306.345;93;0;1;1;0;0;2;69;169;24;3 +17;21;11;4;4;179;22;17;40;306.345;93;0;2;2;0;1;0;63;170;22;8 +28;13;11;6;4;225;26;9;28;306.345;93;0;1;1;0;0;2;69;169;24;3 +20;28;11;6;4;260;50;11;36;306.345;93;0;1;4;1;0;0;65;168;23;4 +33;28;11;2;4;248;25;14;47;306.345;93;0;1;2;0;0;1;86;165;32;1 +28;28;11;3;4;225;26;9;28;306.345;93;0;1;1;0;0;2;69;169;24;3 +11;7;11;4;4;289;36;13;33;306.345;93;0;1;2;1;0;1;90;172;30;24 +15;23;11;5;4;291;31;12;40;306.345;93;0;1;1;1;0;1;73;171;25;3 +33;23;12;3;4;248;25;14;47;261.306;97;0;1;2;0;0;1;86;165;32;1 +34;19;12;3;4;118;10;10;37;261.306;97;0;1;0;0;0;0;83;172;28;64 +36;23;12;4;4;118;13;18;50;261.306;97;0;1;1;1;0;0;98;178;31;2 +1;26;12;4;4;235;11;14;37;261.306;97;0;3;1;0;0;1;88;172;29;8 +28;23;12;5;4;225;26;9;28;261.306;97;0;1;1;0;0;2;69;169;24;2 +20;26;12;6;4;260;50;11;36;261.306;97;0;1;4;1;0;0;65;168;23;8 +34;19;12;3;4;118;10;10;37;261.306;97;0;1;0;0;0;0;83;172;28;56 +10;22;12;4;4;361;52;3;28;261.306;97;0;1;1;1;0;4;80;172;27;8 +28;28;12;5;4;225;26;9;28;261.306;97;0;1;1;0;0;2;69;169;24;3 +20;28;12;6;4;260;50;11;36;261.306;97;0;1;4;1;0;0;65;168;23;3 +28;23;12;3;4;225;26;9;28;261.306;97;0;1;1;0;0;2;69;169;24;2 +10;22;12;4;4;361;52;3;28;261.306;97;0;1;1;1;0;4;80;172;27;8 +34;27;12;6;4;118;10;10;37;261.306;97;0;1;0;0;0;0;83;172;28;2 +24;19;12;6;2;246;25;16;41;261.306;97;0;1;0;1;0;0;67;170;23;8 +28;23;12;6;2;225;26;9;28;261.306;97;0;1;1;0;0;2;69;169;24;2 +28;23;1;4;2;225;26;9;28;308.593;95;0;1;1;0;0;2;69;169;24;1 +34;19;1;2;2;118;10;10;37;308.593;95;0;1;0;0;0;0;83;172;28;1 +34;27;1;3;2;118;10;10;37;308.593;95;0;1;0;0;0;0;83;172;28;1 +14;18;1;3;2;155;12;14;34;308.593;95;0;1;2;1;0;0;95;196;25;8 +28;27;1;4;2;225;26;9;28;308.593;95;0;1;1;0;0;2;69;169;24;2 +27;23;1;5;2;184;42;7;27;308.593;95;0;1;0;0;0;0;58;167;21;2 +28;28;1;5;2;225;26;9;28;308.593;95;0;1;1;0;0;2;69;169;24;2 +28;27;1;6;2;225;26;9;28;308.593;95;0;1;1;0;0;2;69;169;24;1 +34;27;1;2;2;118;10;10;37;308.593;95;0;1;0;0;0;0;83;172;28;2 +28;27;1;3;2;225;26;9;28;308.593;95;0;1;1;0;0;2;69;169;24;2 +34;27;1;3;2;118;10;10;37;308.593;95;0;1;0;0;0;0;83;172;28;2 +34;27;1;4;2;118;10;10;37;308.593;95;0;1;0;0;0;0;83;172;28;2 +34;27;1;5;2;118;10;10;37;308.593;95;0;1;0;0;0;0;83;172;28;2 +34;27;1;6;2;118;10;10;37;308.593;95;0;1;0;0;0;0;83;172;28;2 +34;27;1;2;2;118;10;10;37;308.593;95;0;1;0;0;0;0;83;172;28;2 +34;27;1;3;2;118;10;10;37;308.593;95;0;1;0;0;0;0;83;172;28;2 +22;18;1;3;2;179;26;9;30;308.593;95;0;3;0;0;0;0;56;171;19;8 +11;18;1;3;2;289;36;13;33;308.593;95;0;1;2;1;0;1;90;172;30;8 +34;27;1;4;2;118;10;10;37;308.593;95;0;1;0;0;0;0;83;172;28;2 +27;23;1;5;2;184;42;7;27;308.593;95;0;1;0;0;0;0;58;167;21;2 +34;27;1;5;2;118;10;10;37;308.593;95;0;1;0;0;0;0;83;172;28;2 +34;27;1;2;2;118;10;10;37;308.593;95;0;1;0;0;0;0;83;172;28;0 +28;23;1;3;2;225;26;9;28;308.593;95;0;1;1;0;0;2;69;169;24;1 +11;22;1;5;2;289;36;13;33;308.593;95;0;1;2;1;0;1;90;172;30;3 +27;23;2;6;2;184;42;7;27;302.585;99;0;1;0;0;0;0;58;167;21;1 +24;1;2;4;2;246;25;16;41;302.585;99;0;1;0;1;0;0;67;170;23;8 +3;11;2;4;2;179;51;18;38;302.585;99;0;1;0;1;0;0;89;170;31;8 +14;28;2;5;2;155;12;14;34;302.585;99;0;1;2;1;0;0;95;196;25;2 +6;23;2;5;2;189;29;13;33;302.585;99;0;1;2;0;0;2;69;167;25;8 +20;28;2;6;2;260;50;11;36;302.585;99;0;1;4;1;0;0;65;168;23;2 +11;22;2;6;2;289;36;13;33;302.585;99;0;1;2;1;0;1;90;172;30;8 +31;11;2;2;2;388;15;9;50;302.585;99;0;1;0;0;0;0;76;178;24;8 +31;1;2;3;2;388;15;9;50;302.585;99;0;1;0;0;0;0;76;178;24;8 +28;28;2;2;2;225;26;9;28;302.585;99;0;1;1;0;0;2;69;169;24;2 +28;23;2;3;2;225;26;9;28;302.585;99;0;1;1;0;0;2;69;169;24;2 +22;23;2;3;2;179;26;9;30;302.585;99;0;3;0;0;0;0;56;171;19;1 +27;23;2;3;2;184;42;7;27;302.585;99;0;1;0;0;0;0;58;167;21;8 +28;25;2;5;2;225;26;9;28;302.585;99;0;1;1;0;0;2;69;169;24;3 +18;18;2;2;2;330;16;4;28;302.585;99;0;2;0;0;0;0;84;182;25;8 +18;23;2;3;2;330;16;4;28;302.585;99;0;2;0;0;0;0;84;182;25;1 +28;23;2;4;2;225;26;9;28;302.585;99;0;1;1;0;0;2;69;169;24;1 +6;19;2;5;2;189;29;13;33;302.585;99;0;1;2;0;0;2;69;167;25;8 +19;28;3;3;2;291;50;12;32;343.253;95;0;1;0;1;0;0;65;169;23;2 +20;19;3;3;2;260;50;11;36;343.253;95;0;1;4;1;0;0;65;168;23;8 +30;19;3;3;2;157;27;6;29;343.253;95;0;1;0;1;1;0;75;185;22;3 +17;17;3;3;2;179;22;17;40;343.253;95;0;2;2;0;1;0;63;170;22;8 +15;22;3;4;2;291;31;12;40;343.253;95;0;1;1;1;0;1;73;171;25;8 +20;13;3;4;2;260;50;11;36;343.253;95;0;1;4;1;0;0;65;168;23;8 +22;13;3;5;2;179;26;9;30;343.253;95;0;3;0;0;0;0;56;171;19;8 +33;14;3;6;2;248;25;14;47;343.253;95;0;1;2;0;0;1;86;165;32;3 +20;13;3;6;2;260;50;11;36;343.253;95;0;1;4;1;0;0;65;168;23;40 +17;11;3;2;2;179;22;17;40;343.253;95;0;2;2;0;1;0;63;170;22;40 +14;1;3;2;2;155;12;14;34;343.253;95;0;1;2;1;0;0;95;196;25;16 +20;26;3;2;2;260;50;11;36;343.253;95;0;1;4;1;0;0;65;168;23;16 +14;13;3;3;2;155;12;14;34;343.253;95;0;1;2;1;0;0;95;196;25;8 +11;6;3;5;2;289;36;13;33;343.253;95;0;1;2;1;0;1;90;172;30;8 +17;8;3;5;2;179;22;17;40;343.253;95;0;2;2;0;1;0;63;170;22;8 +20;28;3;6;2;260;50;11;36;343.253;95;0;1;4;1;0;0;65;168;23;4 +28;23;3;6;2;225;26;9;28;343.253;95;0;1;1;0;0;2;69;169;24;1 +7;14;3;2;2;279;5;14;39;343.253;95;0;1;2;1;1;0;68;168;24;8 +3;13;3;3;2;179;51;18;38;343.253;95;0;1;0;1;0;0;89;170;31;24 +28;23;3;4;2;225;26;9;28;343.253;95;0;1;1;0;0;2;69;169;24;2 +28;11;3;2;3;225;26;9;28;343.253;95;0;1;1;0;0;2;69;169;24;8 +22;13;3;2;3;179;26;9;30;343.253;95;0;3;0;0;0;0;56;171;19;1 +28;11;3;3;3;225;26;9;28;343.253;95;0;1;1;0;0;2;69;169;24;8 +28;11;3;4;3;225;26;9;28;343.253;95;0;1;1;0;0;2;69;169;24;16 +3;13;3;4;3;179;51;18;38;343.253;95;0;1;0;1;0;0;89;170;31;3 +7;14;3;5;3;279;5;14;39;343.253;95;0;1;2;1;1;0;68;168;24;16 +28;28;3;6;3;225;26;9;28;343.253;95;0;1;1;0;0;2;69;169;24;2 +33;14;3;6;3;248;25;14;47;343.253;95;0;1;2;0;0;1;86;165;32;3 +28;28;3;2;3;225;26;9;28;343.253;95;0;1;1;0;0;2;69;169;24;1 +15;28;4;4;3;291;31;12;40;326.452;96;0;1;1;1;0;1;73;171;25;1 +28;23;4;4;3;225;26;9;28;326.452;96;0;1;1;0;0;2;69;169;24;1 +14;28;4;3;3;155;12;14;34;326.452;96;0;1;2;1;0;0;95;196;25;1 +24;13;4;4;3;246;25;16;41;326.452;96;0;1;0;1;0;0;67;170;23;24 +14;23;4;5;3;155;12;14;34;326.452;96;0;1;2;1;0;0;95;196;25;1 +28;28;4;6;3;225;26;9;28;326.452;96;0;1;1;0;0;2;69;169;24;2 +20;28;4;6;3;260;50;11;36;326.452;96;0;1;4;1;0;0;65;168;23;4 +3;13;4;4;3;179;51;18;38;326.452;96;0;1;0;1;0;0;89;170;31;24 +36;23;4;5;3;118;13;18;50;326.452;96;0;1;1;1;0;0;98;178;31;1 +15;23;4;6;3;291;31;12;40;326.452;96;0;1;1;1;0;1;73;171;25;3 +24;14;4;6;3;246;25;16;41;326.452;96;0;1;0;1;0;0;67;170;23;8 +15;28;4;6;3;291;31;12;40;326.452;96;0;1;1;1;0;1;73;171;25;1 +33;28;4;6;3;248;25;14;47;326.452;96;0;1;2;0;0;1;86;165;32;8 +20;19;4;6;3;260;50;11;36;326.452;96;0;1;4;1;0;0;65;168;23;56 +11;19;4;3;3;289;36;13;33;326.452;96;0;1;2;1;0;1;90;172;30;8 +14;12;4;4;3;155;12;14;34;326.452;96;0;1;2;1;0;0;95;196;25;24 +23;19;4;4;3;378;49;11;36;326.452;96;0;1;2;0;1;4;65;174;21;8 +11;13;4;5;3;289;36;13;33;326.452;96;0;1;2;1;0;1;90;172;30;16 +1;7;4;6;3;235;11;14;37;326.452;96;0;3;1;0;0;1;88;172;29;3 +2;0;4;2;3;235;29;12;48;326.452;96;1;1;1;0;1;5;88;163;33;0 +11;13;5;4;3;289;36;13;33;378.884;92;0;1;2;1;0;1;90;172;30;8 +14;28;5;5;3;155;12;14;34;378.884;92;0;1;2;1;0;0;95;196;25;2 +14;28;5;2;3;155;12;14;34;378.884;92;0;1;2;1;0;0;95;196;25;1 +3;18;5;3;3;179;51;18;38;378.884;92;0;1;0;1;0;0;89;170;31;8 +28;19;5;3;3;225;26;9;28;378.884;92;0;1;1;0;0;2;69;169;24;8 +27;7;5;4;3;184;42;7;27;378.884;92;0;1;0;0;0;0;58;167;21;4 +14;28;5;2;3;155;12;14;34;378.884;92;0;1;2;1;0;0;95;196;25;2 +3;12;5;3;3;179;51;18;38;378.884;92;0;1;0;1;0;0;89;170;31;1 +11;13;5;4;3;289;36;13;33;378.884;92;0;1;2;1;0;1;90;172;30;24 +7;0;5;4;3;279;5;14;39;378.884;92;1;1;2;1;1;0;68;168;24;0 +18;0;5;4;3;330;16;4;28;378.884;92;1;2;0;0;0;0;84;182;25;0 +23;0;5;4;3;378;49;11;36;378.884;92;1;1;2;0;1;4;65;174;21;0 +31;0;5;4;3;388;15;9;50;378.884;92;1;1;0;0;0;0;76;178;24;0 +3;11;5;3;3;179;51;18;38;378.884;92;0;1;0;1;0;0;89;170;31;1 +36;13;5;4;3;118;13;18;50;378.884;92;0;1;1;1;0;0;98;178;31;24 +10;22;5;6;3;361;52;3;28;378.884;92;0;1;1;1;0;4;80;172;27;8 +24;19;6;2;3;246;25;16;41;377.550;94;0;1;0;1;0;0;67;170;23;8 +10;22;6;2;3;361;52;3;28;377.550;94;0;1;1;1;0;4;80;172;27;8 +24;10;6;3;3;246;25;16;41;377.550;94;0;1;0;1;0;0;67;170;23;24 +15;23;6;5;3;291;31;12;40;377.550;94;0;1;1;1;0;1;73;171;25;4 +24;10;6;6;3;246;25;16;41;377.550;94;0;1;0;1;0;0;67;170;23;8 +3;11;6;2;3;179;51;18;38;377.550;94;0;1;0;1;0;0;89;170;31;8 +14;23;6;2;3;155;12;14;34;377.550;94;0;1;2;1;0;0;95;196;25;4 +24;10;6;2;3;246;25;16;41;377.550;94;0;1;0;1;0;0;67;170;23;8 +36;13;6;4;3;118;13;18;50;377.550;94;0;1;1;1;0;0;98;178;31;8 +1;13;6;6;3;235;11;14;37;377.550;94;0;3;1;0;0;1;88;172;29;16 +36;23;6;3;3;118;13;18;50;377.550;94;0;1;1;1;0;0;98;178;31;1 +36;13;6;4;3;118;13;18;50;377.550;94;0;1;1;1;0;0;98;178;31;80 +23;22;6;5;3;378;49;11;36;377.550;94;0;1;2;0;1;4;65;174;21;8 +3;11;6;6;3;179;51;18;38;377.550;94;0;1;0;1;0;0;89;170;31;2 +32;28;6;2;1;289;48;29;49;377.550;94;0;1;0;0;0;2;108;172;36;2 +28;28;6;5;1;225;26;9;28;377.550;94;0;1;1;0;0;2;69;169;24;2 +14;19;7;3;1;155;12;14;34;275.312;98;0;1;2;1;0;0;95;196;25;16 +36;1;7;4;1;118;13;18;50;275.312;98;0;1;1;1;0;0;98;178;31;8 +34;5;7;6;1;118;10;10;37;275.312;98;0;1;0;0;0;0;83;172;28;8 +34;26;7;6;1;118;10;10;37;275.312;98;0;1;0;0;0;0;83;172;28;4 +18;26;7;3;1;330;16;4;28;275.312;98;0;2;0;0;0;0;84;182;25;8 +22;18;7;5;1;179;26;9;30;275.312;98;0;3;0;0;0;0;56;171;19;8 +14;25;7;6;1;155;12;14;34;275.312;98;0;1;2;1;0;0;95;196;25;2 +18;1;7;2;1;330;16;4;28;275.312;98;0;2;0;0;0;0;84;182;25;8 +18;1;7;3;1;330;16;4;28;275.312;98;0;2;0;0;0;0;84;182;25;8 +30;25;7;2;1;157;27;6;29;275.312;98;0;1;0;1;1;0;75;185;22;3 +10;22;7;3;1;361;52;3;28;275.312;98;0;1;1;1;0;4;80;172;27;8 +11;26;7;4;1;289;36;13;33;275.312;98;0;1;2;1;0;1;90;172;30;8 +3;26;7;5;1;179;51;18;38;275.312;98;0;1;0;1;0;0;89;170;31;8 +11;19;7;2;1;289;36;13;33;275.312;98;0;1;2;1;0;1;90;172;30;32 +11;19;7;5;1;289;36;13;33;275.312;98;0;1;2;1;0;1;90;172;30;8 +20;0;7;5;1;260;50;11;36;275.312;98;1;1;4;1;0;0;65;168;23;0 +11;19;8;6;1;289;36;13;33;265.615;94;0;1;2;1;0;1;90;172;30;8 +30;19;8;6;1;157;27;6;29;265.615;94;0;1;0;1;1;0;75;185;22;3 +11;23;8;2;1;289;36;13;33;265.615;94;0;1;2;1;0;1;90;172;30;1 +9;18;8;3;1;228;14;16;58;265.615;94;0;1;2;0;0;1;65;172;22;8 +26;13;8;5;1;300;26;13;43;265.615;94;0;1;2;1;1;1;77;175;25;1 +26;14;8;5;1;300;26;13;43;265.615;94;0;1;2;1;1;1;77;175;25;2 +20;28;8;6;1;260;50;11;36;265.615;94;0;1;4;1;0;0;65;168;23;4 +11;23;8;3;1;289;36;13;33;265.615;94;0;1;2;1;0;1;90;172;30;4 +33;23;8;4;1;248;25;14;47;265.615;94;0;1;2;0;0;1;86;165;32;1 +21;11;8;5;1;268;11;8;33;265.615;94;0;2;0;0;0;0;79;178;25;8 +22;23;8;5;1;179;26;9;30;265.615;94;0;3;0;0;0;0;56;171;19;1 +36;13;8;5;1;118;13;18;50;265.615;94;0;1;1;1;0;0;98;178;31;3 +33;25;8;2;1;248;25;14;47;265.615;94;0;1;2;0;0;1;86;165;32;2 +1;23;8;3;1;235;11;14;37;265.615;94;0;3;1;0;0;1;88;172;29;1 +36;23;8;5;1;118;13;18;50;265.615;94;0;1;1;1;0;0;98;178;31;1 +1;19;8;5;1;235;11;14;37;265.615;94;0;3;1;0;0;1;88;172;29;8 +10;8;8;3;1;361;52;3;28;265.615;94;0;1;1;1;0;4;80;172;27;8 +27;6;8;4;1;184;42;7;27;265.615;94;0;1;0;0;0;0;58;167;21;8 +3;11;9;2;1;179;51;18;38;294.217;81;0;1;0;1;0;0;89;170;31;8 +3;23;9;6;1;179;51;18;38;294.217;81;0;1;0;1;0;0;89;170;31;3 +11;19;9;4;1;289;36;13;33;294.217;81;0;1;2;1;0;1;90;172;30;24 +5;0;9;5;1;235;20;13;43;294.217;81;1;1;1;1;0;0;106;167;38;0 +24;9;9;2;1;246;25;16;41;294.217;81;0;1;0;1;0;0;67;170;23;16 +15;28;9;3;1;291;31;12;40;294.217;81;0;1;1;1;0;1;73;171;25;3 +8;0;9;3;1;231;35;14;39;294.217;81;1;1;2;1;0;2;100;170;35;0 +19;0;9;3;1;291;50;12;32;294.217;81;1;1;0;1;0;0;65;169;23;0 +3;13;9;4;1;179;51;18;38;294.217;81;0;1;0;1;0;0;89;170;31;8 +24;9;9;4;1;246;25;16;41;294.217;81;0;1;0;1;0;0;67;170;23;32 +3;23;9;5;1;179;51;18;38;294.217;81;0;1;0;1;0;0;89;170;31;1 +15;28;9;6;1;291;31;12;40;294.217;81;0;1;1;1;0;1;73;171;25;4 +20;28;9;6;1;260;50;11;36;294.217;81;0;1;4;1;0;0;65;168;23;4 +5;26;9;4;4;235;20;13;43;294.217;81;0;1;1;1;0;0;106;167;38;8 +36;28;9;5;4;118;13;18;50;294.217;81;0;1;1;1;0;0;98;178;31;1 +5;0;9;5;4;235;20;13;43;294.217;81;1;1;1;1;0;0;106;167;38;0 +15;28;9;6;4;291;31;12;40;294.217;81;0;1;1;1;0;1;73;171;25;3 +15;7;9;2;4;291;31;12;40;294.217;81;0;1;1;1;0;1;73;171;25;40 +3;13;9;2;4;179;51;18;38;294.217;81;0;1;0;1;0;0;89;170;31;8 +11;24;10;2;4;289;36;13;33;265.017;88;0;1;2;1;0;1;90;172;30;8 +1;26;10;2;4;235;11;14;37;265.017;88;0;3;1;0;0;1;88;172;29;4 +11;26;10;2;4;289;36;13;33;265.017;88;0;1;2;1;0;1;90;172;30;8 +11;22;10;6;4;289;36;13;33;265.017;88;0;1;2;1;0;1;90;172;30;8 +36;0;10;6;4;118;13;18;50;265.017;88;1;1;1;1;0;0;98;178;31;0 +33;0;10;6;4;248;25;14;47;265.017;88;1;1;2;0;0;1;86;165;32;0 +22;1;10;2;4;179;26;9;30;265.017;88;0;3;0;0;0;0;56;171;19;8 +34;7;10;2;4;118;10;10;37;265.017;88;0;1;0;0;0;0;83;172;28;3 +13;22;10;2;4;369;17;12;31;265.017;88;0;1;3;1;0;0;70;169;25;8 +3;28;10;4;4;179;51;18;38;265.017;88;0;1;0;1;0;0;89;170;31;1 +22;1;10;4;4;179;26;9;30;265.017;88;0;3;0;0;0;0;56;171;19;64 +5;0;10;4;4;235;20;13;43;265.017;88;1;1;1;1;0;0;106;167;38;0 +11;19;10;5;4;289;36;13;33;265.017;88;0;1;2;1;0;1;90;172;30;16 +20;28;10;6;4;260;50;11;36;265.017;88;0;1;4;1;0;0;65;168;23;3 +5;0;10;6;4;235;20;13;43;265.017;88;1;1;1;1;0;0;106;167;38;0 +5;23;10;2;4;235;20;13;43;265.017;88;0;1;1;1;0;0;106;167;38;2 +5;23;10;2;4;235;20;13;43;265.017;88;0;1;1;1;0;0;106;167;38;2 +36;28;10;3;4;118;13;18;50;265.017;88;0;1;1;1;0;0;98;178;31;1 +15;28;10;3;4;291;31;12;40;265.017;88;0;1;1;1;0;1;73;171;25;4 +22;23;10;5;4;179;26;9;30;265.017;88;0;3;0;0;0;0;56;171;19;16 +36;28;10;5;4;118;13;18;50;265.017;88;0;1;1;1;0;0;98;178;31;1 +10;10;10;2;4;361;52;3;28;265.017;88;0;1;1;1;0;4;80;172;27;8 +20;0;10;3;4;260;50;11;36;265.017;88;1;1;4;1;0;0;65;168;23;0 +15;0;10;3;4;291;31;12;40;265.017;88;1;1;1;1;0;1;73;171;25;0 +30;0;10;3;4;157;27;6;29;265.017;88;1;1;0;1;1;0;75;185;22;0 +22;1;10;4;4;179;26;9;30;265.017;88;0;3;0;0;0;0;56;171;19;5 +22;7;10;4;4;179;26;9;30;265.017;88;0;3;0;0;0;0;56;171;19;5 +36;23;10;5;4;118;13;18;50;265.017;88;0;1;1;1;0;0;98;178;31;1 +34;11;11;2;4;118;10;10;37;284.031;97;0;1;0;0;0;0;83;172;28;8 +33;23;11;2;4;248;25;14;47;284.031;97;0;1;2;0;0;1;86;165;32;2 +3;6;11;3;4;179;51;18;38;284.031;97;0;1;0;1;0;0;89;170;31;8 +20;28;11;6;4;260;50;11;36;284.031;97;0;1;4;1;0;0;65;168;23;3 +15;23;11;2;4;291;31;12;40;284.031;97;0;1;1;1;0;1;73;171;25;1 +23;1;11;2;4;378;49;11;36;284.031;97;0;1;2;0;1;4;65;174;21;8 +14;11;11;2;4;155;12;14;34;284.031;97;0;1;2;1;0;0;95;196;25;120 +5;26;11;2;4;235;20;13;43;284.031;97;0;1;1;1;0;0;106;167;38;8 +18;0;11;3;4;330;16;4;28;284.031;97;1;2;0;0;0;0;84;182;25;0 +1;18;11;4;4;235;11;14;37;284.031;97;0;3;1;0;0;1;88;172;29;1 +34;11;11;4;4;118;10;10;37;284.031;97;0;1;0;0;0;0;83;172;28;3 +1;25;11;5;4;235;11;14;37;284.031;97;0;3;1;0;0;1;88;172;29;2 +3;28;11;5;4;179;51;18;38;284.031;97;0;1;0;1;0;0;89;170;31;3 +24;13;11;6;4;246;25;16;41;284.031;97;0;1;0;1;0;0;67;170;23;8 +15;12;11;6;4;291;31;12;40;284.031;97;0;1;1;1;0;1;73;171;25;4 +24;13;11;2;4;246;25;16;41;284.031;97;0;1;0;1;0;0;67;170;23;8 +3;28;11;3;4;179;51;18;38;284.031;97;0;1;0;1;0;0;89;170;31;1 +20;10;11;4;4;260;50;11;36;284.031;97;0;1;4;1;0;0;65;168;23;8 +20;15;11;6;4;260;50;11;36;284.031;97;0;1;4;1;0;0;65;168;23;8 +23;0;11;6;4;378;49;11;36;284.031;97;1;1;2;0;1;4;65;174;21;0 +7;0;11;3;4;279;5;14;39;284.031;97;1;1;2;1;1;0;68;168;24;0 +3;23;11;5;4;179;51;18;38;284.031;97;0;1;0;1;0;0;89;170;31;1 +28;12;12;2;4;225;26;9;28;236.629;93;0;1;1;0;0;2;69;169;24;3 +3;28;12;2;4;179;51;18;38;236.629;93;0;1;0;1;0;0;89;170;31;2 +3;28;12;2;4;179;51;18;38;236.629;93;0;1;0;1;0;0;89;170;31;1 +1;23;12;2;4;235;11;14;37;236.629;93;0;3;1;0;0;1;88;172;29;3 +36;28;12;3;4;118;13;18;50;236.629;93;0;1;1;1;0;0;98;178;31;1 +20;28;12;6;4;260;50;11;36;236.629;93;0;1;4;1;0;0;65;168;23;4 +24;4;12;5;4;246;25;16;41;236.629;93;0;1;0;1;0;0;67;170;23;8 +3;28;12;5;4;179;51;18;38;236.629;93;0;1;0;1;0;0;89;170;31;1 +3;28;12;6;4;179;51;18;38;236.629;93;0;1;0;1;0;0;89;170;31;1 +22;23;12;3;4;179;26;9;30;236.629;93;0;3;0;0;0;0;56;171;19;1 +34;25;12;3;4;118;10;10;37;236.629;93;0;1;0;0;0;0;83;172;28;8 +1;25;12;5;4;235;11;14;37;236.629;93;0;3;1;0;0;1;88;172;29;2 +3;28;12;6;4;179;51;18;38;236.629;93;0;1;0;1;0;0;89;170;31;1 +5;13;12;3;2;235;20;13;43;236.629;93;0;1;1;1;0;0;106;167;38;8 +1;14;12;3;2;235;11;14;37;236.629;93;0;3;1;0;0;1;88;172;29;4 +20;26;12;4;2;260;50;11;36;236.629;93;0;1;4;1;0;0;65;168;23;8 +30;28;12;2;2;157;27;6;29;236.629;93;0;1;0;1;1;0;75;185;22;2 +3;28;12;2;2;179;51;18;38;236.629;93;0;1;0;1;0;0;89;170;31;3 +11;19;12;2;2;289;36;13;33;236.629;93;0;1;2;1;0;1;90;172;30;8 +28;23;1;4;2;225;26;9;28;330.061;100;0;1;1;0;0;2;69;169;24;5 +34;19;1;2;2;118;10;10;37;330.061;100;0;1;0;0;0;0;83;172;28;32 +14;23;1;2;2;155;12;14;34;330.061;100;0;1;2;1;0;0;95;196;25;2 +1;13;1;3;2;235;11;14;37;330.061;100;0;3;1;0;0;1;88;172;29;1 +14;23;1;3;2;155;12;14;34;330.061;100;0;1;2;1;0;0;95;196;25;4 +11;26;1;2;2;289;36;13;33;330.061;100;0;1;2;1;0;1;90;172;30;8 +15;3;1;4;2;291;31;12;40;330.061;100;0;1;1;1;0;1;73;171;25;8 +5;26;1;2;2;235;20;13;43;330.061;100;0;1;1;1;0;0;106;167;38;8 +36;26;1;2;2;118;13;18;50;330.061;100;0;1;1;1;0;0;98;178;31;4 +3;28;1;4;2;179;51;18;38;330.061;100;0;1;0;1;0;0;89;170;31;1 +3;28;1;6;2;179;51;18;38;330.061;100;0;1;0;1;0;0;89;170;31;1 +34;28;2;3;2;118;10;10;37;251.818;96;0;1;0;0;0;0;83;172;28;2 +3;27;2;4;2;179;51;18;38;251.818;96;0;1;0;1;0;0;89;170;31;3 +28;7;2;4;2;225;26;9;28;251.818;96;0;1;1;0;0;2;69;169;24;1 +11;22;2;6;2;289;36;13;33;251.818;96;0;1;2;1;0;1;90;172;30;3 +20;28;2;6;2;260;50;11;36;251.818;96;0;1;4;1;0;0;65;168;23;3 +3;23;2;6;2;179;51;18;38;251.818;96;0;1;0;1;0;0;89;170;31;3 +3;27;2;2;2;179;51;18;38;251.818;96;0;1;0;1;0;0;89;170;31;2 +3;27;2;4;2;179;51;18;38;251.818;96;0;1;0;1;0;0;89;170;31;3 +3;10;2;5;2;179;51;18;38;251.818;96;0;1;0;1;0;0;89;170;31;8 +24;26;2;5;2;246;25;16;41;251.818;96;0;1;0;1;0;0;67;170;23;8 +3;27;2;6;2;179;51;18;38;251.818;96;0;1;0;1;0;0;89;170;31;3 +6;22;2;2;2;189;29;13;33;251.818;96;0;1;2;0;0;2;69;167;25;8 +3;27;2;2;2;179;51;18;38;251.818;96;0;1;0;1;0;0;89;170;31;3 +24;23;2;3;2;246;25;16;41;251.818;96;0;1;0;1;0;0;67;170;23;2 +15;23;2;3;2;291;31;12;40;251.818;96;0;1;1;1;0;1;73;171;25;2 +30;11;2;4;2;157;27;6;29;251.818;96;0;1;0;1;1;0;75;185;22;16 +3;27;2;4;2;179;51;18;38;251.818;96;0;1;0;1;0;0;89;170;31;3 +3;27;2;6;2;179;51;18;38;251.818;96;0;1;0;1;0;0;89;170;31;3 +24;10;2;6;2;246;25;16;41;251.818;96;0;1;0;1;0;0;67;170;23;24 +3;27;2;4;2;179;51;18;38;251.818;96;0;1;0;1;0;0;89;170;31;3 +3;27;2;6;2;179;51;18;38;251.818;96;0;1;0;1;0;0;89;170;31;3 +34;18;3;3;2;118;10;10;37;244.387;98;0;1;0;0;0;0;83;172;28;8 +24;19;3;4;2;246;25;16;41;244.387;98;0;1;0;1;0;0;67;170;23;16 +24;28;3;6;2;246;25;16;41;244.387;98;0;1;0;1;0;0;67;170;23;2 +20;28;3;6;2;260;50;11;36;244.387;98;0;1;4;1;0;0;65;168;23;4 +3;28;3;2;2;179;51;18;38;244.387;98;0;1;0;1;0;0;89;170;31;2 +1;22;3;2;2;235;11;14;37;244.387;98;0;3;1;0;0;1;88;172;29;8 +17;22;3;3;2;179;22;17;40;244.387;98;0;2;2;0;1;0;63;170;22;8 +23;22;3;3;2;378;49;11;36;244.387;98;0;1;2;0;1;4;65;174;21;8 +3;28;3;2;2;179;51;18;38;244.387;98;0;1;0;1;0;0;89;170;31;16 +10;22;3;4;2;361;52;3;28;244.387;98;0;1;1;1;0;4;80;172;27;8 +13;0;3;4;2;369;17;12;31;244.387;98;1;1;3;1;0;0;70;169;25;0 +1;21;3;5;2;235;11;14;37;244.387;98;0;3;1;0;0;1;88;172;29;8 +36;23;3;6;3;118;13;18;50;244.387;98;0;1;1;1;0;0;98;178;31;2 +36;14;3;3;3;118;13;18;50;244.387;98;0;1;1;1;0;0;98;178;31;3 +36;13;3;4;3;118;13;18;50;244.387;98;0;1;1;1;0;0;98;178;31;8 +1;0;3;5;3;235;11;14;37;244.387;98;1;3;1;0;0;1;88;172;29;0 +24;0;3;5;3;246;25;16;41;244.387;98;1;1;0;1;0;0;67;170;23;0 +36;0;3;5;3;118;13;18;50;244.387;98;1;1;1;1;0;0;98;178;31;0 +3;28;3;6;3;179;51;18;38;244.387;98;0;1;0;1;0;0;89;170;31;8 +11;22;3;6;3;289;36;13;33;244.387;98;0;1;2;1;0;1;90;172;30;8 +20;19;3;2;3;260;50;11;36;244.387;98;0;1;4;1;0;0;65;168;23;8 +24;28;3;3;3;246;25;16;41;244.387;98;0;1;0;1;0;0;67;170;23;2 +3;28;4;4;3;179;51;18;38;239.409;98;0;1;0;1;0;0;89;170;31;4 +20;28;4;6;3;260;50;11;36;239.409;98;0;1;4;1;0;0;65;168;23;3 +18;26;4;6;3;330;16;4;28;239.409;98;0;2;0;0;0;0;84;182;25;4 +13;22;4;2;3;369;17;12;31;239.409;98;0;1;3;1;0;0;70;169;25;4 +33;26;4;2;3;248;25;14;47;239.409;98;0;1;2;0;0;1;86;165;32;4 +18;23;4;4;3;330;16;4;28;239.409;98;0;2;0;0;0;0;84;182;25;8 +3;28;4;4;3;179;51;18;38;239.409;98;0;1;0;1;0;0;89;170;31;8 +36;23;4;2;3;118;13;18;50;239.409;98;0;1;1;1;0;0;98;178;31;1 +36;13;4;4;3;118;13;18;50;239.409;98;0;1;1;1;0;0;98;178;31;120 +26;28;4;6;3;300;26;13;43;239.409;98;0;1;2;1;1;1;77;175;25;8 +20;28;4;6;3;260;50;11;36;239.409;98;0;1;4;1;0;0;65;168;23;4 +3;28;4;2;3;179;51;18;38;239.409;98;0;1;0;1;0;0;89;170;31;4 +34;11;4;4;3;118;10;10;37;239.409;98;0;1;0;0;0;0;83;172;28;2 +5;13;5;2;3;235;20;13;43;246.074;99;0;1;1;1;0;0;106;167;38;16 +33;23;5;4;3;248;25;14;47;246.074;99;0;1;2;0;0;1;86;165;32;2 +13;10;5;2;3;369;17;12;31;246.074;99;0;1;3;1;0;0;70;169;25;8 +22;23;5;4;3;179;26;9;30;246.074;99;0;3;0;0;0;0;56;171;19;3 +3;28;5;4;3;179;51;18;38;246.074;99;0;1;0;1;0;0;89;170;31;4 +10;23;5;5;3;361;52;3;28;246.074;99;0;1;1;1;0;4;80;172;27;1 +20;28;5;6;3;260;50;11;36;246.074;99;0;1;4;1;0;0;65;168;23;3 +17;11;5;2;3;179;22;17;40;246.074;99;0;2;2;0;1;0;63;170;22;2 +17;8;5;2;3;179;22;17;40;246.074;99;0;2;2;0;1;0;63;170;22;3 +9;18;5;4;3;228;14;16;58;246.074;99;0;1;2;0;0;1;65;172;22;8 +28;25;5;4;3;225;26;9;28;246.074;99;0;1;1;0;0;2;69;169;24;3 +18;13;5;6;3;330;16;4;28;246.074;99;0;2;0;0;0;0;84;182;25;8 +22;25;5;2;3;179;26;9;30;246.074;99;0;3;0;0;0;0;56;171;19;2 +34;28;5;2;3;118;10;10;37;246.074;99;0;1;0;0;0;0;83;172;28;1 +1;1;5;2;3;235;11;14;37;246.074;99;0;3;1;0;0;1;88;172;29;8 +22;23;5;4;3;179;26;9;30;246.074;99;0;3;0;0;0;0;56;171;19;3 +34;23;6;2;3;118;10;10;37;253.957;95;0;1;0;0;0;0;83;172;28;3 +3;28;6;2;3;179;51;18;38;253.957;95;0;1;0;1;0;0;89;170;31;3 +34;28;6;3;3;118;10;10;37;253.957;95;0;1;0;0;0;0;83;172;28;2 +28;23;6;5;3;225;26;9;28;253.957;95;0;1;1;0;0;2;69;169;24;4 +20;28;6;6;3;260;50;11;36;253.957;95;0;1;4;1;0;0;65;168;23;4 +3;0;6;6;3;179;51;18;38;253.957;95;1;1;0;1;0;0;89;170;31;0 +15;13;6;2;3;291;31;12;40;253.957;95;0;1;1;1;0;1;73;171;25;40 +3;28;6;2;3;179;51;18;38;253.957;95;0;1;0;1;0;0;89;170;31;24 +24;28;6;3;3;246;25;16;41;253.957;95;0;1;0;1;0;0;67;170;23;3 +3;28;6;2;3;179;51;18;38;253.957;95;0;1;0;1;0;0;89;170;31;4 +5;26;6;3;3;235;20;13;43;253.957;95;0;1;1;1;0;0;106;167;38;8 +3;28;6;2;1;179;51;18;38;253.957;95;0;1;0;1;0;0;89;170;31;2 +28;23;6;4;1;225;26;9;28;253.957;95;0;1;1;0;0;2;69;169;24;2 +36;23;6;4;1;118;13;18;50;253.957;95;0;1;1;1;0;0;98;178;31;2 +3;5;6;4;1;179;51;18;38;253.957;95;0;1;0;1;0;0;89;170;31;8 +22;21;6;4;1;179;26;9;30;253.957;95;0;3;0;0;0;0;56;171;19;2 +24;28;6;6;1;246;25;16;41;253.957;95;0;1;0;1;0;0;67;170;23;2 +18;11;6;3;1;330;16;4;28;253.957;95;0;2;0;0;0;0;84;182;25;1 +1;13;6;3;1;235;11;14;37;253.957;95;0;3;1;0;0;1;88;172;29;8 +22;23;7;5;1;179;26;9;30;230.290;92;0;3;0;0;0;0;56;171;19;2 +28;25;7;5;1;225;26;9;28;230.290;92;0;1;1;0;0;2;69;169;24;4 +20;13;7;6;1;260;50;11;36;230.290;92;0;1;4;1;0;0;65;168;23;8 +21;7;7;2;1;268;11;8;33;230.290;92;0;2;0;0;0;0;79;178;25;8 +18;25;7;6;1;330;16;4;28;230.290;92;0;2;0;0;0;0;84;182;25;8 +34;26;7;6;1;118;10;10;37;230.290;92;0;1;0;0;0;0;83;172;28;8 +20;26;7;2;1;260;50;11;36;230.290;92;0;1;4;1;0;0;65;168;23;4 +34;28;7;3;1;118;10;10;37;230.290;92;0;1;0;0;0;0;83;172;28;8 +26;15;7;2;1;300;26;13;43;230.290;92;0;1;2;1;1;1;77;175;25;8 +2;23;7;2;1;235;29;12;48;230.290;92;0;1;1;0;1;5;88;163;33;1 +24;28;7;3;1;246;25;16;41;230.290;92;0;1;0;1;0;0;67;170;23;2 +28;9;7;3;1;225;26;9;28;230.290;92;0;1;1;0;0;2;69;169;24;112 +3;28;7;3;1;179;51;18;38;230.290;92;0;1;0;1;0;0;89;170;31;1 +36;23;7;6;1;118;13;18;50;230.290;92;0;1;1;1;0;0;98;178;31;1 +10;22;7;6;1;361;52;3;28;230.290;92;0;1;1;1;0;4;80;172;27;8 +11;22;7;2;1;289;36;13;33;230.290;92;0;1;2;1;0;1;90;172;30;8 +5;26;7;2;1;235;20;13;43;230.290;92;0;1;1;1;0;0;106;167;38;8 +24;28;7;3;1;246;25;16;41;230.290;92;0;1;0;1;0;0;67;170;23;2 +15;28;7;5;1;291;31;12;40;230.290;92;0;1;1;1;0;1;73;171;25;1 +7;23;7;5;1;279;5;14;39;230.290;92;0;1;2;1;1;0;68;168;24;2 +3;25;8;5;1;179;51;18;38;249.797;93;0;1;0;1;0;0;89;170;31;4 +17;25;8;2;1;179;22;17;40;249.797;93;0;2;2;0;1;0;63;170;22;1 +24;28;8;3;1;246;25;16;41;249.797;93;0;1;0;1;0;0;67;170;23;4 +34;28;8;3;1;118;10;10;37;249.797;93;0;1;0;0;0;0;83;172;28;4 +11;26;8;3;1;289;36;13;33;249.797;93;0;1;2;1;0;1;90;172;30;8 +5;26;8;3;1;235;20;13;43;249.797;93;0;1;1;1;0;0;106;167;38;8 +15;28;8;5;1;291;31;12;40;249.797;93;0;1;1;1;0;1;73;171;25;4 +3;25;8;2;1;179;51;18;38;249.797;93;0;1;0;1;0;0;89;170;31;4 +17;25;8;3;1;179;22;17;40;249.797;93;0;2;2;0;1;0;63;170;22;8 +18;23;8;5;1;330;16;4;28;249.797;93;0;2;0;0;0;0;84;182;25;16 +1;23;8;3;1;235;11;14;37;249.797;93;0;3;1;0;0;1;88;172;29;4 +24;28;8;3;1;246;25;16;41;249.797;93;0;1;0;1;0;0;67;170;23;1 +34;28;8;3;1;118;10;10;37;249.797;93;0;1;0;0;0;0;83;172;28;5 +15;28;8;5;1;291;31;12;40;249.797;93;0;1;1;1;0;1;73;171;25;2 +20;28;8;2;1;260;50;11;36;249.797;93;0;1;4;1;0;0;65;168;23;3 +24;28;9;3;1;246;25;16;41;261.756;87;0;1;0;1;0;0;67;170;23;1 +24;28;9;3;1;246;25;16;41;261.756;87;0;1;0;1;0;0;67;170;23;1 +34;28;9;3;1;118;10;10;37;261.756;87;0;1;0;0;0;0;83;172;28;3 +14;23;9;3;1;155;12;14;34;261.756;87;0;1;2;1;0;0;95;196;25;2 +15;28;9;5;1;291;31;12;40;261.756;87;0;1;1;1;0;1;73;171;25;2 +22;23;9;6;1;179;26;9;30;261.756;87;0;3;0;0;0;0;56;171;19;8 +33;23;9;6;1;248;25;14;47;261.756;87;0;1;2;0;0;1;86;165;32;1 +3;23;9;2;1;179;51;18;38;261.756;87;0;1;0;1;0;0;89;170;31;4 +28;23;9;4;1;225;26;9;28;261.756;87;0;1;1;0;0;2;69;169;24;1 +22;23;9;2;1;179;26;9;30;261.756;87;0;3;0;0;0;0;56;171;19;2 +13;23;9;3;4;369;17;12;31;261.756;87;0;1;3;1;0;0;70;169;25;8 +10;22;9;3;4;361;52;3;28;261.756;87;0;1;1;1;0;4;80;172;27;8 +32;4;10;5;4;289;48;29;49;284.853;91;0;1;0;0;0;2;108;172;36;1 +25;11;10;5;4;235;16;8;32;284.853;91;0;3;0;0;0;0;75;178;25;3 +24;26;10;6;4;246;25;16;41;284.853;91;0;1;0;1;0;0;67;170;23;8 +32;14;10;4;4;289;48;29;49;284.853;91;0;1;0;0;0;2;108;172;36;3 +15;28;10;4;4;291;31;12;40;284.853;91;0;1;1;1;0;1;73;171;25;2 +34;23;10;3;4;118;10;10;37;284.853;91;0;1;0;0;0;0;83;172;28;2 +32;23;10;5;4;289;48;29;49;284.853;91;0;1;0;0;0;2;108;172;36;2 +15;23;10;6;4;291;31;12;40;284.853;91;0;1;1;1;0;1;73;171;25;1 +28;23;10;3;4;225;26;9;28;284.853;91;0;1;1;0;0;2;69;169;24;2 +13;23;10;3;4;369;17;12;31;284.853;91;0;1;3;1;0;0;70;169;25;8 +13;23;10;3;4;369;17;12;31;284.853;91;0;1;3;1;0;0;70;169;25;3 +28;23;10;3;4;225;26;9;28;284.853;91;0;1;1;0;0;2;69;169;24;4 +13;26;10;3;4;369;17;12;31;284.853;91;0;1;3;1;0;0;70;169;25;8 +3;28;10;4;4;179;51;18;38;284.853;91;0;1;0;1;0;0;89;170;31;3 +9;1;10;4;4;228;14;16;58;284.853;91;0;1;2;0;0;1;65;172;22;1 +15;23;10;4;4;291;31;12;40;284.853;91;0;1;1;1;0;1;73;171;25;1 +13;10;10;5;4;369;17;12;31;284.853;91;0;1;3;1;0;0;70;169;25;8 +28;13;10;5;4;225;26;9;28;284.853;91;0;1;1;0;0;2;69;169;24;1 +13;10;10;6;4;369;17;12;31;284.853;91;0;1;3;1;0;0;70;169;25;8 +28;10;10;6;4;225;26;9;28;284.853;91;0;1;1;0;0;2;69;169;24;3 +6;23;10;2;4;189;29;13;33;284.853;91;0;1;2;0;0;2;69;167;25;8 +25;6;10;2;4;235;16;8;32;284.853;91;0;3;0;0;0;0;75;178;25;8 +33;10;10;2;4;248;25;14;47;284.853;91;0;1;2;0;0;1;86;165;32;8 +28;0;10;2;4;225;26;9;28;284.853;91;1;1;1;0;0;2;69;169;24;0 +28;13;10;3;4;225;26;9;28;284.853;91;0;1;1;0;0;2;69;169;24;3 +3;21;11;3;4;179;51;18;38;268.519;93;0;1;0;1;0;0;89;170;31;1 +34;28;11;4;4;118;10;10;37;268.519;93;0;1;0;0;0;0;83;172;28;3 +18;2;11;4;4;330;16;4;28;268.519;93;0;2;0;0;0;0;84;182;25;24 +3;28;11;6;4;179;51;18;38;268.519;93;0;1;0;1;0;0;89;170;31;1 +34;9;11;3;4;118;10;10;37;268.519;93;0;1;0;0;0;0;83;172;28;8 +11;24;11;4;4;289;36;13;33;268.519;93;0;1;2;1;0;1;90;172;30;8 +25;1;11;6;4;235;16;8;32;268.519;93;0;3;0;0;0;0;75;178;25;8 +28;23;11;6;4;225;26;9;28;268.519;93;0;1;1;0;0;2;69;169;24;4 +10;22;11;3;4;361;52;3;28;268.519;93;0;1;1;1;0;4;80;172;27;8 +15;28;11;4;4;291;31;12;40;268.519;93;0;1;1;1;0;1;73;171;25;2 +34;13;11;5;4;118;10;10;37;268.519;93;0;1;0;0;0;0;83;172;28;2 +28;14;11;5;4;225;26;9;28;268.519;93;0;1;1;0;0;2;69;169;24;3 +3;28;11;2;4;179;51;18;38;268.519;93;0;1;0;1;0;0;89;170;31;1 +34;23;11;2;4;118;10;10;37;268.519;93;0;1;0;0;0;0;83;172;28;8 +34;8;11;3;4;118;10;10;37;268.519;93;0;1;0;0;0;0;83;172;28;8 +28;23;11;3;4;225;26;9;28;268.519;93;0;1;1;0;0;2;69;169;24;2 +15;0;11;3;4;291;31;12;40;268.519;93;1;1;1;1;0;1;73;171;25;0 +11;0;11;4;4;289;36;13;33;268.519;93;1;1;2;1;0;1;90;172;30;0 +33;14;11;5;4;248;25;14;47;268.519;93;0;1;2;0;0;1;86;165;32;4 +5;0;11;5;4;235;20;13;43;268.519;93;1;1;1;1;0;0;106;167;38;0 +28;23;11;6;4;225;26;9;28;268.519;93;0;1;1;0;0;2;69;169;24;2 +13;26;11;6;4;369;17;12;31;268.519;93;0;1;3;1;0;0;70;169;25;8 +10;28;11;2;4;361;52;3;28;268.519;93;0;1;1;1;0;4;80;172;27;2 +3;13;12;3;4;179;51;18;38;280.549;98;0;1;0;1;0;0;89;170;31;32 +15;28;12;4;4;291;31;12;40;280.549;98;0;1;1;1;0;1;73;171;25;1 +28;23;12;4;4;225;26;9;28;280.549;98;0;1;1;0;0;2;69;169;24;3 +22;13;12;6;4;179;26;9;30;280.549;98;0;3;0;0;0;0;56;171;19;1 +28;23;12;6;4;225;26;9;28;280.549;98;0;1;1;0;0;2;69;169;24;3 +28;23;12;4;4;225;26;9;28;280.549;98;0;1;1;0;0;2;69;169;24;3 +10;14;12;5;4;361;52;3;28;280.549;98;0;1;1;1;0;4;80;172;27;4 +17;18;12;6;4;179;22;17;40;280.549;98;0;2;2;0;1;0;63;170;22;2 +5;26;12;6;4;235;20;13;43;280.549;98;0;1;1;1;0;0;106;167;38;8 +12;18;12;2;4;233;51;1;31;280.549;98;0;2;1;1;0;8;68;178;21;8 +22;13;12;3;4;179;26;9;30;280.549;98;0;3;0;0;0;0;56;171;19;16 +28;23;12;3;4;225;26;9;28;280.549;98;0;1;1;0;0;2;69;169;24;2 +28;23;12;5;4;225;26;9;28;280.549;98;0;1;1;0;0;2;69;169;24;3 +28;23;12;2;4;225;26;9;28;280.549;98;0;1;1;0;0;2;69;169;24;2 +14;18;12;3;2;155;12;14;34;280.549;98;0;1;2;1;0;0;95;196;25;80 +22;12;1;2;2;179;26;9;30;313.532;96;0;3;0;0;0;0;56;171;19;24 +22;12;1;5;2;179;26;9;30;313.532;96;0;3;0;0;0;0;56;171;19;16 +17;25;1;5;2;179;22;17;40;313.532;96;0;2;2;0;1;0;63;170;22;2 +17;25;1;6;2;179;22;17;40;313.532;96;0;2;2;0;1;0;63;170;22;2 +22;13;1;2;2;179;26;9;30;313.532;96;0;3;0;0;0;0;56;171;19;3 +17;25;1;4;2;179;22;17;40;313.532;96;0;2;2;0;1;0;63;170;22;2 +32;10;1;5;2;289;48;29;49;313.532;96;0;1;0;0;0;2;108;172;36;8 +17;18;1;6;2;179;22;17;40;313.532;96;0;2;2;0;1;0;63;170;22;3 +22;27;1;2;2;179;26;9;30;313.532;96;0;3;0;0;0;0;56;171;19;2 +14;18;1;3;2;155;12;14;34;313.532;96;0;1;2;1;0;0;95;196;25;8 +22;27;1;4;2;179;26;9;30;313.532;96;0;3;0;0;0;0;56;171;19;2 +3;27;1;4;2;179;51;18;38;313.532;96;0;1;0;1;0;0;89;170;31;3 +11;13;1;4;2;289;36;13;33;313.532;96;0;1;2;1;0;1;90;172;30;8 +3;27;1;5;2;179;51;18;38;313.532;96;0;1;0;1;0;0;89;170;31;3 +3;27;1;6;2;179;51;18;38;313.532;96;0;1;0;1;0;0;89;170;31;2 +3;13;2;3;2;179;51;18;38;264.249;97;0;1;0;1;0;0;89;170;31;8 +28;23;2;3;2;225;26;9;28;264.249;97;0;1;1;0;0;2;69;169;24;3 +33;1;2;4;2;248;25;14;47;264.249;97;0;1;2;0;0;1;86;165;32;8 +3;27;2;4;2;179;51;18;38;264.249;97;0;1;0;1;0;0;89;170;31;2 +28;28;2;5;2;225;26;9;28;264.249;97;0;1;1;0;0;2;69;169;24;3 +3;27;2;5;2;179;51;18;38;264.249;97;0;1;0;1;0;0;89;170;31;2 +22;27;2;5;2;179;26;9;30;264.249;97;0;3;0;0;0;0;56;171;19;2 +29;28;2;6;2;225;15;15;41;264.249;97;0;4;2;1;0;2;94;182;28;2 +3;27;2;6;2;179;51;18;38;264.249;97;0;1;0;1;0;0;89;170;31;2 +12;19;2;2;2;233;51;1;31;264.249;97;0;2;1;1;0;8;68;178;21;2 +3;27;2;2;2;179;51;18;38;264.249;97;0;1;0;1;0;0;89;170;31;2 +28;7;2;3;2;225;26;9;28;264.249;97;0;1;1;0;0;2;69;169;24;8 +3;27;2;4;2;179;51;18;38;264.249;97;0;1;0;1;0;0;89;170;31;3 +3;27;2;5;2;179;51;18;38;264.249;97;0;1;0;1;0;0;89;170;31;3 +28;25;2;5;2;225;26;9;28;264.249;97;0;1;1;0;0;2;69;169;24;3 +22;13;2;5;2;179;26;9;30;264.249;97;0;3;0;0;0;0;56;171;19;2 +17;23;2;6;2;179;22;17;40;264.249;97;0;2;2;0;1;0;63;170;22;2 +3;27;2;6;2;179;51;18;38;264.249;97;0;1;0;1;0;0;89;170;31;3 +12;12;2;4;2;233;51;1;31;264.249;97;0;2;1;1;0;8;68;178;21;3 +22;27;2;4;2;179;26;9;30;264.249;97;0;3;0;0;0;0;56;171;19;2 +3;27;2;4;2;179;51;18;38;264.249;97;0;1;0;1;0;0;89;170;31;2 +3;13;2;5;2;179;51;18;38;264.249;97;0;1;0;1;0;0;89;170;31;8 +3;27;2;6;2;179;51;18;38;264.249;97;0;1;0;1;0;0;89;170;31;2 +14;25;2;2;2;155;12;14;34;264.249;97;0;1;2;1;0;0;95;196;25;5 +25;25;2;2;2;235;16;8;32;264.249;97;0;3;0;0;0;0;75;178;25;3 +3;27;2;2;2;179;51;18;38;264.249;97;0;1;0;1;0;0;89;170;31;2 +28;7;2;2;2;225;26;9;28;264.249;97;0;1;1;0;0;2;69;169;24;2 +3;27;2;3;2;179;51;18;38;264.249;97;0;1;0;1;0;0;89;170;31;2 +33;23;2;3;2;248;25;14;47;264.249;97;0;1;2;0;0;1;86;165;32;2 +28;25;2;3;2;225;26;9;28;264.249;97;0;1;1;0;0;2;69;169;24;2 +3;27;2;4;2;179;51;18;38;264.249;97;0;1;0;1;0;0;89;170;31;2 +3;27;2;5;2;179;51;18;38;264.249;97;0;1;0;1;0;0;89;170;31;2 +25;25;2;6;2;235;16;8;32;264.249;97;0;3;0;0;0;0;75;178;25;2 +3;27;3;2;2;179;51;18;38;222.196;99;0;1;0;1;0;0;89;170;31;2 +33;23;3;2;2;248;25;14;47;222.196;99;0;1;2;0;0;1;86;165;32;2 +9;25;3;3;2;228;14;16;58;222.196;99;0;1;2;0;0;1;65;172;22;3 +33;25;3;3;2;248;25;14;47;222.196;99;0;1;2;0;0;1;86;165;32;3 +9;12;3;3;2;228;14;16;58;222.196;99;0;1;2;0;0;1;65;172;22;112 +3;27;3;4;2;179;51;18;38;222.196;99;0;1;0;1;0;0;89;170;31;2 +28;27;3;5;2;225;26;9;28;222.196;99;0;1;1;0;0;2;69;169;24;2 +3;27;3;5;2;179;51;18;38;222.196;99;0;1;0;1;0;0;89;170;31;3 +28;25;3;5;2;225;26;9;28;222.196;99;0;1;1;0;0;2;69;169;24;2 +22;27;3;6;2;179;26;9;30;222.196;99;0;3;0;0;0;0;56;171;19;3 +25;25;3;2;2;235;16;8;32;222.196;99;0;3;0;0;0;0;75;178;25;3 +10;19;3;2;2;361;52;3;28;222.196;99;0;1;1;1;0;4;80;172;27;8 +3;13;3;3;2;179;51;18;38;222.196;99;0;1;0;1;0;0;89;170;31;8 +3;27;3;4;2;179;51;18;38;222.196;99;0;1;0;1;0;0;89;170;31;2 +3;27;3;5;2;179;51;18;38;222.196;99;0;1;0;1;0;0;89;170;31;3 +22;27;3;6;2;179;26;9;30;222.196;99;0;3;0;0;0;0;56;171;19;2 +3;10;3;2;2;179;51;18;38;222.196;99;0;1;0;1;0;0;89;170;31;4 +33;13;3;2;2;248;25;14;47;222.196;99;0;1;2;0;0;1;86;165;32;2 +3;27;3;2;2;179;51;18;38;222.196;99;0;1;0;1;0;0;89;170;31;3 +28;7;3;2;2;225;26;9;28;222.196;99;0;1;1;0;0;2;69;169;24;8 +3;27;3;3;2;179;51;18;38;222.196;99;0;1;0;1;0;0;89;170;31;2 +11;23;3;4;2;289;36;13;33;222.196;99;0;1;2;1;0;1;90;172;30;8 +9;25;3;4;2;228;14;16;58;222.196;99;0;1;2;0;0;1;65;172;22;2 +3;27;3;4;2;179;51;18;38;222.196;99;0;1;0;1;0;0;89;170;31;2 +33;23;3;5;2;248;25;14;47;222.196;99;0;1;2;0;0;1;86;165;32;3 +3;27;3;5;2;179;51;18;38;222.196;99;0;1;0;1;0;0;89;170;31;3 +22;23;3;6;2;179;26;9;30;222.196;99;0;3;0;0;0;0;56;171;19;2 +3;27;3;6;2;179;51;18;38;222.196;99;0;1;0;1;0;0;89;170;31;3 +3;27;3;3;3;179;51;18;38;222.196;99;0;1;0;1;0;0;89;170;31;3 +16;23;3;4;3;118;15;24;46;222.196;99;0;1;2;1;1;0;75;175;25;8 +14;13;3;4;3;155;12;14;34;222.196;99;0;1;2;1;0;0;95;196;25;24 +3;27;3;4;3;179;51;18;38;222.196;99;0;1;0;1;0;0;89;170;31;3 +3;27;3;5;3;179;51;18;38;222.196;99;0;1;0;1;0;0;89;170;31;3 +22;13;3;2;3;179;26;9;30;222.196;99;0;3;0;0;0;0;56;171;19;2 +11;19;3;2;3;289;36;13;33;222.196;99;0;1;2;1;0;1;90;172;30;104 +13;22;3;4;3;369;17;12;31;222.196;99;0;1;3;1;0;0;70;169;25;8 +28;13;4;2;3;225;26;9;28;246.288;91;0;1;1;0;0;2;69;169;24;8 +34;10;4;2;3;118;10;10;37;246.288;91;0;1;0;0;0;0;83;172;28;8 +10;19;4;3;3;361;52;3;28;246.288;91;0;1;1;1;0;4;80;172;27;8 +33;19;4;4;3;248;25;14;47;246.288;91;0;1;2;0;0;1;86;165;32;8 +6;13;4;5;3;189;29;13;33;246.288;91;0;1;2;0;0;2;69;167;25;8 +22;27;4;6;3;179;26;9;30;246.288;91;0;3;0;0;0;0;56;171;19;2 +13;7;4;2;3;369;17;12;31;246.288;91;0;1;3;1;0;0;70;169;25;24 +17;16;4;3;3;179;22;17;40;246.288;91;0;2;2;0;1;0;63;170;22;2 +36;23;4;3;3;118;13;18;50;246.288;91;0;1;1;1;0;0;98;178;31;3 +10;23;4;3;3;361;52;3;28;246.288;91;0;1;1;1;0;4;80;172;27;2 +34;10;4;4;3;118;10;10;37;246.288;91;0;1;0;0;0;0;83;172;28;2 +1;22;4;6;3;235;11;14;37;246.288;91;0;3;1;0;0;1;88;172;29;8 +22;27;4;6;3;179;26;9;30;246.288;91;0;3;0;0;0;0;56;171;19;2 +28;19;4;2;3;225;26;9;28;246.288;91;0;1;1;0;0;2;69;169;24;8 +25;16;4;3;3;235;16;8;32;246.288;91;0;3;0;0;0;0;75;178;25;3 +22;27;4;6;3;179;26;9;30;246.288;91;0;3;0;0;0;0;56;171;19;2 +14;28;4;3;3;155;12;14;34;246.288;91;0;1;2;1;0;0;95;196;25;4 +28;19;4;5;3;225;26;9;28;246.288;91;0;1;1;0;0;2;69;169;24;8 +36;14;4;5;3;118;13;18;50;246.288;91;0;1;1;1;0;0;98;178;31;2 +22;27;4;6;3;179;26;9;30;246.288;91;0;3;0;0;0;0;56;171;19;2 +1;22;5;2;3;235;11;14;37;237.656;99;0;3;1;0;0;1;88;172;29;8 +29;19;5;4;3;225;15;15;41;237.656;99;0;4;2;1;0;2;94;182;28;3 +25;28;5;4;3;235;16;8;32;237.656;99;0;3;0;0;0;0;75;178;25;2 +34;8;5;4;3;118;10;10;37;237.656;99;0;1;0;0;0;0;83;172;28;3 +5;26;5;4;3;235;20;13;43;237.656;99;0;1;1;1;0;0;106;167;38;8 +22;13;5;5;3;179;26;9;30;237.656;99;0;3;0;0;0;0;56;171;19;1 +15;28;5;5;3;291;31;12;40;237.656;99;0;1;1;1;0;1;73;171;25;2 +29;14;5;5;3;225;15;15;41;237.656;99;0;4;2;1;0;2;94;182;28;8 +26;19;5;6;3;300;26;13;43;237.656;99;0;1;2;1;1;1;77;175;25;64 +29;22;5;6;3;225;15;15;41;237.656;99;0;4;2;1;0;2;94;182;28;8 +22;27;5;6;3;179;26;9;30;237.656;99;0;3;0;0;0;0;56;171;19;2 +36;23;5;2;3;118;13;18;50;237.656;99;0;1;1;1;0;0;98;178;31;2 +36;5;5;3;3;118;13;18;50;237.656;99;0;1;1;1;0;0;98;178;31;3 +34;28;5;3;3;118;10;10;37;237.656;99;0;1;0;0;0;0;83;172;28;1 +36;0;5;3;3;118;13;18;50;237.656;99;1;1;1;1;0;0;98;178;31;0 +22;27;5;4;3;179;26;9;30;237.656;99;0;3;0;0;0;0;56;171;19;2 +23;0;5;4;3;378;49;11;36;237.656;99;1;1;2;0;1;4;65;174;21;0 +17;16;5;6;3;179;22;17;40;237.656;99;0;2;2;0;1;0;63;170;22;1 +14;10;5;2;3;155;12;14;34;237.656;99;0;1;2;1;0;0;95;196;25;48 +25;10;5;2;3;235;16;8;32;237.656;99;0;3;0;0;0;0;75;178;25;8 +15;22;5;4;3;291;31;12;40;237.656;99;0;1;1;1;0;1;73;171;25;8 +17;10;5;4;3;179;22;17;40;237.656;99;0;2;2;0;1;0;63;170;22;8 +28;6;5;4;3;225;26;9;28;237.656;99;0;1;1;0;0;2;69;169;24;3 +18;10;5;5;3;330;16;4;28;237.656;99;0;2;0;0;0;0;84;182;25;8 +25;23;5;5;3;235;16;8;32;237.656;99;0;3;0;0;0;0;75;178;25;2 +15;28;5;5;3;291;31;12;40;237.656;99;0;1;1;1;0;1;73;171;25;2 +22;27;5;6;3;179;26;9;30;237.656;99;0;3;0;0;0;0;56;171;19;2 +10;7;5;2;3;361;52;3;28;237.656;99;0;1;1;1;0;4;80;172;27;8 +14;23;5;4;3;155;12;14;34;237.656;99;0;1;2;1;0;0;95;196;25;2 +17;25;5;6;3;179;22;17;40;237.656;99;0;2;2;0;1;0;63;170;22;8 +14;10;5;6;3;155;12;14;34;237.656;99;0;1;2;1;0;0;95;196;25;8 +28;11;5;2;3;225;26;9;28;237.656;99;0;1;1;0;0;2;69;169;24;1 +16;7;6;4;3;118;15;24;46;275.089;96;0;1;2;1;1;0;75;175;25;8 +22;27;6;4;3;179;26;9;30;275.089;96;0;3;0;0;0;0;56;171;19;3 +34;26;6;6;3;118;10;10;37;275.089;96;0;1;0;0;0;0;83;172;28;8 +34;10;6;4;3;118;10;10;37;275.089;96;0;1;0;0;0;0;83;172;28;8 +23;22;6;5;3;378;49;11;36;275.089;96;0;1;2;0;1;4;65;174;21;8 +36;19;6;5;3;118;13;18;50;275.089;96;0;1;1;1;0;0;98;178;31;24 +12;19;6;6;3;233;51;1;31;275.089;96;0;2;1;1;0;8;68;178;21;8 +22;27;6;6;3;179;26;9;30;275.089;96;0;3;0;0;0;0;56;171;19;2 +2;0;6;2;3;235;29;12;48;275.089;96;1;1;1;0;1;5;88;163;33;0 +21;0;6;2;3;268;11;8;33;275.089;96;1;2;0;0;0;0;79;178;25;0 +36;19;6;5;3;118;13;18;50;275.089;96;0;1;1;1;0;0;98;178;31;3 +22;13;6;5;3;179;26;9;30;275.089;96;0;3;0;0;0;0;56;171;19;2 +15;28;6;5;3;291;31;12;40;275.089;96;0;1;1;1;0;1;73;171;25;2 +22;13;6;2;1;179;26;9;30;275.089;96;0;3;0;0;0;0;56;171;19;3 +34;25;6;2;1;118;10;10;37;275.089;96;0;1;0;0;0;0;83;172;28;3 +12;22;6;5;1;233;51;1;31;275.089;96;0;2;1;1;0;8;68;178;21;8 +34;8;6;6;1;118;10;10;37;275.089;96;0;1;0;0;0;0;83;172;28;2 +34;10;6;4;1;118;10;10;37;275.089;96;0;1;0;0;0;0;83;172;28;3 +12;22;6;4;1;233;51;1;31;275.089;96;0;2;1;1;0;8;68;178;21;3 +5;26;7;4;1;235;20;13;43;264.604;93;0;1;1;1;0;0;106;167;38;4 +12;19;7;6;1;233;51;1;31;264.604;93;0;2;1;1;0;8;68;178;21;2 +9;6;7;2;1;228;14;16;58;264.604;93;0;1;2;0;0;1;65;172;22;8 +34;28;7;2;1;118;10;10;37;264.604;93;0;1;0;0;0;0;83;172;28;4 +9;6;7;3;1;228;14;16;58;264.604;93;0;1;2;0;0;1;65;172;22;120 +6;22;7;3;1;189;29;13;33;264.604;93;0;1;2;0;0;2;69;167;25;16 +34;23;7;4;1;118;10;10;37;264.604;93;0;1;0;0;0;0;83;172;28;2 +10;22;7;4;1;361;52;3;28;264.604;93;0;1;1;1;0;4;80;172;27;8 +28;22;7;4;1;225;26;9;28;264.604;93;0;1;1;0;0;2;69;169;24;8 +13;13;7;2;1;369;17;12;31;264.604;93;0;1;3;1;0;0;70;169;25;80 +11;14;7;3;1;289;36;13;33;264.604;93;0;1;2;1;0;1;90;172;30;8 +1;11;7;3;1;235;11;14;37;264.604;93;0;3;1;0;0;1;88;172;29;4 +4;0;0;3;1;118;14;13;40;271.219;95;0;1;1;1;0;8;98;170;34;0 +8;0;0;4;2;231;35;14;39;271.219;95;0;1;2;1;0;2;100;170;35;0 +35;0;0;6;3;179;45;14;53;271.219;95;0;1;1;0;0;1;77;175;25;0 diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..745e9d1 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,16 @@ +# Web Framework +Flask==2.3.3 +Flask-CORS==4.0.0 + +# Data Processing +pandas==2.0.3 +numpy==1.24.3 + +# Machine Learning +scikit-learn==1.3.0 +xgboost==1.7.6 +lightgbm==4.1.0 +joblib==1.3.1 + +# Utilities +python-dotenv==1.0.0 diff --git a/backend/services/__init__.py b/backend/services/__init__.py new file mode 100644 index 0000000..0c49ee5 --- /dev/null +++ b/backend/services/__init__.py @@ -0,0 +1,4 @@ +from .data_service import DataService, data_service +from .analysis_service import AnalysisService, analysis_service +from .predict_service import PredictService, predict_service +from .cluster_service import ClusterService, cluster_service diff --git a/backend/services/analysis_service.py b/backend/services/analysis_service.py new file mode 100644 index 0000000..bddd35a --- /dev/null +++ b/backend/services/analysis_service.py @@ -0,0 +1,119 @@ +import os +import joblib +import numpy as np + +import config +from core.feature_mining import get_correlation_for_heatmap, group_comparison + + +class AnalysisService: + def __init__(self): + self.models = {} + self.feature_names = None + + def _ensure_models_loaded(self): + if not self.models: + model_files = { + 'random_forest': 'random_forest_model.pkl', + 'xgboost': 'xgboost_model.pkl', + 'lightgbm': 'lightgbm_model.pkl', + } + + for name, filename in model_files.items(): + model_path = os.path.join(config.MODELS_DIR, filename) + if os.path.exists(model_path): + try: + self.models[name] = joblib.load(model_path) + except Exception as e: + print(f"Failed to load {name}: {e}") + + feature_names_path = os.path.join(config.MODELS_DIR, 'feature_names.pkl') + if os.path.exists(feature_names_path): + self.feature_names = joblib.load(feature_names_path) + + def get_feature_importance(self, model_type='random_forest'): + self._ensure_models_loaded() + + if model_type not in self.models: + if self.models: + model_type = list(self.models.keys())[0] + else: + return self._get_default_importance() + + model = self.models[model_type] + + try: + if hasattr(model, 'feature_importances_'): + importances = model.feature_importances_ + else: + return self._get_default_importance() + + feature_names = self.feature_names or [f'feature_{i}' for i in range(len(importances))] + + if len(feature_names) != len(importances): + feature_names = [f'feature_{i}' for i in range(len(importances))] + + feature_importance = list(zip(feature_names, importances)) + feature_importance.sort(key=lambda x: x[1], reverse=True) + + features = [] + for i, (name, imp) in enumerate(feature_importance[:15]): + features.append({ + 'name': name, + 'name_cn': config.FEATURE_NAME_CN.get(name, name), + 'importance': round(float(imp), 4), + 'rank': i + 1 + }) + + return { + 'model_type': model_type, + 'features': features + } + except Exception as e: + print(f"Error getting feature importance: {e}") + return self._get_default_importance() + + def _get_default_importance(self): + default_features = [ + ('Reason for absence', 0.25), + ('Transportation expense', 0.12), + ('Distance from Residence to Work', 0.10), + ('Service time', 0.08), + ('Age', 0.07), + ('Work load Average/day', 0.06), + ('Body mass index', 0.05), + ('Social drinker', 0.04), + ('Hit target', 0.03), + ('Son', 0.03), + ('Pet', 0.02), + ('Education', 0.02), + ('Social smoker', 0.01) + ] + + features = [] + for i, (name, imp) in enumerate(default_features): + features.append({ + 'name': name, + 'name_cn': config.FEATURE_NAME_CN.get(name, name), + 'importance': imp, + 'rank': i + 1 + }) + + return { + 'model_type': 'default', + 'features': features + } + + def get_correlation(self): + return get_correlation_for_heatmap() + + def get_group_comparison(self, dimension): + valid_dimensions = ['drinker', 'smoker', 'education', 'children', 'pet'] + + if dimension not in valid_dimensions: + raise ValueError(f"Invalid dimension: {dimension}. Must be one of {valid_dimensions}") + + return group_comparison(dimension) + + +analysis_service = AnalysisService() diff --git a/backend/services/cluster_service.py b/backend/services/cluster_service.py new file mode 100644 index 0000000..050c988 --- /dev/null +++ b/backend/services/cluster_service.py @@ -0,0 +1,18 @@ +from core.clustering import KMeansAnalyzer + + +class ClusterService: + def __init__(self): + self.analyzer = KMeansAnalyzer() + + def get_cluster_result(self, n_clusters=3): + return self.analyzer.get_cluster_results(n_clusters) + + def get_cluster_profile(self, n_clusters=3): + return self.analyzer.get_cluster_profile(n_clusters) + + def get_scatter_data(self, n_clusters=3, x_axis='Age', y_axis='Absenteeism time in hours'): + return self.analyzer.get_scatter_data(n_clusters, x_axis, y_axis) + + +cluster_service = ClusterService() diff --git a/backend/services/data_service.py b/backend/services/data_service.py new file mode 100644 index 0000000..5743afb --- /dev/null +++ b/backend/services/data_service.py @@ -0,0 +1,162 @@ +import pandas as pd +import numpy as np + +import config +from core.preprocessing import get_clean_data + + +class DataService: + def __init__(self): + self._df = None + + @property + def df(self): + if self._df is None: + self._df = get_clean_data() + return self._df + + def get_basic_stats(self): + df = self.df + + total_records = len(df) + total_employees = df['ID'].nunique() + total_absent_hours = df['Absenteeism time in hours'].sum() + avg_absent_hours = round(df['Absenteeism time in hours'].mean(), 2) + max_absent_hours = int(df['Absenteeism time in hours'].max()) + min_absent_hours = int(df['Absenteeism time in hours'].min()) + + high_risk_count = len(df[df['Absenteeism time in hours'] > 8]) + high_risk_ratio = round(high_risk_count / total_records, 4) + + return { + 'total_records': total_records, + 'total_employees': total_employees, + 'total_absent_hours': int(total_absent_hours), + 'avg_absent_hours': avg_absent_hours, + 'max_absent_hours': max_absent_hours, + 'min_absent_hours': min_absent_hours, + 'high_risk_ratio': high_risk_ratio + } + + def get_monthly_trend(self): + df = self.df + + monthly = df.groupby('Month of absence').agg({ + 'Absenteeism time in hours': ['sum', 'mean', 'count'] + }).reset_index() + + monthly.columns = ['month', 'total_hours', 'avg_hours', 'record_count'] + + months = ['1月', '2月', '3月', '4月', '5月', '6月', + '7月', '8月', '9月', '10月', '11月', '12月'] + + result = { + 'months': months, + 'total_hours': [], + 'avg_hours': [], + 'record_counts': [] + } + + for i in range(1, 13): + row = monthly[monthly['month'] == i] + if len(row) > 0: + result['total_hours'].append(int(row['total_hours'].values[0])) + result['avg_hours'].append(round(float(row['avg_hours'].values[0]), 2)) + result['record_counts'].append(int(row['record_count'].values[0])) + else: + result['total_hours'].append(0) + result['avg_hours'].append(0) + result['record_counts'].append(0) + + return result + + def get_weekday_distribution(self): + df = self.df + + weekday = df.groupby('Day of the week').agg({ + 'Absenteeism time in hours': ['sum', 'mean', 'count'] + }).reset_index() + + weekday.columns = ['weekday', 'total_hours', 'avg_hours', 'record_count'] + + result = { + 'weekdays': [], + 'weekday_codes': [], + 'total_hours': [], + 'avg_hours': [], + 'record_counts': [] + } + + for code in [2, 3, 4, 5, 6]: + row = weekday[weekday['weekday'] == code] + result['weekdays'].append(config.WEEKDAY_NAMES.get(code, str(code))) + result['weekday_codes'].append(code) + if len(row) > 0: + result['total_hours'].append(int(row['total_hours'].values[0])) + result['avg_hours'].append(round(float(row['avg_hours'].values[0]), 2)) + result['record_counts'].append(int(row['record_count'].values[0])) + else: + result['total_hours'].append(0) + result['avg_hours'].append(0) + result['record_counts'].append(0) + + return result + + def get_reason_distribution(self): + df = self.df + + reason = df.groupby('Reason for absence').agg({ + 'Absenteeism time in hours': 'count' + }).reset_index() + + reason.columns = ['code', 'count'] + reason = reason.sort_values('count', ascending=False) + + total = reason['count'].sum() + + result = { + 'reasons': [] + } + + for _, row in reason.iterrows(): + code = int(row['code']) + result['reasons'].append({ + 'code': code, + 'name': config.REASON_NAMES.get(code, f'原因{code}'), + 'count': int(row['count']), + 'percentage': round(row['count'] / total * 100, 1) + }) + + return result + + def get_season_distribution(self): + df = self.df + + season = df.groupby('Seasons').agg({ + 'Absenteeism time in hours': ['sum', 'mean', 'count'] + }).reset_index() + + season.columns = ['season', 'total_hours', 'avg_hours', 'record_count'] + + total_records = season['record_count'].sum() + + result = { + 'seasons': [] + } + + for code in [1, 2, 3, 4]: + row = season[season['season'] == code] + if len(row) > 0: + result['seasons'].append({ + 'code': int(code), + 'name': config.SEASON_NAMES.get(code, f'季节{code}'), + 'total_hours': int(row['total_hours'].values[0]), + 'avg_hours': round(float(row['avg_hours'].values[0]), 2), + 'record_count': int(row['record_count'].values[0]), + 'percentage': round(row['record_count'].values[0] / total_records * 100, 1) + }) + + return result + + +data_service = DataService() diff --git a/backend/services/predict_service.py b/backend/services/predict_service.py new file mode 100644 index 0000000..2000414 --- /dev/null +++ b/backend/services/predict_service.py @@ -0,0 +1,373 @@ +import os +import numpy as np +import joblib + +import config + + +MODEL_INFO = { + 'random_forest': { + 'name': 'random_forest', + 'name_cn': '随机森林', + 'description': '基于决策树的集成学习算法' + }, + 'xgboost': { + 'name': 'xgboost', + 'name_cn': 'XGBoost', + 'description': '高效的梯度提升算法' + }, + 'lightgbm': { + 'name': 'lightgbm', + 'name_cn': 'LightGBM', + 'description': '微软轻量级梯度提升框架' + }, + 'gradient_boosting': { + 'name': 'gradient_boosting', + 'name_cn': 'GBDT', + 'description': '梯度提升决策树' + }, + 'extra_trees': { + 'name': 'extra_trees', + 'name_cn': '极端随机树', + 'description': '随机森林的变体,随机性更强' + }, + 'stacking': { + 'name': 'stacking', + 'name_cn': 'Stacking集成', + 'description': '多层堆叠集成学习' + } +} + + +class PredictService: + def __init__(self): + self.models = {} + self.scaler = None + self.feature_names = None + self.selected_features = None + self.label_encoders = {} + self.model_metrics = {} + self.default_model = 'random_forest' + + def _ensure_models_loaded(self): + if not self.models: + self.load_models() + + def load_models(self): + model_files = { + 'random_forest': 'random_forest_model.pkl', + 'xgboost': 'xgboost_model.pkl', + 'lightgbm': 'lightgbm_model.pkl', + 'gradient_boosting': 'gradient_boosting_model.pkl', + 'extra_trees': 'extra_trees_model.pkl', + 'stacking': 'stacking_model.pkl' + } + + for name, filename in model_files.items(): + model_path = os.path.join(config.MODELS_DIR, filename) + if os.path.exists(model_path): + try: + self.models[name] = joblib.load(model_path) + print(f"Loaded {name} model") + except Exception as e: + print(f"Failed to load {name}: {e}") + + if os.path.exists(config.SCALER_PATH): + self.scaler = joblib.load(config.SCALER_PATH) + + feature_names_path = os.path.join(config.MODELS_DIR, 'feature_names.pkl') + if os.path.exists(feature_names_path): + self.feature_names = joblib.load(feature_names_path) + + selected_features_path = os.path.join(config.MODELS_DIR, 'selected_features.pkl') + if os.path.exists(selected_features_path): + self.selected_features = joblib.load(selected_features_path) + + label_encoders_path = os.path.join(config.MODELS_DIR, 'label_encoders.pkl') + if os.path.exists(label_encoders_path): + self.label_encoders = joblib.load(label_encoders_path) + + metrics_path = os.path.join(config.MODELS_DIR, 'model_metrics.pkl') + if os.path.exists(metrics_path): + self.model_metrics = joblib.load(metrics_path) + + if self.model_metrics: + valid_metrics = {k: v for k, v in self.model_metrics.items() if k in self.models} + if valid_metrics: + best_model = max(valid_metrics.items(), key=lambda x: x[1]['r2']) + self.default_model = best_model[0] + + def get_available_models(self): + self._ensure_models_loaded() + + models = [] + for name in self.models.keys(): + info = MODEL_INFO.get(name, { + 'name': name, + 'name_cn': name, + 'description': '' + }).copy() + info['is_available'] = True + info['is_default'] = (name == self.default_model) + + if name in self.model_metrics: + info['metrics'] = self.model_metrics[name] + else: + info['metrics'] = {'r2': 0, 'rmse': 0, 'mae': 0} + + models.append(info) + + models.sort(key=lambda x: x['metrics']['r2'], reverse=True) + + return models + + def predict_single(self, data, model_type=None): + self._ensure_models_loaded() + + if model_type is None: + model_type = self.default_model + + if model_type not in self.models: + available = list(self.models.keys()) + if available: + model_type = available[0] + else: + return self._get_default_prediction(data) + + model = self.models[model_type] + + if self.scaler is None or self.feature_names is None: + return self._get_default_prediction(data) + + features = self._prepare_features(data) + + try: + predicted_hours = model.predict([features])[0] + predicted_hours = max(0, float(predicted_hours)) + except Exception as e: + print(f"Prediction error: {e}") + return self._get_default_prediction(data) + + risk_level, risk_label = self._get_risk_level(predicted_hours) + + confidence = 0.85 + if model_type in self.model_metrics: + confidence = max(0.5, self.model_metrics[model_type].get('r2', 0.85)) + + return { + 'predicted_hours': round(predicted_hours, 2), + 'risk_level': risk_level, + 'risk_label': risk_label, + 'confidence': round(confidence, 2), + 'model_used': model_type, + 'model_name_cn': MODEL_INFO.get(model_type, {}).get('name_cn', model_type) + } + + def predict_compare(self, data): + self._ensure_models_loaded() + + results = [] + + for name in self.models.keys(): + try: + result = self.predict_single(data, name) + result['model'] = name + result['model_name_cn'] = MODEL_INFO.get(name, {}).get('name_cn', name) + + if name in self.model_metrics: + result['r2'] = self.model_metrics[name]['r2'] + else: + result['r2'] = 0 + + results.append(result) + except Exception as e: + print(f"Compare error for {name}: {e}") + + results.sort(key=lambda x: x.get('r2', 0), reverse=True) + + if results: + results[0]['recommended'] = True + + return results + + def _prepare_features(self, data): + feature_map = { + 'Reason for absence': data.get('reason_for_absence', 23), + 'Month of absence': data.get('month_of_absence', 7), + 'Day of the week': data.get('day_of_week', 3), + 'Seasons': data.get('seasons', 1), + 'Transportation expense': data.get('transportation_expense', 200), + 'Distance from Residence to Work': data.get('distance', 20), + 'Service time': data.get('service_time', 5), + 'Age': data.get('age', 30), + 'Work load Average/day': data.get('work_load', 250), + 'Hit target': data.get('hit_target', 95), + 'Disciplinary failure': data.get('disciplinary_failure', 0), + 'Education': data.get('education', 1), + 'Son': data.get('son', 0), + 'Social drinker': data.get('social_drinker', 0), + 'Social smoker': data.get('social_smoker', 0), + 'Pet': data.get('pet', 0), + 'Body mass index': data.get('bmi', 25) + } + + age = feature_map['Age'] + service_time = feature_map['Service time'] + work_load = feature_map['Work load Average/day'] + distance = feature_map['Distance from Residence to Work'] + expense = feature_map['Transportation expense'] + bmi = feature_map['Body mass index'] + son = feature_map['Son'] + pet = feature_map['Pet'] + social_drinker = feature_map['Social drinker'] + social_smoker = feature_map['Social smoker'] + hit_target = feature_map['Hit target'] + seasons = feature_map['Seasons'] + day_of_week = feature_map['Day of the week'] + + derived_features = { + 'workload_per_age': work_load / (age + 1), + 'expense_per_distance': expense / (distance + 1), + 'age_service_ratio': age / (service_time + 1), + 'has_children': 1 if son > 0 else 0, + 'has_pet': 1 if pet > 0 else 0, + 'family_responsibility': son + pet, + 'health_risk': 1 if (social_drinker == 1 or social_smoker == 1 or bmi > 30) else 0, + 'lifestyle_risk': int(social_drinker) + int(social_smoker), + 'age_group': 1 if age <= 30 else (2 if age <= 40 else (3 if age <= 50 else 4)), + 'service_group': 1 if service_time <= 5 else (2 if service_time <= 10 else (3 if service_time <= 20 else 4)), + 'bmi_category': 1 if bmi <= 18.5 else (2 if bmi <= 25 else (3 if bmi <= 30 else 4)), + 'workload_category': 1 if work_load <= 200 else (2 if work_load <= 250 else (3 if work_load <= 300 else 4)), + 'commute_category': 1 if distance <= 10 else (2 if distance <= 20 else (3 if distance <= 50 else 4)), + 'seasonal_risk': 1 if seasons in [1, 3] else 0, + 'weekday_risk': 1 if day_of_week in [2, 6] else 0, + 'hit_target_ratio': hit_target / 100, + 'experience_level': 1 if service_time <= 5 else (2 if service_time <= 10 else (3 if service_time <= 15 else 4)), + 'age_workload_interaction': age * work_load / 10000, + 'service_bmi_interaction': service_time * bmi / 100 + } + + all_features = {**feature_map, **derived_features} + + features = [] + for fname in self.feature_names: + if fname in all_features: + val = all_features[fname] + + if fname in self.label_encoders: + try: + val = self.label_encoders[fname].transform([str(val)])[0] + except: + val = 0 + + features.append(float(val)) + else: + features.append(0.0) + + features = np.array(features).reshape(1, -1) + features = self.scaler.transform(features)[0] + + if self.selected_features: + selected_indices = [] + for sf in self.selected_features: + if sf in self.feature_names: + selected_indices.append(self.feature_names.index(sf)) + if selected_indices: + features = features[selected_indices] + + return features + + def _get_risk_level(self, hours): + if hours < 4: + return 'low', '低风险' + elif hours <= 8: + return 'medium', '中风险' + else: + return 'high', '高风险' + + def _get_default_prediction(self, data): + base_hours = 5.0 + + expense = data.get('transportation_expense', 200) + if expense > 300: + base_hours += 1.0 + elif expense < 150: + base_hours -= 0.5 + + distance = data.get('distance', 20) + if distance > 40: + base_hours += 1.5 + elif distance > 25: + base_hours += 0.8 + + service_time = data.get('service_time', 5) + if service_time < 3: + base_hours += 0.5 + elif service_time > 15: + base_hours -= 0.5 + + age = data.get('age', 30) + if age > 50: + base_hours += 0.5 + elif age < 25: + base_hours += 0.3 + + work_load = data.get('work_load', 250) + if work_load > 300: + base_hours += 1.5 + elif work_load > 260: + base_hours += 0.5 + + bmi = data.get('bmi', 25) + if bmi > 30: + base_hours += 0.8 + elif bmi < 20: + base_hours += 0.3 + + if data.get('social_drinker', 0) == 1: + base_hours += 0.8 + if data.get('social_smoker', 0) == 1: + base_hours += 0.5 + + son = data.get('son', 0) + if son > 0: + base_hours += 0.3 * son + + pet = data.get('pet', 0) + if pet > 0: + base_hours -= 0.1 * pet + + hit_target = data.get('hit_target', 95) + if hit_target < 90: + base_hours += 0.5 + + base_hours = max(0.5, base_hours) + + risk_level, risk_label = self._get_risk_level(base_hours) + + return { + 'predicted_hours': round(base_hours, 2), + 'risk_level': risk_level, + 'risk_label': risk_label, + 'confidence': 0.75, + 'model_used': 'default', + 'model_name_cn': '默认规则' + } + + def get_model_info(self): + self._ensure_models_loaded() + + models = self.get_available_models() + + return { + 'models': models, + 'training_info': { + 'train_samples': 2884, + 'test_samples': 722, + 'feature_count': len(self.feature_names) if self.feature_names else 20, + 'training_date': '2026-03-08' + } + } + + +predict_service = PredictService() diff --git a/backend/utils/__init__.py b/backend/utils/__init__.py new file mode 100644 index 0000000..2f3b343 --- /dev/null +++ b/backend/utils/__init__.py @@ -0,0 +1 @@ +from .common import format_response, format_error diff --git a/backend/utils/common.py b/backend/utils/common.py new file mode 100644 index 0000000..8cffcb2 --- /dev/null +++ b/backend/utils/common.py @@ -0,0 +1,14 @@ +def format_response(data, code=200, message='success'): + return { + 'code': code, + 'message': message, + 'data': data + } + + +def format_error(message, code=400): + return { + 'code': code, + 'message': message, + 'data': None + } diff --git a/docs/0.md b/docs/0.md new file mode 100644 index 0000000..f957a1a --- /dev/null +++ b/docs/0.md @@ -0,0 +1,83 @@ +既然你的题目是**《基于多维特征挖掘的员工缺勤影响因素分析与预测研究》**,你的前端就不应该是一个“考勤录入系统”(比如点击“打卡”按钮),而应该是一个**“数据分析与可视化大屏”**。 +你的前端核心任务是:**把算法跑出来的结果,用图表漂亮地展示出来,并提供一个交互式的“预测窗口”。** +以下是为你规划的**前端功能模块(4-5个页面)**,每个页面都直接对应你的题目和算法: +--- +### 页面一:数据概览与全局统计 +**目的:** 让人一眼看懂数据集的整体情况。 +* **关键指标卡片(KPI):** + * 总样本数(例如:740) + * 平均缺勤时长 + * 高风险员工占比 + * 最常见的缺勤原因(例如:牙科咨询) +* **可视化图表:** + * **缺勤原因分布饼图:** 展示各种 ICD 疾病代码(或医疗咨询、献血等)的比例。 + * **每月缺勤趋势折线图:** 横轴是1-12月,纵轴是缺勤总时长,看看哪个月大家最爱请假(是不是夏天?)。 + * **星期几缺勤热力图:** 周一到周五,哪天颜色最深(缺勤最多)。 +--- +### 页面二:影响因素分析 —— **对应题目的“影响因素分析”** +**目的:** 展示你的核心算法成果(特征重要性、相关性),回答“为什么缺勤”。 +* **核心图表 1:特征重要性排序条形图** + * **内容:** 横轴是特征(距离、BMI、饮酒、工龄...),纵轴是重要性得分。 + * **设计:** 降序排列,最高的那个(比如 Reason for absence 或 Service time)在最上面或最左边。 + * **交互:** 鼠标悬停显示具体分数。 +* **核心图表 2:相关性热力图** + * **内容:** 展示各个字段之间的相关系数矩阵。 + * **亮点:** 高亮显示“饮酒”与“缺勤时长”的交点,或者“通勤距离”与“缺勤时长”的交点,颜色越深代表关联越强。 +* **群体对比分析:** + * **柱状图:** 饮酒者 vs 不饮酒者的平均缺勤时长对比。 + * **柱状图:** 高学历 vs 低学历的缺勤时长对比。 +--- +### 页面三:缺勤预测模型 —— **对应题目的“预测研究”** +**目的:** 提供一个交互窗口,演示你的 XGBoost/随机森林模型是如何工作的。 +* **左侧:参数输入表单** + * 设计一个表单,列出数据集中的关键字段(供用户填写): + * *ID*:随意填(如 36) + * *Reason for absence*:下拉菜单(1-28 或 归类后的“疾病/个人事务”) + * *Month*:下拉菜单(1-12) + * *Day of week*:下拉菜单(周一-周五) + * *Transportation expense*:滑动条或输入框(例如:200) + * *Distance from Residence to Work*:输入框(例如:15) + * *Service time*:输入框(例如:10年) + * *Age*:输入框(例如:35) + * *Work load Average/day*:输入框(例如:250000) + * *Hit target*:输入框(例如:90%) + * *Disciplinary failure*:单选框(是/否) + * *Education*:下拉菜单(高中/本科/硕士...) + * *Son*:数字输入(0, 1, 2...) + * *Social drinker*:单选框(是/否) + * *Social smoker*:单选框(是/否) + * *Pet*:数字输入 + * *Body mass index*:输入框(例如:25) + * **底部按钮:** **“开始预测”** +* **右侧:预测结果展示** + * **结果数字:** 预测的缺勤时长(例如:预测结果 8 小时)。 + * **风险等级:** + * < 4小时:绿色标签(低风险) + * 4-8小时:黄色标签(中风险) + * > 8小时:红色标签(高风险,警钟图标) + * **模型可信度:** 显示当前模型的准确率(例如:85% Accuracy)。 +--- +### 页面四:员工画像与聚类 —— **对应“多维特征挖掘”的进阶** +**目的:** 展示 K-Means 聚类算法挖掘出的群体特征。 +* **雷达图:** + * 画 3-4 个多边形,代表 3-4 类员工(如:模范型、压力型、生活习惯型)。 + * 轴向维度:[年龄, 工龄, 负荷, BMI, 缺勤倾向]。 + * 让人一眼看出不同群体的差异(例如:压力型的“负荷”轴特别长)。 +* **散点图:** + * 横轴:年龄,纵轴:缺勤时长。点按聚类结果着色(红点、蓝点、绿点)。 +--- +### 推荐技术栈(实现难度低,效果好) +为了在短时间内做出漂亮的图表,推荐以下组合: +1. **前端框架:** **Vue.js** (Vue 3) 或 **React**。推荐 Vue,国内毕设用得极多,文档好查。 +2. **UI 组件库:** **Element Plus** (配合 Vue) 或 **Ant Design**。 + * 这里面的表单组件、卡片、按钮可以直接拖过来用,不用自己写 CSS。 +3. **图表库:** **ECharts** (百度开源的)。 + * **必杀技:** 它的柱状图、饼图、雷达图、热力图效果非常炫酷,支持动画,非常适合答辩演示。 +4. **后端接口:** Python **Flask** 或 **FastAPI**。 + * 写几个简单的 API 接口(`/api/predict`, `/api/feature_importance`),前端调这些接口拿数据。 +### 答辩时的演示脚本 +1. **打开页面一:** “大家请看,这是 700 多条数据的概览,我们发现周五的缺勤率最高...” +2. **打开页面二:** “通过随机森林算法,我们计算了各因素的影响权重,发现‘通勤距离’和‘工作负荷’是导致缺勤的两大主因...” +3. **打开页面三:** “为了验证模型实用性,我构建了这个预测模块。假设我们有一个 35 岁、住得很远、爱喝酒的员工,系统预测他可能会缺勤 8 小时,属于高风险...” +4. **打开页面四:** “最后通过聚类分析,我们将员工分为了三类,红色群体是‘高压高负荷’群体,建议HR重点关注...” +这样一套下来,你的前端不仅漂亮,而且逻辑紧扣题目,绝对是加分项! diff --git a/docs/00_需求规格说明书.md b/docs/00_需求规格说明书.md new file mode 100644 index 0000000..bdc0d01 --- /dev/null +++ b/docs/00_需求规格说明书.md @@ -0,0 +1,609 @@ +# 需求规格说明书 + +## 基于多维特征挖掘的员工缺勤分析与预测系统 + +**文档版本**:V1.0 +**编写日期**:2026年3月 +**编写人**:张硕 + +--- + +## 1. 引言 + +### 1.1 编写目的 + +本文档旨在详细说明"基于多维特征挖掘的员工缺勤分析与预测系统"的功能需求和非功能需求,为系统的设计、开发、测试和验收提供依据。本文档的预期读者包括: + +- 项目指导教师 +- 系统开发人员 +- 测试人员 +- 项目评审专家 + +### 1.2 项目背景 + +#### 1.2.1 课题来源 + +本课题为河南农业大学软件学院本科毕业设计项目。 + +#### 1.2.2 项目背景 + +随着企业数字化转型的深入推进,人力资源管理正从经验驱动向数据驱动转变。员工缺勤作为影响企业运营效率的重要因素,其背后蕴含着丰富的多维度信息。传统的缺勤管理方式主要依赖人工统计和经验判断,缺乏对多维度特征之间复杂关系的深入挖掘。 + +本系统基于UCI Absenteeism数据集,利用机器学习算法对员工考勤数据进行深度分析,挖掘影响缺勤的多维度特征,构建缺勤预测模型,为企业人力资源管理提供科学、客观的决策支持。 + +#### 1.2.3 术语定义 + +| 术语 | 定义 | +|------|------| +| UCI | University of California Irvine,加州大学欧文分校,著名的机器学习数据集仓库 | +| ICD | International Classification of Diseases,国际疾病分类代码 | +| 缺勤 | 员工在应该工作的时间内未出勤的情况 | +| 特征挖掘 | 从原始数据中提取有价值的特征信息的过程 | +| K-Means | 一种经典的无监督聚类算法 | +| 随机森林 | 一种基于决策树的集成学习算法 | +| XGBoost | 一种高效的梯度提升算法 | + +--- + +## 2. 项目概述 + +### 2.1 项目目标 + +本项目的核心目标是设计并实现一个完整的员工缺勤分析与预测系统,具体目标如下: + +1. **数据概览**:提供直观的数据统计和可视化展示,帮助企业快速了解整体考勤状况 +2. **因素分析**:挖掘影响缺勤的关键因素,回答"为什么缺勤"的问题 +3. **风险预测**:构建预测模型,实现对员工缺勤风险的精准识别和预警 +4. **员工画像**:利用聚类算法对员工进行分群,实现精细化管理 + +### 2.2 功能概述 + +系统包含四大核心功能模块: + +| 模块编号 | 模块名称 | 功能概述 | +|----------|----------|----------| +| F01 | 数据概览与全局统计 | 展示基础统计指标、时间维度趋势、缺勤原因分布 | +| F02 | 多维特征挖掘与影响因素分析 | 特征重要性排序、相关性分析、群体对比 | +| F03 | 员工缺勤风险预测 | 单次预测、风险等级评估、模型性能展示 | +| F04 | 员工画像与群体聚类 | K-Means聚类、群体雷达图、散点图展示 | + +### 2.3 用户特征 + +系统的目标用户主要包括: + +| 用户类型 | 描述 | 主要使用场景 | +|----------|------|--------------| +| HR管理人员 | 企业人力资源部门工作人员 | 查看考勤统计、识别高风险员工、制定管理策略 | +| 部门主管 | 各业务部门负责人 | 了解本部门员工考勤情况、优化工作安排 | +| 数据分析师 | 企业数据分析人员 | 深入分析考勤数据、挖掘潜在规律 | + +### 2.4 运行环境 + +#### 2.4.1 硬件环境 + +| 项目 | 最低配置 | 推荐配置 | +|------|----------|----------| +| CPU | 双核 2.0GHz | 四核 2.5GHz及以上 | +| 内存 | 4GB | 8GB及以上 | +| 硬盘 | 10GB可用空间 | 20GB及以上 | +| 网络 | 10Mbps | 100Mbps及以上 | + +#### 2.4.2 软件环境 + +| 项目 | 要求 | +|------|------| +| 操作系统 | Windows 10/11、Linux、macOS | +| 浏览器 | Chrome 90+、Firefox 88+、Edge 90+ | +| Python版本 | 3.8及以上 | +| Node.js版本 | 16.0及以上 | + +--- + +## 3. 功能需求 + +### 3.1 F01 数据概览与全局统计 + +#### 3.1.1 F01-01 基础统计指标展示 + +**功能描述**:系统自动加载数据集,计算并展示关键统计指标。 + +**输入**:无(自动加载) + +**输出**: + +| 指标名称 | 说明 | +|----------|------| +| 样本总数 | 数据集中的记录总数 | +| 员工总数 | 去重后的员工人数 | +| 缺勤总时长 | 所有记录的缺勤小时数总和 | +| 平均缺勤时长 | 每条记录的平均缺勤小时数 | +| 最大缺勤时长 | 单次最大缺勤小时数 | +| 最小缺勤时长 | 单次最小缺勤小时数 | +| 高风险员工占比 | 缺勤时长超过8小时的员工比例 | + +**业务规则**: +- 高风险定义:单次缺勤时长 > 8小时 +- 统计数据实时计算,不缓存 + +**界面展示**:以KPI卡片形式展示,每个指标一张卡片。 + +--- + +#### 3.1.2 F01-02 月度缺勤趋势分析 + +**功能描述**:以折线图形式展示全年(1-12月)的缺勤变化趋势。 + +**输入**:无 + +**输出**: + +| 字段 | 说明 | +|------|------| +| month | 月份(1-12) | +| total_hours | 该月缺勤总时长 | +| avg_hours | 该月平均缺勤时长 | +| record_count | 该月记录数 | + +**界面展示**: +- 图表类型:折线图 +- 横轴:月份(1-12月) +- 纵轴:缺勤时长(小时) +- 支持鼠标悬停显示具体数值 + +--- + +#### 3.1.3 F01-03 星期分布分析 + +**功能描述**:分析周一至周五的缺勤分布情况。 + +**输入**:无 + +**输出**: + +| 字段 | 说明 | +|------|------| +| weekday | 星期(周一至周五) | +| total_hours | 该星期缺勤总时长 | +| avg_hours | 该星期平均缺勤时长 | +| record_count | 该星期记录数 | + +**界面展示**: +- 图表类型:柱状图或热力图 +- 横轴:星期(周一至周五) +- 纵轴:缺勤时长或记录数 + +--- + +#### 3.1.4 F01-04 缺勤原因分布分析 + +**功能描述**:展示各类缺勤原因的占比分布。 + +**输入**:无 + +**输出**: + +| 字段 | 说明 | +|------|------| +| reason_code | 缺勤原因代码(0-28) | +| reason_name | 缺勤原因名称 | +| count | 该原因出现次数 | +| percentage | 占比百分比 | + +**缺勤原因分类**: + +| 代码范围 | 类别 | 说明 | +|----------|------|------| +| 1-21 | ICD疾病 | 国际疾病分类代码 | +| 22 | 医疗随访 | 患者随访 | +| 23 | 医疗咨询 | 门诊咨询 | +| 24 | 献血 | 无偿献血 | +| 25 | 实验室检查 | 医学检查 | +| 26 | 无故缺勤 | 未经批准的缺勤 | +| 27 | 理疗 | 物理治疗 | +| 28 | 牙科咨询 | 口腔科就诊 | +| 0 | 未知 | 原因未记录 | + +**界面展示**: +- 图表类型:饼图 +- 显示各类原因的占比 +- 支持点击查看详情 + +--- + +### 3.2 F02 多维特征挖掘与影响因素分析 + +#### 3.2.1 F02-01 特征重要性排序 + +**功能描述**:利用训练好的随机森林模型,计算各维度特征对缺勤的影响权重。 + +**输入**:无 + +**输出**: + +| 字段 | 说明 | +|------|------| +| feature_name | 特征名称 | +| importance_score | 重要性得分(0-1) | +| rank | 排名 | + +**分析的特征包括**: + +| 特征名称 | 中文名称 | 特征类型 | +|----------|----------|----------| +| Reason for absence | 缺勤原因 | 类别型 | +| Month of absence | 缺勤月份 | 类别型 | +| Day of the week | 星期几 | 类别型 | +| Seasons | 季节 | 类别型 | +| Transportation expense | 交通费用 | 数值型 | +| Distance from Residence to Work | 通勤距离 | 数值型 | +| Service time | 工龄 | 数值型 | +| Age | 年龄 | 数值型 | +| Work load Average/day | 日均工作负荷 | 数值型 | +| Hit target | 达标率 | 数值型 | +| Disciplinary failure | 违纪记录 | 二分类 | +| Education | 学历 | 类别型 | +| Son | 子女数量 | 数值型 | +| Social drinker | 饮酒习惯 | 二分类 | +| Social smoker | 吸烟习惯 | 二分类 | +| Pet | 宠物数量 | 数值型 | +| Body mass index | BMI指数 | 数值型 | + +**界面展示**: +- 图表类型:水平柱状图 +- 按重要性得分降序排列 +- 鼠标悬停显示具体分数 + +--- + +#### 3.2.2 F02-02 相关性热力图分析 + +**功能描述**:计算特征之间的皮尔逊相关系数,以热力图形式展示。 + +**输入**:无 + +**输出**:相关系数矩阵(n×n) + +**重点关注的关联**: +- 生活习惯(饮酒、吸烟)与缺勤时长的相关性 +- 通勤距离与缺勤时长的相关性 +- 工作负荷与缺勤时长的相关性 + +**界面展示**: +- 图表类型:热力图 +- 颜色范围:-1(负相关,蓝色)到 +1(正相关,红色) +- 支持鼠标悬停显示具体相关系数 + +--- + +#### 3.2.3 F02-03 群体对比分析 + +**功能描述**:按不同维度分组,对比各组的平均缺勤时长。 + +**支持的对比维度**: + +| 维度 | 分组 | +|------|------| +| 饮酒习惯 | 饮酒者 vs 不饮酒者 | +| 吸烟习惯 | 吸烟者 vs 不吸烟者 | +| 学历 | 高中 vs 本科 vs 研究生及以上 | +| 是否有子女 | 有子女 vs 无子女 | +| 是否有宠物 | 有宠物 vs 无宠物 | + +**输出**: + +| 字段 | 说明 | +|------|------| +| group_name | 分组名称 | +| avg_hours | 平均缺勤时长 | +| count | 记录数 | + +**界面展示**: +- 图表类型:分组柱状图 +- 支持切换不同的对比维度 +- 显示差异百分比 + +--- + +### 3.3 F03 员工缺勤风险预测 + +#### 3.3.1 F03-01 单次缺勤预测 + +**功能描述**:接收用户输入的员工属性,调用预测模型返回预测的缺勤时长。 + +**输入参数**: + +| 参数名 | 类型 | 取值范围 | 必填 | +|--------|------|----------|------| +| reason_for_absence | int | 0-28 | 是 | +| month_of_absence | int | 1-12 | 是 | +| day_of_week | int | 2-6 | 是 | +| seasons | int | 1-4 | 是 | +| transportation_expense | int | 100-400 | 是 | +| distance | int | 1-60 | 是 | +| service_time | int | 1-30 | 是 | +| age | int | 18-60 | 是 | +| work_load | float | 200-350 | 是 | +| hit_target | int | 80-100 | 是 | +| disciplinary_failure | int | 0-1 | 是 | +| education | int | 1-4 | 是 | +| son | int | 0-5 | 是 | +| social_drinker | int | 0-1 | 是 | +| social_smoker | int | 0-1 | 是 | +| pet | int | 0-10 | 是 | +| bmi | float | 18-40 | 是 | + +**输出**: + +| 字段 | 说明 | +|------|------| +| predicted_hours | 预测的缺勤时长(小时) | +| risk_level | 风险等级(low/medium/high) | +| confidence | 模型置信度 | + +**风险等级判定规则**: + +| 预测时长 | 风险等级 | 颜色标识 | +|----------|----------|----------| +| < 4小时 | 低风险(low) | 绿色 | +| 4-8小时 | 中风险(medium) | 黄色 | +| > 8小时 | 高风险(high) | 红色 | + +**界面展示**: +- 左侧:参数输入表单 +- 右侧:预测结果展示 +- 底部:开始预测按钮 + +--- + +#### 3.3.2 F03-02 风险等级评估 + +**功能描述**:根据预测结果,自动评估并展示风险等级。 + +**业务规则**: +- 风险等级根据预测时长自动计算 +- 高风险员工需要特别关注标识 +- 支持风险等级的筛选和统计 + +--- + +#### 3.3.3 F03-03 模型性能展示 + +**功能描述**:展示当前预测模型的性能指标。 + +**输出指标**: + +| 指标名称 | 说明 | 目标值 | +|----------|------|--------| +| R² | 决定系数 | ≥ 0.80 | +| MSE | 均方误差 | - | +| RMSE | 均方根误差 | - | +| MAE | 平均绝对误差 | - | +| 训练样本数 | 模型训练使用的样本量 | - | + +**界面展示**: +- 以卡片形式展示各指标 +- 包含模型类型说明(随机森林/XGBoost) + +--- + +### 3.4 F04 员工画像与群体聚类 + +#### 3.4.1 F04-01 K-Means聚类结果展示 + +**功能描述**:利用K-Means算法对员工进行聚类分析。 + +**输入参数**(可选): + +| 参数名 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| n_clusters | int | 3 | 聚类数量 | + +**输出**: + +| 字段 | 说明 | +|------|------| +| cluster_id | 聚类编号 | +| cluster_name | 聚类名称(自动生成或人工标注) | +| member_count | 该聚类包含的员工数 | +| center_point | 聚类中心点坐标 | + +**聚类特征维度**: +- 年龄 +- 工龄 +- 工作负荷 +- BMI指数 +- 缺勤倾向 + +--- + +#### 3.4.2 F04-02 员工群体雷达图 + +**功能描述**:以雷达图形式展示各聚类群体的特征分布。 + +**输出**: +- 各聚类在多个维度上的特征值(归一化后) + +**界面展示**: +- 图表类型:雷达图 +- 每个聚类用不同颜色表示 +- 维度:年龄、工龄、工作负荷、BMI、缺勤倾向 + +--- + +#### 3.4.3 F04-03 聚类散点图 + +**功能描述**:以散点图形式展示员工在聚类空间的分布。 + +**输出**: + +| 字段 | 说明 | +|------|------| +| employee_id | 员工ID | +| x | 横坐标(年龄或PCA降维后的第一主成分) | +| y | 纵坐标(缺勤时长或PCA降维后的第二主成分) | +| cluster_id | 所属聚类编号 | + +**界面展示**: +- 图表类型:散点图 +- 不同聚类用不同颜色区分 +- 支持鼠标悬停查看员工详情 + +--- + +## 4. 非功能需求 + +### 4.1 性能需求 + +| 指标 | 要求 | +|------|------| +| 页面加载时间 | 首屏加载时间 ≤ 3秒 | +| 接口响应时间 | 普通查询接口 ≤ 500ms | +| 预测接口响应时间 | ≤ 1秒 | +| 并发用户数 | 支持10个并发用户 | +| 数据处理能力 | 支持10000条以上记录处理 | + +### 4.2 安全需求 + +| 需求项 | 说明 | +|--------|------| +| 数据安全 | 数据文件存储安全,防止未授权访问 | +| 接口安全 | API接口具备基本的访问控制 | +| 输入验证 | 前后端均需对用户输入进行校验 | +| 错误处理 | 不向前端暴露敏感的错误信息 | + +### 4.3 可用性需求 + +| 需求项 | 说明 | +|--------|------| +| 界面友好 | 界面简洁明了,操作直观 | +| 响应式设计 | 支持不同屏幕尺寸访问 | +| 错误提示 | 提供清晰的错误提示和操作引导 | +| 帮助信息 | 关键功能提供操作提示 | +| 可访问性 | 支持主流浏览器访问 | + +### 4.4 兼容性需求 + +| 类型 | 要求 | +|------|------| +| 浏览器兼容 | Chrome 90+、Firefox 88+、Edge 90+、Safari 14+ | +| 操作系统 | Windows 10/11、macOS 10.15+、主流Linux发行版 | +| 屏幕分辨率 | 支持1366×768及以上分辨率 | + +### 4.5 可维护性需求 + +| 需求项 | 说明 | +|--------|------| +| 代码规范 | 遵循Python PEP8和Vue风格指南 | +| 注释文档 | 关键代码提供注释说明 | +| 模块化设计 | 高内聚低耦合,便于维护扩展 | +| 版本控制 | 使用Git进行版本管理 | + +--- + +## 5. 用例图与用例描述 + +### 5.1 用例图 + +``` + +------------------------------------------+ + | 员工缺勤分析与预测系统 | + | | + | +------------------+ | + | | F01 数据概览 | | + | +------------------+ | + | | - 基础统计 | | + | | - 月度趋势 | | + | | - 星期分布 | | + | | - 原因分布 | | + | +------------------+ | + | | + | +------------------+ | + | | F02 影响因素分析 | | + | +------------------+ | + +--------+ | | - 特征重要性 | +--------+ | + | |------>| | - 相关性分析 |<------| | | + | 用户 | | | - 群体对比 | | 用户 | | + | |<------| +------------------+ | | | + +--------+ | +--------+ | + | +------------------+ | | | + | | F03 缺勤预测 |<----| 用户 | | + | +------------------+ | | | + | | - 单次预测 | +--------+ | + | | - 风险评估 | | + | | - 模型性能 | | + | +------------------+ | + | | + | +------------------+ | + | | F04 员工画像 | | + | +------------------+ | + | | - 聚类结果 | | + | | - 群体雷达图 | | + | | - 散点图 | | + | +------------------+ | + | | + +------------------------------------------+ +``` + +### 5.2 用例详细描述 + +#### UC01 查看数据概览 + +| 项目 | 描述 | +|------|------| +| 用例名称 | 查看数据概览 | +| 参与者 | 用户 | +| 前置条件 | 用户已打开系统 | +| 主要流程 | 1. 系统加载数据集
2. 计算基础统计指标
3. 展示KPI卡片
4. 渲染月度趋势图
5. 渲染星期分布图
6. 渲染原因分布饼图 | +| 后置条件 | 数据概览页面展示完成 | +| 异常流程 | 数据加载失败时显示错误提示 | + +#### UC02 分析影响因素 + +| 项目 | 描述 | +|------|------| +| 用例名称 | 分析影响因素 | +| 参与者 | 用户 | +| 前置条件 | 预测模型已训练完成 | +| 主要流程 | 1. 加载训练好的模型
2. 提取特征重要性
3. 计算相关系数矩阵
4. 展示特征重要性柱状图
5. 展示相关性热力图
6. 支持切换群体对比维度 | +| 后置条件 | 影响因素分析结果展示完成 | + +#### UC03 进行缺勤预测 + +| 项目 | 描述 | +|------|------| +| 用例名称 | 进行缺勤预测 | +| 参与者 | 用户 | +| 前置条件 | 预测模型已训练完成 | +| 主要流程 | 1. 用户填写员工属性表单
2. 点击"开始预测"按钮
3. 系统调用预测模型
4. 返回预测结果
5. 展示风险等级 | +| 后置条件 | 预测结果展示完成 | +| 异常流程 | 输入参数不合法时提示错误 | + +#### UC04 查看员工画像 + +| 项目 | 描述 | +|------|------| +| 用例名称 | 查看员工画像 | +| 参与者 | 用户 | +| 前置条件 | 聚类模型已训练完成 | +| 主要流程 | 1. 执行K-Means聚类
2. 计算聚类中心
3. 展示聚类结果
4. 渲染群体雷达图
5. 渲染散点分布图 | +| 后置条件 | 员工画像展示完成 | + +--- + +## 6. 附录 + +### 6.1 参考文档 + +1. UCI Machine Learning Repository. Absenteeism at work Data Set +2. 开题报告文档 +3. 项目架构设计文档 + +### 6.2 文档修改历史 + +| 版本 | 日期 | 修改人 | 修改内容 | +|------|------|--------|----------| +| V1.0 | 2026-03 | 张硕 | 初始版本 | + +--- + +**文档结束** diff --git a/docs/01_系统架构设计.md b/docs/01_系统架构设计.md new file mode 100644 index 0000000..d94ebe6 --- /dev/null +++ b/docs/01_系统架构设计.md @@ -0,0 +1,613 @@ +# 系统架构设计文档 + +## 基于多维特征挖掘的员工缺勤分析与预测系统 + +**文档版本**:V1.0 +**编写日期**:2026年3月 +**编写人**:张硕 + +--- + +## 1. 概述 + +### 1.1 设计目标 + +本系统架构设计旨在实现以下目标: + +1. **高可用性**:系统稳定可靠,能够持续提供服务 +2. **可扩展性**:便于后续功能扩展和算法升级 +3. **可维护性**:代码结构清晰,便于理解和维护 +4. **高性能**:快速响应前端请求,提供流畅的用户体验 + +### 1.2 设计原则 + +| 原则 | 说明 | +|------|------| +| 分层设计 | 前后端分离,后端采用三层架构 | +| 模块化 | 功能模块独立,高内聚低耦合 | +| 单一职责 | 每个模块只负责一个特定功能 | +| 开闭原则 | 对扩展开放,对修改关闭 | +| 接口隔离 | 接口设计精简,避免冗余 | + +--- + +## 2. 系统架构 + +### 2.1 整体架构图 + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ 用户层 (User Layer) │ +│ 浏览器 (Chrome/Firefox/Edge) │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 前端层 (Frontend Layer) │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Dashboard │ │ FactorAnalysis│ │ Prediction │ │ Clustering │ │ +│ │ 数据概览 │ │ 影响因素 │ │ 缺勤预测 │ │ 员工画像 │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ 公共组件 (ChartComponent, ResultCard) │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ Vue 3 + Element Plus + ECharts + Axios + Vue Router │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + │ HTTP/REST API + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 后端层 (Backend Layer) │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ API Layer (api/) │ │ +│ │ overview_routes │ analysis_routes │ predict_routes │ │ +│ │ cluster_routes │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ Service Layer (services/) │ │ +│ │ data_service │ analysis_service │ predict_service │ │ +│ │ cluster_service │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ Core Layer (core/) │ │ +│ │ preprocessing │ feature_mining │ train_model │ │ +│ │ clustering │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ Flask + scikit-learn + XGBoost + pandas │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 数据层 (Data Layer) │ +│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ data/raw/ │ │ data/processed/ │ │ models/ │ │ +│ │ 原始CSV数据 │ │ 处理后数据 │ │ 模型文件.pkl │ │ +│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### 2.2 技术架构 + +``` +┌────────────────────────────────────────────────────────────────┐ +│ 技术栈总览 │ +├────────────────────────────────────────────────────────────────┤ +│ │ +│ 前端技术栈 后端技术栈 │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ Vue 3 │ │ Python 3.8+ │ │ +│ │ Element Plus │ │ Flask │ │ +│ │ ECharts 5 │ ◄─────► │ scikit-learn │ │ +│ │ Axios │ HTTP │ XGBoost │ │ +│ │ Vue Router │ REST │ pandas │ │ +│ │ Vite │ │ numpy │ │ +│ └──────────────────┘ │ joblib │ │ +│ └──────────────────┘ │ +│ │ +│ 算法技术 数据存储 │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ 随机森林 (RF) │ │ CSV文件 │ │ +│ │ XGBoost │ │ PKL模型文件 │ │ +│ │ K-Means │ │ JSON响应 │ │ +│ │ StandardScaler │ │ │ │ +│ │ OneHotEncoder │ │ │ │ +│ └──────────────────┘ └──────────────────┘ │ +│ │ +└────────────────────────────────────────────────────────────────┘ +``` + +### 2.3 部署架构 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 单机部署架构 │ +│ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ 服务器 │ │ +│ │ │ │ +│ │ ┌─────────────────┐ ┌─────────────────┐ │ │ +│ │ │ Flask Server │ │ Vite Dev │ │ │ +│ │ │ Port: 5000 │ │ Port: 5173 │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ - REST API │ │ - Vue App │ │ │ +│ │ │ - ML Models │ │ - Static │ │ │ +│ │ │ - Data Files │ │ │ │ │ +│ │ └─────────────────┘ └─────────────────┘ │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────────────┐ │ │ +│ │ │ 文件系统 │ │ │ +│ │ │ /backend/data/ - 数据文件 │ │ │ +│ │ │ /backend/models/ - 模型文件 │ │ │ +│ │ │ /frontend/dist/ - 前端构建产物 │ │ │ +│ │ └─────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 3. 后端架构设计 + +### 3.1 分层设计 + +后端采用经典的三层架构,各层职责明确: + +| 层级 | 目录 | 职责 | 依赖关系 | +|------|------|------|----------| +| API层 | api/ | 接收HTTP请求,参数校验,调用服务层,返回响应 | 依赖Service层 | +| Service层 | services/ | 业务逻辑处理,协调Core层和Data层 | 依赖Core层 | +| Core层 | core/ | 核心算法实现,数据处理,模型训练 | 无依赖 | + +### 3.2 模块划分 + +``` +backend/ +├── app.py # 应用入口,Flask实例配置 +├── config.py # 配置文件(路径、参数等) +├── requirements.txt # Python依赖清单 +│ +├── api/ # API接口层 +│ ├── __init__.py +│ ├── overview_routes.py # 数据概览接口 +│ ├── analysis_routes.py # 影响因素分析接口 +│ ├── predict_routes.py # 预测接口 +│ └── cluster_routes.py # 聚类接口 +│ +├── services/ # 业务逻辑层 +│ ├── __init__.py +│ ├── data_service.py # 数据服务 +│ ├── analysis_service.py # 分析服务 +│ ├── predict_service.py # 预测服务 +│ └── cluster_service.py # 聚类服务 +│ +├── core/ # 核心算法层 +│ ├── __init__.py +│ ├── preprocessing.py # 数据预处理 +│ ├── feature_mining.py # 特征挖掘 +│ ├── train_model.py # 模型训练 +│ └── clustering.py # 聚类分析 +│ +├── data/ # 数据存储 +│ ├── raw/ # 原始数据 +│ │ └── Absenteeism_at_work.csv +│ └── processed/ # 处理后数据 +│ └── clean_data.csv +│ +├── models/ # 模型存储 +│ ├── rf_model.pkl # 随机森林模型 +│ ├── xgb_model.pkl # XGBoost模型 +│ ├── kmeans_model.pkl # K-Means模型 +│ ├── scaler.pkl # 标准化器 +│ └── encoder.pkl # 编码器 +│ +└── utils/ # 工具函数 + ├── __init__.py + └── common.py # 通用工具函数 +``` + +### 3.3 各模块职责详解 + +#### 3.3.1 API层 (api/) + +| 文件 | 职责 | 主要接口 | +|------|------|----------| +| overview_routes.py | 数据概览相关接口 | /api/overview/stats, /api/overview/trend | +| analysis_routes.py | 影响因素分析接口 | /api/analysis/importance, /api/analysis/correlation | +| predict_routes.py | 缺勤预测接口 | /api/predict/single, /api/predict/model-info | +| cluster_routes.py | 聚类分析接口 | /api/cluster/result, /api/cluster/profile | + +#### 3.3.2 Service层 (services/) + +| 文件 | 职责 | 核心方法 | +|------|------|----------| +| data_service.py | 数据读取与基础统计 | get_raw_data(), get_statistics() | +| analysis_service.py | 特征分析业务逻辑 | get_importance(), get_correlation() | +| predict_service.py | 预测业务逻辑 | predict_single(), load_model() | +| cluster_service.py | 聚类业务逻辑 | get_clusters(), get_profile() | + +#### 3.3.3 Core层 (core/) + +| 文件 | 职责 | 核心类/方法 | +|------|------|-------------| +| preprocessing.py | 数据预处理 | DataPreprocessor类 | +| feature_mining.py | 特征挖掘 | calculate_importance(), calculate_correlation() | +| train_model.py | 模型训练 | train_rf(), train_xgboost() | +| clustering.py | 聚类分析 | KMeansAnalyzer类 | + +--- + +## 4. 前端架构设计 + +### 4.1 组件化设计 + +``` +frontend/src/ +├── components/ # 公共组件 +│ ├── ChartComponent.vue # ECharts图表封装组件 +│ ├── ResultCard.vue # 预测结果展示卡片 +│ ├── KPICard.vue # KPI指标卡片 +│ └── LoadingSpinner.vue # 加载动画组件 +│ +├── views/ # 页面组件 +│ ├── Dashboard.vue # 数据概览页 +│ ├── FactorAnalysis.vue # 影响因素分析页 +│ ├── Prediction.vue # 缺勤预测页 +│ └── Clustering.vue # 员工画像页 +│ +├── api/ # API调用 +│ ├── request.js # Axios封装 +│ ├── overview.js # 概览API +│ ├── analysis.js # 分析API +│ ├── predict.js # 预测API +│ └── cluster.js # 聚类API +│ +├── router/ # 路由配置 +│ └── index.js +│ +├── assets/ # 静态资源 +│ └── styles/ +│ └── main.css +│ +├── App.vue # 根组件 +└── main.js # 入口文件 +``` + +### 4.2 状态管理 + +由于本项目状态较为简单,不引入Vuex/Pinia,使用以下方式管理状态: + +- **组件内部状态**:使用Vue 3的ref/reactive +- **跨组件通信**:使用props和emit +- **API状态**:在API层统一管理 + +### 4.3 路由设计 + +```javascript +const routes = [ + { + path: '/', + redirect: '/dashboard' + }, + { + path: '/dashboard', + name: 'Dashboard', + component: () => import('@/views/Dashboard.vue'), + meta: { title: '数据概览' } + }, + { + path: '/analysis', + name: 'FactorAnalysis', + component: () => import('@/views/FactorAnalysis.vue'), + meta: { title: '影响因素分析' } + }, + { + path: '/prediction', + name: 'Prediction', + component: () => import('@/views/Prediction.vue'), + meta: { title: '缺勤预测' } + }, + { + path: '/clustering', + name: 'Clustering', + component: () => import('@/views/Clustering.vue'), + meta: { title: '员工画像' } + } +] +``` + +--- + +## 5. 算法架构设计 + +### 5.1 数据预处理流程 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 数据预处理流程 │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ 原始CSV数据 │───►│ 数据清洗 │───►│ 特征分离 │ │ +│ │ │ │ (缺失值处理) │ │ │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ 处理后数据 │◄───│ 特征合并 │◄───│ 特征编码 │ │ +│ │ clean_data │ │ │ │ + 标准化 │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ +│ 特征处理方式: │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ 类别型特征 → OneHotEncoder │ │ +│ │ - Reason for absence │ │ +│ │ - Month, Day, Seasons │ │ +│ │ - Education, Disciplinary failure │ │ +│ │ - Social drinker, Social smoker │ │ +│ ├────────────────────────────────────────────────────────┤ │ +│ │ 数值型特征 → StandardScaler │ │ +│ │ - Transportation expense │ │ +│ │ - Distance, Service time, Age │ │ +│ │ - Work load, Hit target │ │ +│ │ - Son, Pet, BMI │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 5.2 特征挖掘流程 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 特征挖掘流程 │ +│ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ 特征重要性计算 │ │ +│ │ │ │ +│ │ 训练数据 ──► 随机森林模型 ──► feature_importances_ │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ 特征重要性排序结果 │ │ +│ │ │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ 相关性分析 │ │ +│ │ │ │ +│ │ 数据矩阵 ──► pandas.DataFrame.corr() ──► 相关系数矩阵 │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ 热力图数据 │ │ +│ │ │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 5.3 预测模型流程 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 预测模型流程 │ +│ │ +│ 训练阶段: │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ 处理后数据 │───►│ 划分数据集 │───►│ 模型训练 │ │ +│ │ │ │ Train/Test │ │ RF + XGBoost │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────┐ │ +│ │ 模型评估 │ │ +│ │ - R² (决定系数) │ │ +│ │ - MSE (均方误差) │ │ +│ │ - RMSE (均方根误差) │ │ +│ └──────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────┐ │ +│ │ 保存模型 (.pkl文件) │ │ +│ └──────────────────────────────────┘ │ +│ │ +│ 预测阶段: │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ 用户输入 │───►│ 特征预处理 │───►│ 加载模型 │ │ +│ │ (表单数据) │ │ (编码+标准化)│ │ 预测推理 │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────┐ │ +│ │ 返回预测结果 │ │ +│ │ - 预测时长 │ │ +│ │ - 风险等级 │ │ +│ │ - 置信度 │ │ +│ └──────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 5.4 聚类分析流程 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 聚类分析流程 │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ 处理后数据 │───►│ 特征选择 │───►│ K-Means │ │ +│ │ │ │ (关键维度) │ │ 聚类 │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ 聚类结果 │ │ +│ │ │ │ +│ │ ┌─────────────────┐ ┌─────────────────┐ │ │ +│ │ │ 聚类标签 │ │ 聚类中心 │ │ │ +│ │ │ (每条记录所属簇) │ │ (每个簇的中心点) │ │ │ +│ │ └─────────────────┘ └─────────────────┘ │ │ +│ │ │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ 可视化输出 │ │ +│ │ │ │ +│ │ - 雷达图:展示各聚类群体的特征分布 │ │ +│ │ - 散点图:展示员工在聚类空间的分布 │ │ +│ │ - 统计表:各聚类的成员数量、特征均值 │ │ +│ │ │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 6. 技术选型 + +### 6.1 后端技术栈 + +| 技术 | 版本 | 用途 | 选择理由 | +|------|------|------|----------| +| Python | 3.8+ | 开发语言 | 丰富的数据科学和机器学习库 | +| Flask | 2.x | Web框架 | 轻量级,易于上手,适合中小项目 | +| scikit-learn | 1.x | 机器学习 | 提供完整的机器学习工具链 | +| XGBoost | 1.x | 梯度提升 | 高性能,适合结构化数据预测 | +| pandas | 1.x | 数据处理 | 强大的数据分析和处理能力 | +| numpy | 1.x | 数值计算 | 高效的数组操作 | +| joblib | 1.x | 模型持久化 | 高效的模型序列化 | + +### 6.2 前端技术栈 + +| 技术 | 版本 | 用途 | 选择理由 | +|------|------|------|----------| +| Vue | 3.x | 前端框架 | 组合式API,性能优秀,生态完善 | +| Element Plus | 2.x | UI组件库 | 组件丰富,文档完善,适合管理后台 | +| ECharts | 5.x | 图表库 | 功能强大,图表类型丰富,国内主流 | +| Axios | 1.x | HTTP客户端 | Promise支持,拦截器功能完善 | +| Vue Router | 4.x | 路由管理 | Vue官方路由解决方案 | +| Vite | 4.x | 构建工具 | 开发体验好,构建速度快 | + +### 6.3 算法选型 + +| 算法 | 用途 | 选择理由 | +|------|------|----------| +| 随机森林 | 特征重要性计算、预测 | 可解释性强,能输出特征重要性 | +| XGBoost | 预测模型 | 性能优异,适合回归任务 | +| K-Means | 员工聚类 | 简单高效,适合无监督聚类 | +| StandardScaler | 数值标准化 | 消除量纲影响,提高模型效果 | +| OneHotEncoder | 类别编码 | 处理类别型特征的标准方法 | + +--- + +## 7. 附录 + +### 7.1 目录结构完整版 + +``` +Absenteeism_Analysis_System/ +│ +├── backend/ # 后端项目 +│ ├── app.py # 应用入口 +│ ├── config.py # 配置文件 +│ ├── requirements.txt # 依赖清单 +│ │ +│ ├── api/ # API接口层 +│ │ ├── __init__.py +│ │ ├── overview_routes.py +│ │ ├── analysis_routes.py +│ │ ├── predict_routes.py +│ │ └── cluster_routes.py +│ │ +│ ├── services/ # 业务逻辑层 +│ │ ├── __init__.py +│ │ ├── data_service.py +│ │ ├── analysis_service.py +│ │ ├── predict_service.py +│ │ └── cluster_service.py +│ │ +│ ├── core/ # 核心算法层 +│ │ ├── __init__.py +│ │ ├── preprocessing.py +│ │ ├── feature_mining.py +│ │ ├── train_model.py +│ │ └── clustering.py +│ │ +│ ├── data/ # 数据目录 +│ │ ├── raw/ +│ │ │ └── Absenteeism_at_work.csv +│ │ └── processed/ +│ │ └── clean_data.csv +│ │ +│ ├── models/ # 模型目录 +│ │ ├── rf_model.pkl +│ │ ├── xgb_model.pkl +│ │ ├── kmeans_model.pkl +│ │ ├── scaler.pkl +│ │ └── encoder.pkl +│ │ +│ └── utils/ # 工具函数 +│ ├── __init__.py +│ └── common.py +│ +├── frontend/ # 前端项目 +│ ├── public/ +│ ├── src/ +│ │ ├── api/ +│ │ │ ├── request.js +│ │ │ ├── overview.js +│ │ │ ├── analysis.js +│ │ │ ├── predict.js +│ │ │ └── cluster.js +│ │ ├── assets/ +│ │ │ └── styles/ +│ │ │ └── main.css +│ │ ├── components/ +│ │ │ ├── ChartComponent.vue +│ │ │ ├── ResultCard.vue +│ │ │ ├── KPICard.vue +│ │ │ └── LoadingSpinner.vue +│ │ ├── router/ +│ │ │ └── index.js +│ │ ├── views/ +│ │ │ ├── Dashboard.vue +│ │ │ ├── FactorAnalysis.vue +│ │ │ ├── Prediction.vue +│ │ │ └── Clustering.vue +│ │ ├── App.vue +│ │ └── main.js +│ ├── index.html +│ ├── package.json +│ ├── pnpm-lock.yaml +│ └── vite.config.js +│ +├── docs/ # 文档目录 +│ ├── 00_需求规格说明书.md +│ ├── 01_系统架构设计.md +│ ├── 02_接口设计文档.md +│ ├── 03_数据设计文档.md +│ ├── 04_UI原型设计.md +│ └── ... +│ +├── data/ # 原始数据(项目根目录) +│ └── Absenteeism_at_work.csv +│ +└── README.md # 项目说明 +``` + +### 7.2 文档修改历史 + +| 版本 | 日期 | 修改人 | 修改内容 | +|------|------|--------|----------| +| V1.0 | 2026-03 | 张硕 | 初始版本 | + +--- + +**文档结束** diff --git a/docs/02_接口设计文档.md b/docs/02_接口设计文档.md new file mode 100644 index 0000000..f397bb6 --- /dev/null +++ b/docs/02_接口设计文档.md @@ -0,0 +1,891 @@ +# 接口设计文档 + +## 基于多维特征挖掘的员工缺勤分析与预测系统 + +**文档版本**:V1.0 +**编写日期**:2026年3月 +**编写人**:张硕 + +--- + +## 1. 概述 + +### 1.1 接口规范 + +本系统采用RESTful API设计风格,所有接口遵循以下规范: + +| 项目 | 规范 | +|------|------| +| 协议 | HTTP/HTTPS | +| 数据格式 | JSON | +| 字符编码 | UTF-8 | +| 时间格式 | ISO 8601 (YYYY-MM-DD HH:mm:ss) | + +### 1.2 基础路径 + +| 环境 | 基础路径 | +|------|----------| +| 开发环境 | http://localhost:5000/api | +| 生产环境 | http://your-domain/api | + +### 1.3 响应格式 + +#### 成功响应 + +```json +{ + "code": 200, + "message": "success", + "data": { + // 具体数据 + } +} +``` + +#### 错误响应 + +```json +{ + "code": 400, + "message": "错误描述", + "data": null +} +``` + +### 1.4 状态码说明 + +| 状态码 | 说明 | +|--------|------| +| 200 | 请求成功 | +| 400 | 请求参数错误 | +| 404 | 资源不存在 | +| 500 | 服务器内部错误 | + +--- + +## 2. 数据概览模块 + +### 2.1 获取基础统计指标 + +**接口路径**:`GET /api/overview/stats` + +**接口描述**:获取数据集的基础统计指标 + +**请求参数**:无 + +**响应示例**: + +```json +{ + "code": 200, + "message": "success", + "data": { + "total_records": 740, + "total_employees": 36, + "total_absent_hours": 1184, + "avg_absent_hours": 1.6, + "max_absent_hours": 120, + "min_absent_hours": 0, + "high_risk_ratio": 0.15 + } +} +``` + +**字段说明**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| total_records | int | 总记录数 | +| total_employees | int | 员工总数 | +| total_absent_hours | float | 缺勤总时长(小时) | +| avg_absent_hours | float | 平均缺勤时长(小时) | +| max_absent_hours | int | 最大缺勤时长(小时) | +| min_absent_hours | int | 最小缺勤时长(小时) | +| high_risk_ratio | float | 高风险员工占比 | + +--- + +### 2.2 获取月度趋势数据 + +**接口路径**:`GET /api/overview/trend` + +**接口描述**:获取全年12个月的缺勤趋势数据 + +**请求参数**:无 + +**响应示例**: + +```json +{ + "code": 200, + "message": "success", + "data": { + "months": ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"], + "total_hours": [80, 65, 90, 75, 100, 85, 110, 95, 70, 88, 92, 78], + "avg_hours": [1.2, 1.0, 1.4, 1.1, 1.5, 1.3, 1.7, 1.4, 1.1, 1.3, 1.4, 1.2], + "record_counts": [67, 65, 64, 68, 67, 65, 65, 68, 64, 68, 66, 65] + } +} +``` + +**字段说明**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| months | string[] | 月份列表 | +| total_hours | float[] | 每月缺勤总时长 | +| avg_hours | float[] | 每月平均缺勤时长 | +| record_counts | int[] | 每月记录数 | + +--- + +### 2.3 获取星期分布数据 + +**接口路径**:`GET /api/overview/weekday` + +**接口描述**:获取周一至周五的缺勤分布数据 + +**请求参数**:无 + +**响应示例**: + +```json +{ + "code": 200, + "message": "success", + "data": { + "weekdays": ["周一", "周二", "周三", "周四", "周五"], + "weekday_codes": [2, 3, 4, 5, 6], + "total_hours": [180, 200, 190, 210, 250], + "avg_hours": [1.2, 1.4, 1.3, 1.4, 1.7], + "record_counts": [150, 143, 146, 150, 147] + } +} +``` + +**字段说明**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| weekdays | string[] | 星期名称列表 | +| weekday_codes | int[] | 星期代码(2=周一, 6=周五) | +| total_hours | float[] | 每天缺勤总时长 | +| avg_hours | float[] | 每天平均缺勤时长 | +| record_counts | int[] | 每天记录数 | + +--- + +### 2.4 获取缺勤原因分布 + +**接口路径**:`GET /api/overview/reasons` + +**接口描述**:获取各类缺勤原因的分布数据 + +**请求参数**:无 + +**响应示例**: + +```json +{ + "code": 200, + "message": "success", + "data": { + "reasons": [ + { + "code": 23, + "name": "医疗咨询", + "count": 150, + "percentage": 20.3 + }, + { + "code": 28, + "name": "牙科咨询", + "count": 120, + "percentage": 16.2 + }, + { + "code": 27, + "name": "理疗", + "count": 100, + "percentage": 13.5 + } + // ... 更多原因 + ] + } +} +``` + +**字段说明**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| code | int | 缺勤原因代码 | +| name | string | 缺勤原因名称 | +| count | int | 出现次数 | +| percentage | float | 占比百分比 | + +**缺勤原因对照表**: + +| 代码 | 名称 | 代码 | 名称 | +|------|------|------|------| +| 0 | 未知原因 | 15 | 妊娠相关 | +| 1 | 传染病 | 16 | 围产期疾病 | +| 2 | 肿瘤 | 17 | 先天性畸形 | +| 3 | 血液疾病 | 18 | 症状体征 | +| 4 | 内分泌疾病 | 19 | 损伤中毒 | +| 5 | 精神行为障碍 | 20 | 外部原因 | +| 6 | 神经系统疾病 | 21 | 健康因素 | +| 7 | 眼部疾病 | 22 | 医疗随访 | +| 8 | 耳部疾病 | 23 | 医疗咨询 | +| 9 | 循环系统疾病 | 24 | 献血 | +| 10 | 呼吸系统疾病 | 25 | 实验室检查 | +| 11 | 消化系统疾病 | 26 | 无故缺勤 | +| 12 | 皮肤疾病 | 27 | 理疗 | +| 13 | 肌肉骨骼疾病 | 28 | 牙科咨询 | +| 14 | 泌尿生殖疾病 | - | - | + +--- + +### 2.5 获取季节分布数据 + +**接口路径**:`GET /api/overview/seasons` + +**接口描述**:获取四季的缺勤分布数据 + +**请求参数**:无 + +**响应示例**: + +```json +{ + "code": 200, + "message": "success", + "data": { + "seasons": [ + { + "code": 1, + "name": "夏季", + "total_hours": 320, + "avg_hours": 1.5, + "record_count": 213, + "percentage": 27.0 + }, + { + "code": 2, + "name": "秋季", + "total_hours": 290, + "avg_hours": 1.4, + "record_count": 207, + "percentage": 28.0 + }, + { + "code": 3, + "name": "冬季", + "total_hours": 280, + "avg_hours": 1.3, + "record_count": 215, + "percentage": 29.1 + }, + { + "code": 4, + "name": "春季", + "total_hours": 294, + "avg_hours": 1.4, + "record_count": 210, + "percentage": 28.4 + } + ] + } +} +``` + +--- + +## 3. 影响因素分析模块 + +### 3.1 获取特征重要性排序 + +**接口路径**:`GET /api/analysis/importance` + +**接口描述**:获取各特征对缺勤的影响权重 + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| model | string | 否 | 模型类型(rf/xgboost),默认rf | + +**响应示例**: + +```json +{ + "code": 200, + "message": "success", + "data": { + "model_type": "random_forest", + "features": [ + { + "name": "Reason for absence", + "name_cn": "缺勤原因", + "importance": 0.35, + "rank": 1 + }, + { + "name": "Transportation expense", + "name_cn": "交通费用", + "importance": 0.12, + "rank": 2 + }, + { + "name": "Distance from Residence to Work", + "name_cn": "通勤距离", + "importance": 0.10, + "rank": 3 + }, + { + "name": "Service time", + "name_cn": "工龄", + "importance": 0.08, + "rank": 4 + }, + { + "name": "Age", + "name_cn": "年龄", + "importance": 0.07, + "rank": 5 + }, + { + "name": "Work load Average/day", + "name_cn": "日均工作负荷", + "importance": 0.06, + "rank": 6 + }, + { + "name": "Body mass index", + "name_cn": "BMI指数", + "importance": 0.05, + "rank": 7 + }, + { + "name": "Social drinker", + "name_cn": "饮酒习惯", + "importance": 0.04, + "rank": 8 + }, + { + "name": "Hit target", + "name_cn": "达标率", + "importance": 0.03, + "rank": 9 + }, + { + "name": "Son", + "name_cn": "子女数量", + "importance": 0.03, + "rank": 10 + }, + { + "name": "Pet", + "name_cn": "宠物数量", + "importance": 0.02, + "rank": 11 + }, + { + "name": "Education", + "name_cn": "学历", + "importance": 0.02, + "rank": 12 + }, + { + "name": "Social smoker", + "name_cn": "吸烟习惯", + "importance": 0.01, + "rank": 13 + } + ] + } +} +``` + +**字段说明**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| model_type | string | 模型类型 | +| features | array | 特征列表 | +| name | string | 特征英文名 | +| name_cn | string | 特征中文名 | +| importance | float | 重要性得分(0-1) | +| rank | int | 排名 | + +--- + +### 3.2 获取相关性矩阵 + +**接口路径**:`GET /api/analysis/correlation` + +**接口描述**:获取特征之间的相关系数矩阵 + +**请求参数**:无 + +**响应示例**: + +```json +{ + "code": 200, + "message": "success", + "data": { + "features": ["Age", "Service time", "Distance", "Work load", "BMI", "Absent hours"], + "matrix": [ + [1.00, 0.67, 0.12, 0.08, 0.15, 0.05], + [0.67, 1.00, 0.10, 0.05, 0.12, 0.08], + [0.12, 0.10, 1.00, 0.03, 0.05, 0.18], + [0.08, 0.05, 0.03, 1.00, 0.02, 0.10], + [0.15, 0.12, 0.05, 0.02, 1.00, 0.06], + [0.05, 0.08, 0.18, 0.10, 0.06, 1.00] + ] + } +} +``` + +**字段说明**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| features | string[] | 特征名称列表 | +| matrix | float[][] | 相关系数矩阵(n×n) | + +--- + +### 3.3 群体对比分析 + +**接口路径**:`GET /api/analysis/compare` + +**接口描述**:按指定维度分组对比缺勤时长 + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| dimension | string | 是 | 对比维度(drinker/smoker/education/children/pet) | + +**响应示例**(dimension=drinker): + +```json +{ + "code": 200, + "message": "success", + "data": { + "dimension": "drinker", + "dimension_name": "饮酒习惯", + "groups": [ + { + "name": "不饮酒", + "value": 0, + "avg_hours": 1.2, + "count": 400, + "percentage": 54.1 + }, + { + "name": "饮酒", + "value": 1, + "avg_hours": 2.1, + "count": 340, + "percentage": 45.9 + } + ], + "difference": { + "value": 0.9, + "percentage": 75.0 + } + } +} +``` + +**dimension参数说明**: + +| 值 | 说明 | 分组 | +|------|------|------| +| drinker | 饮酒习惯 | 不饮酒(0) / 饮酒(1) | +| smoker | 吸烟习惯 | 不吸烟(0) / 吸烟(1) | +| education | 学历 | 高中(1) / 本科(2) / 研究生及以上(3-4) | +| children | 子女 | 无子女(0) / 有子女(≥1) | +| pet | 宠物 | 无宠物(0) / 有宠物(≥1) | + +--- + +## 4. 预测模块 + +### 4.1 单次缺勤预测 + +**接口路径**:`POST /api/predict/single` + +**接口描述**:根据输入的员工属性预测缺勤时长 + +**请求头**: + +``` +Content-Type: application/json +``` + +**请求参数**: + +```json +{ + "reason_for_absence": 23, + "month_of_absence": 7, + "day_of_week": 3, + "seasons": 1, + "transportation_expense": 289, + "distance": 36, + "service_time": 13, + "age": 33, + "work_load": 239.55, + "hit_target": 97, + "disciplinary_failure": 0, + "education": 1, + "son": 2, + "social_drinker": 1, + "social_smoker": 0, + "pet": 1, + "bmi": 30 +} +``` + +**参数说明**: + +| 参数名 | 类型 | 取值范围 | 说明 | +|--------|------|----------|------| +| reason_for_absence | int | 0-28 | 缺勤原因代码 | +| month_of_absence | int | 1-12 | 缺勤月份 | +| day_of_week | int | 2-6 | 星期(2=周一, 6=周五) | +| seasons | int | 1-4 | 季节(1=夏, 4=春) | +| transportation_expense | int | 100-400 | 交通费用 | +| distance | int | 1-60 | 通勤距离(公里) | +| service_time | int | 1-30 | 工龄(年) | +| age | int | 18-60 | 年龄 | +| work_load | float | 200-350 | 日均工作负荷 | +| hit_target | int | 80-100 | 达标率(%) | +| disciplinary_failure | int | 0-1 | 是否违纪(0=否, 1=是) | +| education | int | 1-4 | 学历(1=高中, 4=博士) | +| son | int | 0-5 | 子女数量 | +| social_drinker | int | 0-1 | 是否饮酒 | +| social_smoker | int | 0-1 | 是否吸烟 | +| pet | int | 0-10 | 宠物数量 | +| bmi | float | 18-40 | BMI指数 | + +**响应示例**: + +```json +{ + "code": 200, + "message": "success", + "data": { + "predicted_hours": 5.2, + "risk_level": "medium", + "risk_label": "中风险", + "confidence": 0.85, + "model_used": "random_forest" + } +} +``` + +**响应字段说明**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| predicted_hours | float | 预测缺勤时长(小时) | +| risk_level | string | 风险等级(low/medium/high) | +| risk_label | string | 风险等级中文标签 | +| confidence | float | 模型置信度(0-1) | +| model_used | string | 使用的模型 | + +**风险等级判定**: + +| 预测时长 | risk_level | risk_label | 颜色 | +|----------|------------|------------|------| +| < 4小时 | low | 低风险 | 绿色 | +| 4-8小时 | medium | 中风险 | 黄色 | +| > 8小时 | high | 高风险 | 红色 | + +--- + +### 4.2 获取模型性能信息 + +**接口路径**:`GET /api/predict/model-info` + +**接口描述**:获取当前预测模型的性能指标 + +**请求参数**:无 + +**响应示例**: + +```json +{ + "code": 200, + "message": "success", + "data": { + "models": [ + { + "name": "random_forest", + "name_cn": "随机森林", + "metrics": { + "r2": 0.82, + "mse": 15.5, + "rmse": 3.94, + "mae": 2.8 + }, + "is_active": true + }, + { + "name": "xgboost", + "name_cn": "XGBoost", + "metrics": { + "r2": 0.85, + "mse": 12.8, + "rmse": 3.58, + "mae": 2.5 + }, + "is_active": false + } + ], + "training_info": { + "train_samples": 592, + "test_samples": 148, + "feature_count": 17, + "training_date": "2026-03-01" + } + } +} +``` + +**字段说明**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| r2 | float | 决定系数(越接近1越好) | +| mse | float | 均方误差(越小越好) | +| rmse | float | 均方根误差(越小越好) | +| mae | float | 平均绝对误差(越小越好) | + +--- + +## 5. 聚类模块 + +### 5.1 获取聚类结果 + +**接口路径**:`GET /api/cluster/result` + +**接口描述**:获取K-Means聚类分析结果 + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| n_clusters | int | 否 | 聚类数量,默认3 | + +**响应示例**: + +```json +{ + "code": 200, + "message": "success", + "data": { + "n_clusters": 3, + "clusters": [ + { + "id": 0, + "name": "模范型员工", + "member_count": 120, + "percentage": 33.3, + "center": { + "age": 42, + "service_time": 18, + "work_load": 240, + "bmi": 25, + "absent_tendency": 0.8 + }, + "description": "工龄长、工作稳定、缺勤率低" + }, + { + "id": 1, + "name": "压力型员工", + "member_count": 100, + "percentage": 27.8, + "center": { + "age": 28, + "service_time": 5, + "work_load": 280, + "bmi": 23, + "absent_tendency": 2.5 + }, + "description": "年轻、工龄短、工作负荷大、缺勤较多" + }, + { + "id": 2, + "name": "生活习惯型员工", + "member_count": 140, + "percentage": 38.9, + "center": { + "age": 35, + "service_time": 10, + "work_load": 250, + "bmi": 30, + "absent_tendency": 1.5 + }, + "description": "BMI偏高、有饮酒习惯、中等缺勤率" + } + ] + } +} +``` + +--- + +### 5.2 获取员工画像数据 + +**接口路径**:`GET /api/cluster/profile` + +**接口描述**:获取用于绘制雷达图的员工画像数据 + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| n_clusters | int | 否 | 聚类数量,默认3 | + +**响应示例**: + +```json +{ + "code": 200, + "message": "success", + "data": { + "dimensions": ["年龄", "工龄", "工作负荷", "BMI", "缺勤倾向"], + "dimension_keys": ["age", "service_time", "work_load", "bmi", "absent_tendency"], + "clusters": [ + { + "id": 0, + "name": "模范型", + "values": [0.75, 0.90, 0.60, 0.55, 0.20] + }, + { + "id": 1, + "name": "压力型", + "values": [0.35, 0.20, 0.85, 0.45, 0.70] + }, + { + "id": 2, + "name": "生活习惯型", + "values": [0.55, 0.50, 0.65, 0.80, 0.45] + } + ] + } +} +``` + +**字段说明**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| dimensions | string[] | 雷达图维度名称(中文) | +| dimension_keys | string[] | 维度对应的英文键名 | +| clusters | array | 各聚类的画像数据 | +| values | float[] | 归一化后的特征值(0-1) | + +--- + +### 5.3 获取聚类散点图数据 + +**接口路径**:`GET /api/cluster/scatter` + +**接口描述**:获取用于绘制散点图的聚类分布数据 + +**请求参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| n_clusters | int | 否 | 聚类数量,默认3 | +| x_axis | string | 否 | X轴维度,默认age | +| y_axis | string | 否 | Y轴维度,默认absent_hours | + +**响应示例**: + +```json +{ + "code": 200, + "message": "success", + "data": { + "x_axis": "age", + "x_axis_name": "年龄", + "y_axis": "absent_hours", + "y_axis_name": "缺勤时长", + "points": [ + { + "employee_id": 11, + "x": 33, + "y": 4, + "cluster_id": 2 + }, + { + "employee_id": 36, + "x": 50, + "y": 0, + "cluster_id": 0 + } + // ... 更多数据点 + ], + "cluster_colors": { + "0": "#67C23A", + "1": "#E6A23C", + "2": "#F56C6C" + } + } +} +``` + +--- + +## 6. 错误码定义 + +| 错误码 | 说明 | 解决方案 | +|--------|------|----------| +| 1001 | 数据文件不存在 | 检查数据文件路径 | +| 1002 | 数据文件格式错误 | 检查CSV文件格式 | +| 2001 | 模型文件不存在 | 先训练模型 | +| 2002 | 模型加载失败 | 重新训练并保存模型 | +| 3001 | 参数缺失 | 检查必填参数 | +| 3002 | 参数值超出范围 | 检查参数取值范围 | +| 4001 | 聚类数量无效 | n_clusters应在2-10之间 | + +--- + +## 7. 附录 + +### 7.1 接口清单汇总 + +| 模块 | 接口 | 方法 | 说明 | +|------|------|------|------| +| 数据概览 | /api/overview/stats | GET | 基础统计指标 | +| 数据概览 | /api/overview/trend | GET | 月度趋势 | +| 数据概览 | /api/overview/weekday | GET | 星期分布 | +| 数据概览 | /api/overview/reasons | GET | 原因分布 | +| 数据概览 | /api/overview/seasons | GET | 季节分布 | +| 因素分析 | /api/analysis/importance | GET | 特征重要性 | +| 因素分析 | /api/analysis/correlation | GET | 相关性矩阵 | +| 因素分析 | /api/analysis/compare | GET | 群体对比 | +| 预测 | /api/predict/single | POST | 单次预测 | +| 预测 | /api/predict/model-info | GET | 模型信息 | +| 聚类 | /api/cluster/result | GET | 聚类结果 | +| 聚类 | /api/cluster/profile | GET | 员工画像 | +| 聚类 | /api/cluster/scatter | GET | 散点图数据 | + +### 7.2 文档修改历史 + +| 版本 | 日期 | 修改人 | 修改内容 | +|------|------|--------|----------| +| V1.0 | 2026-03 | 张硕 | 初始版本 | + +--- + +**文档结束** diff --git a/docs/03_数据设计文档.md b/docs/03_数据设计文档.md new file mode 100644 index 0000000..340cdae --- /dev/null +++ b/docs/03_数据设计文档.md @@ -0,0 +1,426 @@ +# 数据设计文档 + +## 基于多维特征挖掘的员工缺勤分析与预测系统 + +**文档版本**:V1.0 +**编写日期**:2026年3月 +**编写人**:张硕 + +--- + +## 1. 数据集概述 + +### 1.1 数据来源 + +| 项目 | 内容 | +|------|------| +| 数据集名称 | Absenteeism at work | +| 数据来源 | UCI Machine Learning Repository | +| 原始提供方 | 巴西某快递公司 (2007-2010年) | +| 数据提供者 | Andrea Martiniano, Ricardo Pinto Ferreira, Renato Jose Sassi | +| 所属机构 | Universidade Nove de Julho, Brazil | + +### 1.2 数据规模 + +| 项目 | 数值 | +|------|------| +| 记录总数 | 740条 | +| 特征数量 | 21个字段 | +| 员工数量 | 36人 | +| 时间跨度 | 2007年7月 - 2010年7月 | + +### 1.3 数据质量 + +| 检查项 | 结果 | 说明 | +|--------|------|------| +| 缺失值 | 无 | 数据完整无缺失 | +| 重复记录 | 无 | 无重复数据 | +| 异常值 | 需检查 | 部分字段可能存在异常值 | +| 数据一致性 | 良好 | 字段格式一致 | + +--- + +## 2. 字段说明 + +### 2.1 字段完整列表 + +| 序号 | 字段名 | 中文名称 | 数据类型 | 取值范围 | 说明 | +|------|--------|----------|----------|----------|------| +| 1 | ID | 员工标识 | int | 1-36 | 唯一标识员工 | +| 2 | Reason for absence | 缺勤原因 | int | 0-28 | ICD代码或非疾病原因 | +| 3 | Month of absence | 缺勤月份 | int | 1-12 | 月份 | +| 4 | Day of the week | 星期几 | int | 2-6 | 2=周一, 6=周五 | +| 5 | Seasons | 季节 | int | 1-4 | 1=夏, 4=春 | +| 6 | Transportation expense | 交通费用 | int | 118-388 | 月交通费用(雷亚尔) | +| 7 | Distance from Residence to Work | 通勤距离 | int | 5-52 | 公里数 | +| 8 | Service time | 工龄 | int | 1-29 | 年数 | +| 9 | Age | 年龄 | int | 27-58 | 周岁 | +| 10 | Work load Average/day | 日均工作负荷 | float | 205-350 | 目标达成量/天 | +| 11 | Hit target | 达标率 | int | 81-100 | 百分比 | +| 12 | Disciplinary failure | 违纪记录 | int | 0-1 | 0=否, 1=是 | +| 13 | Education | 学历 | int | 1-4 | 1=高中, 4=博士 | +| 14 | Son | 子女数量 | int | 0-4 | 子女人数 | +| 15 | Social drinker | 饮酒习惯 | int | 0-1 | 0=否, 1=是 | +| 16 | Social smoker | 吸烟习惯 | int | 0-1 | 0=否, 1=是 | +| 17 | Pet | 宠物数量 | int | 0-8 | 宠物数量 | +| 18 | Weight | 体重 | int | 56-108 | 公斤 | +| 19 | Height | 身高 | int | 163-196 | 厘米 | +| 20 | Body mass index | BMI指数 | float | 19-38 | 体重/身高² | +| 21 | Absenteeism time in hours | 缺勤时长 | int | 0-120 | 目标变量(小时) | + +### 2.2 特征分类 + +#### 2.2.1 类别型特征 + +| 字段名 | 类别数 | 类别说明 | +|--------|--------|----------| +| Reason for absence | 29 | 0-28,ICD疾病代码或非疾病原因 | +| Month of absence | 12 | 1-12月 | +| Day of the week | 5 | 周一至周五 | +| Seasons | 4 | 夏秋冬春 | +| Disciplinary failure | 2 | 是/否 | +| Education | 4 | 高中/本科/研究生/博士 | +| Social drinker | 2 | 是/否 | +| Social smoker | 2 | 是/否 | + +#### 2.2.2 数值型特征 + +| 字段名 | 类型 | 范围 | 均值 | 标准差 | +|--------|------|------|------|--------| +| Transportation expense | 连续 | 118-388 | 221.3 | 69.1 | +| Distance from Residence to Work | 连续 | 5-52 | 29.6 | 14.8 | +| Service time | 连续 | 1-29 | 12.0 | 5.7 | +| Age | 连续 | 27-58 | 36.9 | 6.5 | +| Work load Average/day | 连续 | 205-350 | 270.7 | 37.1 | +| Hit target | 连续 | 81-100 | 94.6 | 4.0 | +| Son | 离散 | 0-4 | 1.0 | 1.1 | +| Pet | 离散 | 0-8 | 0.8 | 1.5 | +| Weight | 连续 | 56-108 | 79.0 | 12.4 | +| Height | 连续 | 163-196 | 172.9 | 6.0 | +| Body mass index | 连续 | 19-38 | 26.7 | 4.3 | +| Absenteeism time in hours | 连续 | 0-120 | 6.9 | 13.3 | + +### 2.3 缺勤原因详细说明 + +#### 2.3.1 ICD疾病分类(代码1-21) + +| 代码 | ICD分类 | 疾病类型 | +|------|---------|----------| +| 1 | I | 传染病和寄生虫病 | +| 2 | II | 肿瘤 | +| 3 | III | 血液及造血器官疾病 | +| 4 | IV | 内分泌、营养和代谢疾病 | +| 5 | V | 精神和行为障碍 | +| 6 | VI | 神经系统疾病 | +| 7 | VII | 眼及其附属器疾病 | +| 8 | VIII | 耳及乳突疾病 | +| 9 | IX | 循环系统疾病 | +| 10 | X | 呼吸系统疾病 | +| 11 | XI | 消化系统疾病 | +| 12 | XII | 皮肤和皮下组织疾病 | +| 13 | XIII | 肌肉骨骼系统和结缔组织疾病 | +| 14 | XIV | 泌尿生殖系统疾病 | +| 15 | XV | 妊娠、分娩和产褥期 | +| 16 | XVI | 围产期疾病 | +| 17 | XVII | 先天性畸形 | +| 18 | XVIII | 症状、体征异常发现 | +| 19 | XIX | 损伤、中毒 | +| 20 | XX | 外部原因导致的发病和死亡 | +| 21 | XXI | 影响健康状态的因素 | + +#### 2.3.2 非疾病原因(代码22-28) + +| 代码 | 名称 | 说明 | +|------|------|------| +| 22 | 医疗随访 | 患者定期随访复查 | +| 23 | 医疗咨询 | 门诊就医咨询 | +| 24 | 献血 | 无偿献血活动 | +| 25 | 实验室检查 | 医学检验检查 | +| 26 | 无故缺勤 | 未经批准的缺勤 | +| 27 | 理疗 | 物理治疗康复 | +| 28 | 牙科咨询 | 口腔科就诊 | + +#### 2.3.3 特殊值 + +| 代码 | 说明 | +|------|------| +| 0 | 未知原因(数据中存在) | + +### 2.4 季节编码说明 + +| 代码 | 季节 | 月份范围(巴西) | +|------|------|------------------| +| 1 | 夏季 | 12月-2月 | +| 2 | 秋季 | 3月-5月 | +| 3 | 冬季 | 6月-8月 | +| 4 | 春季 | 9月-11月 | + +### 2.5 学历编码说明 + +| 代码 | 学历 | 说明 | +|------|------|------| +| 1 | 高中 | 高中及以下学历 | +| 2 | 本科 | 大学本科学历 | +| 3 | 研究生 | 硕士研究生 | +| 4 | 博士 | 博士研究生 | + +--- + +## 3. 数据预处理 + +### 3.1 数据清洗 + +#### 3.1.1 缺失值处理 + +数据集本身无缺失值,但在预处理过程中需确保: + +``` +检查步骤: +1. 统计每个字段的缺失值数量 +2. 如发现缺失值,数值型用中位数填充,类别型用众数填充 +``` + +#### 3.1.2 异常值处理 + +| 字段 | 异常值判定标准 | 处理方式 | +|------|----------------|----------| +| Absenteeism time in hours | > 24小时(超过一天) | 保留,但做标记 | +| Work load Average/day | < 100 或 > 500 | 检查后决定保留或剔除 | +| Age | < 18 或 > 65 | 检查数据有效性 | + +#### 3.1.3 数据类型转换 + +| 字段 | 原始类型 | 转换后类型 | 说明 | +|------|----------|------------|------| +| ID | int | int | 保持不变 | +| Reason for absence | int | category | 转为类别型 | +| Month of absence | int | category | 转为类别型 | +| Day of the week | int | category | 转为类别型 | +| Seasons | int | category | 转为类别型 | +| Education | int | category | 转为类别型 | +| Disciplinary failure | int | category | 转为类别型 | +| Social drinker | int | category | 转为类别型 | +| Social smoker | int | category | 转为类别型 | + +### 3.2 特征编码 + +#### 3.2.1 独热编码 (One-Hot Encoding) + +对以下类别型特征进行独热编码: + +| 字段 | 编码后特征数 | 说明 | +|------|--------------|------| +| Reason for absence | 29 | 每个原因一个二进制特征 | +| Month of absence | 12 | 每个月份一个二进制特征 | +| Day of the week | 5 | 每个星期一个二进制特征 | +| Seasons | 4 | 每个季节一个二进制特征 | +| Education | 4 | 每个学历一个二进制特征 | +| Disciplinary failure | 2 | 是/否两个特征 | +| Social drinker | 2 | 是/否两个特征 | +| Social smoker | 2 | 是/否两个特征 | + +**编码示例**: + +``` +原始数据:Reason for absence = 23 + +编码后: + Reason_0: 0 + Reason_1: 0 + ... + Reason_23: 1 + ... + Reason_28: 0 +``` + +#### 3.2.2 标准化处理 (StandardScaler) + +对以下数值型特征进行标准化处理(均值为0,标准差为1): + +| 字段 | 标准化公式 | +|------|------------| +| Transportation expense | (x - μ) / σ | +| Distance from Residence to Work | (x - μ) / σ | +| Service time | (x - μ) / σ | +| Age | (x - μ) / σ | +| Work load Average/day | (x - μ) / σ | +| Hit target | (x - μ) / σ | +| Son | (x - μ) / σ | +| Pet | (x - μ) / σ | +| Weight | (x - μ) / σ | +| Height | (x - μ) / σ | +| Body mass index | (x - μ) / σ | + +### 3.3 特征工程 + +#### 3.3.1 派生特征 + +可考虑创建以下派生特征: + +| 派生特征 | 计算方式 | 说明 | +|----------|----------|------| +| has_children | Son > 0 | 是否有子女(二分类) | +| has_pet | Pet > 0 | 是否有宠物(二分类) | +| age_group | 年龄分组 | 青年/中年/老年 | +| service_category | 工龄分组 | 新员工/老员工 | +| bmi_category | BMI分组 | 正常/超重/肥胖 | +| workload_level | 负荷等级 | 低/中/高 | + +#### 3.3.2 特征选择 + +基于特征重要性分析,选择对预测最有价值的特征: + +| 优先级 | 特征 | 选择依据 | +|--------|------|----------| +| 高 | Reason for absence | 业务含义明确,影响直接 | +| 高 | Transportation expense | 特征重要性高 | +| 高 | Distance from Residence to Work | 特征重要性高 | +| 高 | Service time | 特征重要性高 | +| 高 | Age | 特征重要性高 | +| 中 | Work load Average/day | 有一定影响 | +| 中 | Body mass index | 有一定影响 | +| 中 | Social drinker | 群体差异明显 | +| 低 | Pet | 影响较小 | +| 低 | Height | 信息可由BMI代替 | + +### 3.4 数据划分 + +#### 3.4.1 训练集/测试集划分 + +| 数据集 | 比例 | 记录数 | 用途 | +|--------|------|--------|------| +| 训练集 | 80% | 592条 | 模型训练 | +| 测试集 | 20% | 148条 | 模型评估 | + +#### 3.4.2 划分方式 + +- 使用分层抽样,确保各缺勤原因在训练集和测试集中比例一致 +- 随机种子固定(random_state=42),保证结果可复现 + +--- + +## 4. 数据存储方案 + +### 4.1 目录结构 + +``` +backend/data/ +├── raw/ # 原始数据 +│ └── Absenteeism_at_work.csv # UCI原始数据集 +│ +├── processed/ # 处理后数据 +│ ├── clean_data.csv # 清洗后的数据 +│ ├── encoded_data.csv # 编码后的数据 +│ ├── train_data.csv # 训练数据 +│ └── test_data.csv # 测试数据 +│ +└── analysis/ # 分析结果数据 + ├── statistics.json # 统计结果 + ├── correlation.json # 相关性矩阵 + └── feature_importance.json # 特征重要性 +``` + +### 4.2 模型存储 + +``` +backend/models/ +├── rf_model.pkl # 随机森林模型 +├── xgb_model.pkl # XGBoost模型 +├── kmeans_model.pkl # K-Means模型 +├── scaler.pkl # StandardScaler对象 +├── encoder.pkl # OneHotEncoder对象 +└── model_info.json # 模型元信息 +``` + +### 4.3 数据文件格式 + +#### 4.3.1 CSV文件格式 + +``` +分隔符:分号 (;) +编码:UTF-8 +表头:第一行为字段名 +``` + +#### 4.3.2 JSON文件格式 + +```json +{ + "created_at": "2026-03-01T10:00:00", + "version": "1.0", + "data": { + // 具体数据内容 + } +} +``` + +--- + +## 5. 数据字典 + +### 5.1 原始数据字典 + +| 字段名 | 数据类型 | 是否为空 | 默认值 | 说明 | +|--------|----------|----------|--------|------| +| ID | INTEGER | NOT NULL | - | 员工唯一标识 | +| Reason for absence | INTEGER | NOT NULL | - | 缺勤原因代码 | +| Month of absence | INTEGER | NOT NULL | - | 月份(1-12) | +| Day of the week | INTEGER | NOT NULL | - | 星期(2-6) | +| Seasons | INTEGER | NOT NULL | - | 季节(1-4) | +| Transportation expense | INTEGER | NOT NULL | - | 交通费用 | +| Distance from Residence to Work | INTEGER | NOT NULL | - | 通勤距离(km) | +| Service time | INTEGER | NOT NULL | - | 工龄(年) | +| Age | INTEGER | NOT NULL | - | 年龄 | +| Work load Average/day | REAL | NOT NULL | - | 日均工作负荷 | +| Hit target | INTEGER | NOT NULL | - | 达标率(%) | +| Disciplinary failure | INTEGER | NOT NULL | 0 | 违纪记录(0/1) | +| Education | INTEGER | NOT NULL | - | 学历(1-4) | +| Son | INTEGER | NOT NULL | 0 | 子女数量 | +| Social drinker | INTEGER | NOT NULL | 0 | 饮酒习惯(0/1) | +| Social smoker | INTEGER | NOT NULL | 0 | 吸烟习惯(0/1) | +| Pet | INTEGER | NOT NULL | 0 | 宠物数量 | +| Weight | INTEGER | NOT NULL | - | 体重(kg) | +| Height | INTEGER | NOT NULL | - | 身高(cm) | +| Body mass index | REAL | NOT NULL | - | BMI指数 | +| Absenteeism time in hours | INTEGER | NOT NULL | - | 缺勤时长(目标变量) | + +--- + +## 6. 附录 + +### 6.1 数据统计摘要 + +``` +数据集基本信息: +- 记录数:740 +- 特征数:21 +- 员工数:36 +- 缺勤总时长:5028小时 +- 平均缺勤时长:6.9小时 + +缺勤原因TOP5: +1. 医疗咨询(23):149次 (20.1%) +2. 牙科咨询(28):112次 (15.1%) +3. 理疗(27):94次 (12.7%) +4. 疾病咨询(22):74次 (10.0%) +5. 消化系统疾病(11):59次 (8.0%) + +学历分布: +- 高中:633人 (85.5%) +- 本科:79人 (10.7%) +- 研究生及以上:28人 (3.8%) + +生活习惯: +- 饮酒者:340人 (45.9%) +- 吸烟者:90人 (12.2%) +``` + +### 6.2 文档修改历史 + +| 版本 | 日期 | 修改人 | 修改内容 | +|------|------|--------|----------| +| V1.0 | 2026-03 | 张硕 | 初始版本 | + +--- + +**文档结束** diff --git a/docs/04_UI原型设计.md b/docs/04_UI原型设计.md new file mode 100644 index 0000000..db74629 --- /dev/null +++ b/docs/04_UI原型设计.md @@ -0,0 +1,787 @@ +# UI原型设计文档 + +## 基于多维特征挖掘的员工缺勤分析与预测系统 + +**文档版本**:V1.0 +**编写日期**:2026年3月 +**编写人**:张硕 + +--- + +## 1. 设计原则 + +### 1.1 视觉风格 + +| 设计要素 | 设计规范 | +|----------|----------| +| 主色调 | Element Plus默认蓝色 (#409EFF) | +| 辅助色 | 成功绿(#67C23A)、警告黄(#E6A23C)、危险红(#F56C6C) | +| 背景色 | 浅灰色 (#F5F7FA) | +| 字体 | 系统默认字体(中文:微软雅黑/PingFang SC) | +| 字号 | 标题16px、正文14px、辅助文字12px | +| 圆角 | 4px | +| 阴影 | 轻微阴影增加层次感 | + +### 1.2 交互原则 + +| 原则 | 说明 | +|------|------| +| 一致性 | 相同功能使用相同的交互方式 | +| 反馈性 | 操作后给予明确的视觉反馈 | +| 容错性 | 提供撤销操作和错误提示 | +| 易学性 | 界面简洁直观,降低学习成本 | +| 高效性 | 减少操作步骤,提高工作效率 | + +### 1.3 响应式设计 + +| 屏幕尺寸 | 适配方案 | +|----------|----------| +| ≥1920px | 大屏显示,图表放大 | +| 1366-1920px | 标准显示,默认布局 | +| <1366px | 紧凑布局,图表自适应 | + +--- + +## 2. 整体布局 + +### 2.1 页面框架 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Header (顶部导航栏) │ +│ ┌───────────────────────────────────────────────────────────┐ │ +│ │ Logo │ 数据概览 │ 影响因素 │ 缺勤预测 │ 员工画像 │ │ +│ └───────────────────────────────────────────────────────────┘ │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ │ +│ Main Content │ +│ (主内容区域) │ +│ │ +│ │ +├─────────────────────────────────────────────────────────────────┤ +│ Footer (底部信息栏 - 可选) │ +│ ┌───────────────────────────────────────────────────────────┐ │ +│ │ © 2026 基于多维特征挖掘的员工缺勤分析与预测系统 │ │ +│ └───────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 2.2 导航设计 + +**顶部导航菜单**: + +| 菜单项 | 图标 | 路由 | 说明 | +|--------|------|------|------| +| 数据概览 | 📊 | /dashboard | 首页,展示整体统计 | +| 影响因素 | 🔍 | /analysis | 特征重要性分析 | +| 缺勤预测 | 🎯 | /prediction | 预测功能入口 | +| 员工画像 | 👥 | /clustering | 聚类分析结果 | + +--- + +## 3. 页面一:数据概览 (Dashboard) + +### 3.1 页面布局 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 数据概览 │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │ KPI卡片1 │ │ KPI卡片2 │ │ KPI卡片3 │ │ KPI卡片4 │ │ +│ │ 总记录数 │ │ 员工总数 │ │平均缺勤 │ │高风险占比│ │ +│ │ 740 │ │ 36 │ │ 6.9h │ │ 15% │ │ +│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ +│ │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────┐ ┌─────────────────────────┐ │ +│ │ │ │ │ │ +│ │ 月度缺勤趋势折线图 │ │ 星期分布柱状图 │ │ +│ │ │ │ │ │ +│ │ (ECharts Line Chart) │ │ (ECharts Bar Chart) │ │ +│ │ │ │ │ │ +│ └─────────────────────────┘ └─────────────────────────┘ │ +│ │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────┐ ┌─────────────────────────┐ │ +│ │ │ │ │ │ +│ │ 缺勤原因分布饼图 │ │ 季节分布饼图 │ │ +│ │ │ │ │ │ +│ │ (ECharts Pie Chart) │ │ (ECharts Pie Chart) │ │ +│ │ │ │ │ │ +│ └─────────────────────────┘ └─────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 3.2 组件说明 + +#### 3.2.1 KPI卡片组件 + +``` +┌─────────────────────────────────────┐ +│ ┌───────┐ │ +│ │ 图标 │ 总记录数 │ +│ │ 📊 │ │ +│ └───────┘ │ +│ │ +│ 740 │ +│ 条 │ +│ │ +│ 较上月 ↑ 5% │ +└─────────────────────────────────────┘ +``` + +**组件属性**: + +| 属性 | 类型 | 说明 | +|------|------|------| +| title | string | 指标名称 | +| value | number/string | 指标值 | +| unit | string | 单位 | +| icon | string | 图标 | +| trend | string | 趋势(可选) | +| trendType | string | 趋势类型(up/down) | + +#### 3.2.2 月度趋势折线图 + +**ECharts配置要点**: + +```javascript +{ + title: { text: '月度缺勤趋势' }, + xAxis: { + type: 'category', + data: ['1月', '2月', ..., '12月'] + }, + yAxis: { + type: 'value', + name: '缺勤时长(小时)' + }, + series: [{ + type: 'line', + smooth: true, + data: [80, 65, 90, ...] + }], + tooltip: { + trigger: 'axis' + } +} +``` + +#### 3.2.3 缺勤原因饼图 + +**ECharts配置要点**: + +```javascript +{ + title: { text: '缺勤原因分布' }, + series: [{ + type: 'pie', + radius: ['40%', '70%'], // 环形图 + data: [ + { value: 149, name: '医疗咨询' }, + { value: 112, name: '牙科咨询' }, + // ... + ] + }], + legend: { + orient: 'vertical', + right: 10 + } +} +``` + +### 3.3 交互流程 + +1. 用户进入页面,自动加载统计数据 +2. KPI卡片依次显示(可添加动画效果) +3. 图表异步加载,显示加载动画 +4. 图表支持鼠标悬停查看详情 +5. 点击图表某区域可钻取详情(可选) + +--- + +## 4. 页面二:影响因素分析 (FactorAnalysis) + +### 4.1 页面布局 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 影响因素分析 │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌───────────────────────────────────────────────────────────┐ │ +│ │ │ │ +│ │ 特征重要性排序条形图 │ │ +│ │ (水平柱状图,降序排列) │ │ +│ │ │ │ +│ │ 通勤距离 ████████████████████████ 0.35 │ │ +│ │ 交通费用 ███████████████████ 0.28 │ │ +│ │ 工龄 ██████████████ 0.21 │ │ +│ │ 年龄 ████████████ 0.18 │ │ +│ │ 工作负荷 ████████ 0.12 │ │ +│ │ ... │ │ +│ │ │ │ +│ └───────────────────────────────────────────────────────────┘ │ +│ │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────┐ ┌─────────────────────────┐ │ +│ │ │ │ │ │ +│ │ 相关性热力图 │ │ 群体对比分析 │ │ +│ │ │ │ │ │ +│ │ (Heatmap) │ │ ┌───────────────────┐ │ │ +│ │ │ │ │ 对比维度: [下拉框] │ │ │ +│ │ 显示特征间相关系数 │ │ └───────────────────┘ │ │ +│ │ │ │ │ │ +│ │ │ │ (分组柱状图) │ │ +│ └─────────────────────────┘ └─────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 4.2 组件说明 + +#### 4.2.1 特征重要性条形图 + +``` +特征重要性排序 +┌────────────────────────────────────────────────────────┐ +│ │ +│ Reason for absence ████████████████████████████ │ 0.35 +│ Transportation exp ████████████████████ │ 0.28 +│ Distance █████████████████ │ 0.24 +│ Service time ██████████████ │ 0.21 +│ Age ████████████ │ 0.18 +│ Work load ██████████ │ 0.15 +│ BMI ████████ │ 0.12 +│ Social drinker ██████ │ 0.09 +│ Hit target ████ │ 0.06 +│ Son ███ │ 0.05 +│ Pet ██ │ 0.03 +│ Education ██ │ 0.03 +│ Social smoker █ │ 0.01 +│ │ +└────────────────────────────────────────────────────────┘ +``` + +**ECharts配置要点**: + +```javascript +{ + title: { text: '特征重要性排序' }, + grid: { left: '20%' }, // 留出标签空间 + xAxis: { + type: 'value', + name: '重要性得分' + }, + yAxis: { + type: 'category', + data: ['Reason for absence', 'Transportation', ...] + }, + series: [{ + type: 'bar', + data: [0.35, 0.28, ...], + itemStyle: { + color: '#409EFF' + } + }] +} +``` + +#### 4.2.2 相关性热力图 + +``` +┌─────────────────────────────────────────────────────────┐ +│ 相关性热力图 │ +│ │ +│ Age SrvT Dist Load BMI AbsH │ +│ ┌─────────────────────────────────────┐ │ +│ Age │ 1.0 0.67 0.12 0.08 0.15 0.05 │ │ +│ │ ■■■ ■■□ □□□ □□□ □□□ □□□ │ │ +│ SrvT │ 0.67 1.0 0.10 0.05 0.12 0.08 │ │ +│ │ ■■□ ■■■ □□□ □□□ □□□ □□□ │ │ +│ Dist │ 0.12 0.10 1.0 0.03 0.05 0.18 │ │ +│ │ □□□ □□□ ■■■ □□□ □□□ □□□ │ │ +│ ... │ ... │ │ +│ └─────────────────────────────────────┘ │ +│ │ +│ 图例: -1 (蓝色) ←→ 0 (白色) ←→ +1 (红色) │ +└─────────────────────────────────────────────────────────┘ +``` + +**ECharts配置要点**: + +```javascript +{ + title: { text: '相关性热力图' }, + tooltip: { + formatter: function(params) { + return `${params.name}: ${params.value[2].toFixed(2)}`; + } + }, + xAxis: { type: 'category', data: featureNames }, + yAxis: { type: 'category', data: featureNames }, + visualMap: { + min: -1, + max: 1, + calculable: true, + inRange: { + color: ['#313695', '#ffffff', '#a50026'] + } + }, + series: [{ + type: 'heatmap', + data: correlationData + }] +} +``` + +#### 4.2.3 群体对比选择器 + +``` +┌───────────────────────────────────────────────────────────┐ +│ 群体对比分析 │ +│ │ +│ 选择对比维度: [ 饮酒习惯 ▼ ] │ +│ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ │ │ +│ │ 平均缺勤时长(小时) │ │ +│ │ │ │ +│ │ 不饮酒 ████████████████ 1.2h │ │ +│ │ 饮酒 ██████████████████████████ 2.1h │ │ +│ │ │ │ +│ │ 差异: 饮酒者比不饮酒者高 75% │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +└───────────────────────────────────────────────────────────┘ +``` + +**对比维度选项**: + +| 选项 | 分组 | +|------|------| +| 饮酒习惯 | 饮酒 / 不饮酒 | +| 吸烟习惯 | 吸烟 / 不吸烟 | +| 学历 | 高中 / 本科 / 研究生+ | +| 子女 | 有子女 / 无子女 | +| 宠物 | 有宠物 / 无宠物 | + +### 4.3 交互流程 + +1. 页面加载时自动获取特征重要性数据 +2. 渲染特征重要性条形图 +3. 并行加载相关性矩阵,渲染热力图 +4. 用户选择对比维度后,更新群体对比图 +5. 所有图表支持鼠标悬停查看详情 + +--- + +## 5. 页面三:缺勤预测 (Prediction) + +### 5.1 页面布局 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 缺勤预测 │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌───────────────────────────┐ ┌───────────────────────────┐ │ +│ │ │ │ │ │ +│ │ 参数输入表单 │ │ 预测结果展示 │ │ +│ │ │ │ │ │ +│ │ 缺勤原因: [下拉选择] │ │ ┌───────────────────┐ │ │ +│ │ 缺勤月份: [下拉选择] │ │ │ │ │ │ +│ │ 星期几: [下拉选择] │ │ │ 预测结果 │ │ │ +│ │ 季节: [下拉选择] │ │ │ │ │ │ +│ │ │ │ │ 5.2 小时 │ │ │ +│ │ 交通费用: [输入框] │ │ │ │ │ │ +│ │ 通勤距离: [输入框] │ │ │ ● 中风险 │ │ │ +│ │ 工龄: [输入框] │ │ │ │ │ │ +│ │ 年龄: [输入框] │ │ └───────────────────┘ │ │ +│ │ │ │ │ │ +│ │ 日均工作负荷: [输入框] │ │ ┌───────────────────┐ │ │ +│ │ 达标率: [输入框] │ │ │ 模型信息 │ │ │ +│ │ 违纪记录: [是/否] │ │ │ R²: 0.82 │ │ │ +│ │ 学历: [下拉选择] │ │ │ MSE: 15.5 │ │ │ +│ │ 子女数量: [输入框] │ │ │ 置信度: 85% │ │ │ +│ │ 饮酒习惯: [是/否] │ │ └───────────────────┘ │ │ +│ │ 吸烟习惯: [是/否] │ │ │ │ +│ │ 宠物数量: [输入框] │ │ │ │ +│ │ BMI指数: [输入框] │ │ │ │ +│ │ │ │ │ │ +│ │ [ 开始预测 ] │ │ │ │ +│ │ │ │ │ │ +│ └───────────────────────────┘ └───────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 5.2 组件说明 + +#### 5.2.1 参数输入表单 + +**表单字段设计**: + +| 字段 | 组件类型 | 选项/范围 | 默认值 | +|------|----------|-----------|--------| +| 缺勤原因 | el-select | 0-28 | 23 | +| 缺勤月份 | el-select | 1-12 | 当前月 | +| 星期几 | el-select | 周一-周五 | 周一 | +| 季节 | el-select | 夏秋冬春 | 当前季节 | +| 交通费用 | el-input-number | 100-400 | 200 | +| 通勤距离 | el-input-number | 1-60 | 20 | +| 工龄 | el-input-number | 1-30 | 5 | +| 年龄 | el-input-number | 18-60 | 30 | +| 日均工作负荷 | el-input-number | 200-350 | 250 | +| 达标率 | el-input-number | 80-100 | 95 | +| 违纪记录 | el-radio-group | 是/否 | 否 | +| 学历 | el-select | 高中/本科/研究生/博士 | 本科 | +| 子女数量 | el-input-number | 0-5 | 0 | +| 饮酒习惯 | el-radio-group | 是/否 | 否 | +| 吸烟习惯 | el-radio-group | 是/否 | 否 | +| 宠物数量 | el-input-number | 0-10 | 0 | +| BMI指数 | el-input-number | 18-40 | 25 | + +**表单验证规则**: + +| 字段 | 验证规则 | +|------|----------| +| 缺勤原因 | 必填 | +| 缺勤月份 | 必填,范围1-12 | +| 交通费用 | 必填,范围100-400 | +| 通勤距离 | 必填,范围1-60 | +| 年龄 | 必填,范围18-60 | +| BMI指数 | 必填,范围18-40 | + +#### 5.2.2 预测结果卡片 + +``` +┌─────────────────────────────────────┐ +│ │ +│ 预测结果 │ +│ │ +│ 5.2 │ +│ 小时 │ +│ │ +│ ┌─────────────────────────────┐ │ +│ │ ● 中风险 (黄色) │ │ +│ │ 缺勤时长: 4-8小时 │ │ +│ └─────────────────────────────┘ │ +│ │ +│ 模型置信度: 85% │ +│ 使用模型: 随机森林 │ +│ │ +└─────────────────────────────────────┘ +``` + +**风险等级展示**: + +| 等级 | 颜色 | 图标 | 说明 | +|------|------|------|------| +| 低风险 | 绿色 (#67C23A) | ✓ | 缺勤时长 < 4小时 | +| 中风险 | 黄色 (#E6A23C) | ⚠ | 缺勤时长 4-8小时 | +| 高风险 | 红色 (#F56C6C) | ✕ | 缺勤时长 > 8小时 | + +### 5.3 交互流程 + +1. 页面加载,显示空表单 +2. 用户填写表单字段 +3. 点击"开始预测"按钮 +4. 前端验证表单数据 +5. 发送请求到后端API +6. 显示加载动画 +7. 接收预测结果 +8. 渲染结果卡片(带动画效果) + +--- + +## 6. 页面四:员工画像 (Clustering) + +### 6.1 页面布局 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 员工画像 │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ 聚类数量: [ 3 ▼ ] [ 重新聚类 ] │ +│ │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌───────────────────────────────────────────────────────────┐ │ +│ │ │ │ +│ │ 员工群体雷达图 │ │ +│ │ │ │ +│ │ 年龄 │ │ +│ │ ▲ │ │ +│ │ /│\ │ │ +│ │ / │ \ │ │ +│ │ 工龄 ◄──────┼──────► 工作负荷 │ │ +│ │ \ │ / │ │ +│ │ \ │ / │ │ +│ │ \ │ / │ │ +│ │ 缺勤倾向 ▼ BMI │ │ +│ │ │ │ +│ │ 图例: ─── 模范型 ─── 压力型 ─── 生活习惯型 │ │ +│ │ │ │ +│ └───────────────────────────────────────────────────────────┘ │ +│ │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────┐ ┌─────────────────────────┐ │ +│ │ │ │ │ │ +│ │ 聚类结果统计 │ │ 聚类散点图 │ │ +│ │ │ │ │ │ +│ │ 模范型: 120人 (33%) │ │ ● │ │ +│ │ 压力型: 100人 (28%) │ │ ● ● ○ │ │ +│ │ 生活习惯型: 140人(39%)│ │ ● ○ ● │ │ +│ │ │ │ ○ ● │ │ +│ │ 点击查看详细建议... │ │ │ │ +│ │ │ │ ● 模范型 ○ 压力型 │ │ +│ └─────────────────────────┘ └─────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 6.2 组件说明 + +#### 6.2.1 员工群体雷达图 + +``` + 年龄 + ▲ + /|\ + / | \ + / | \ + / | \ + / | \ + 工龄 ◄───────┼───────► 工作负荷 + \ | / + \ | / + \ | / + \ | / + \|/ + ▼ + 缺勤倾向 BMI + +各聚类特征(归一化): +───────────────────────────────────────── +模范型 (绿色): 0.75 0.90 0.60 0.55 0.20 +压力型 (橙色): 0.35 0.20 0.85 0.45 0.70 +生活习惯型 (红色): 0.55 0.50 0.65 0.80 0.45 +``` + +**ECharts配置要点**: + +```javascript +{ + title: { text: '员工群体画像' }, + legend: { data: ['模范型', '压力型', '生活习惯型'] }, + radar: { + indicator: [ + { name: '年龄', max: 1 }, + { name: '工龄', max: 1 }, + { name: '工作负荷', max: 1 }, + { name: 'BMI', max: 1 }, + { name: '缺勤倾向', max: 1 } + ] + }, + series: [{ + type: 'radar', + data: [ + { value: [0.75, 0.90, 0.60, 0.55, 0.20], name: '模范型' }, + { value: [0.35, 0.20, 0.85, 0.45, 0.70], name: '压力型' }, + { value: [0.55, 0.50, 0.65, 0.80, 0.45], name: '生活习惯型' } + ] + }] +} +``` + +#### 6.2.2 聚类结果统计 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 聚类结果统计 │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 模范型员工 120人 (33.3%) │ │ +│ │ ████████████████████████████████ │ │ +│ │ 特点: 工龄长、工作稳定、缺勤率低 │ │ +│ │ 建议: 保持现有管理方式,可作为榜样员工 │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 压力型员工 100人 (27.8%) │ │ +│ │ ████████████████████████ │ │ +│ │ 特点: 年轻、工龄短、工作负荷大、缺勤较多 │ │ +│ │ 建议: 关注工作压力,适当减少加班 │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 生活习惯型员工 140人 (38.9%) │ │ +│ │ ████████████████████████████████████ │ │ +│ │ 特点: BMI偏高、有饮酒习惯、中等缺勤率 │ │ +│ │ 建议: 关注员工健康,组织体检和健康活动 │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +#### 6.2.3 聚类散点图 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 聚类散点图 │ +│ │ +│ 缺勤 │ +│ 时长 │ +│ ▲ │ +│ 40 │ ○ │ +│ │ ○ ○ │ +│ 30 │ ○ ○ │ +│ │ ○ ● ○ ○ │ +│ 20 │ ● ● ○ ○ │ +│ │ ● ● ● ○ ○ │ +│ 10 │● ● ○ ○ ○ │ +│ │● ● ○ ○ ○ │ +│ 0 │● ● ○ ○ ○ │ +│ └─────────────────────────────────────────────────────► │ +│ 20 30 40 50 60 年龄 │ +│ │ +│ ● 模范型 ○ 压力型 ◐ 生活习惯型 │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 6.3 交互流程 + +1. 页面加载,默认使用3个聚类 +2. 渲染雷达图和散点图 +3. 用户可调整聚类数量(2-5) +4. 点击"重新聚类"按钮更新结果 +5. 点击某个聚类可查看详细信息和建议 +6. 散点图支持鼠标悬停查看员工详情 + +--- + +## 7. 公共组件 + +### 7.1 ChartComponent.vue + +**用途**:封装ECharts图表,统一管理图表生命周期 + +**Props**: + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| option | Object | {} | ECharts配置项 | +| loading | Boolean | false | 是否加载中 | +| height | String | '400px' | 图表高度 | +| width | String | '100%' | 图表宽度 | + +**使用示例**: + +```vue + +``` + +### 7.2 ResultCard.vue + +**用途**:展示预测结果 + +**Props**: + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| predictedHours | Number | 0 | 预测时长 | +| riskLevel | String | 'low' | 风险等级 | +| confidence | Number | 0 | 置信度 | + +### 7.3 KPICard.vue + +**用途**:展示KPI指标卡片 + +**Props**: + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| title | String | '' | 指标名称 | +| value | String/Number | '' | 指标值 | +| unit | String | '' | 单位 | +| icon | String | '' | 图标类名 | +| color | String | '#409EFF' | 主题色 | + +### 7.4 LoadingSpinner.vue + +**用途**:加载动画组件 + +**Props**: + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| text | String | '加载中...' | 提示文字 | + +--- + +## 8. 配色方案 + +### 8.1 主色调 + +| 用途 | 颜色值 | 说明 | +|------|--------|------| +| 主色 | #409EFF | Element Plus主色 | +| 成功 | #67C23A | 低风险、正向指标 | +| 警告 | #E6A23C | 中风险、需关注 | +| 危险 | #F56C6C | 高风险、异常 | +| 信息 | #909399 | 辅助信息 | + +### 8.2 图表配色 + +```javascript +const chartColors = [ + '#5470c6', // 蓝色 + '#91cc75', // 绿色 + '#fac858', // 黄色 + '#ee6666', // 红色 + '#73c0de', // 浅蓝 + '#3ba272', // 深绿 + '#fc8452', // 橙色 + '#9a60b4', // 紫色 + '#ea7ccc' // 粉色 +]; +``` + +--- + +## 9. 附录 + +### 9.1 页面清单 + +| 页面 | 路由 | 主要图表 | 主要交互 | +|------|------|----------|----------| +| 数据概览 | /dashboard | 折线图、饼图、柱状图 | 图表悬停、钻取 | +| 影响因素 | /analysis | 条形图、热力图 | 维度切换 | +| 缺勤预测 | /prediction | - | 表单提交 | +| 员工画像 | /clustering | 雷达图、散点图 | 聚类数调整 | + +### 9.2 文档修改历史 + +| 版本 | 日期 | 修改人 | 修改内容 | +|------|------|--------|----------| +| V1.0 | 2026-03 | 张硕 | 初始版本 | + +--- + +**文档结束** diff --git a/docs/1.md b/docs/1.md new file mode 100644 index 0000000..916759b --- /dev/null +++ b/docs/1.md @@ -0,0 +1,111 @@ +这是一个典型的**前后端分离**架构的毕设项目结构。为了契合你的题目《基于多维特征挖掘的员工缺勤分析与预测系统设计与实现》,我们将项目分为 `Backend`(Python 后端,负责算法与逻辑)和 `Frontend`(Vue 前端,负责展示与交互)。 +以下是详细的工程目录结构及说明: +--- +### 📁 项目根目录:Absenteeism_Analysis_System/ +``` +Absenteeism_Analysis_System/ +│ +├── backend/ # 后端项目目录 (Python/Flask) +│ ├── app.py # 程序入口文件 (启动服务) +│ ├── config.py # 配置文件 (路径、密钥等) +│ ├── requirements.txt # Python依赖库清单 (pandas, scikit-learn, flask等) +│ │ +│ ├── data/ # 数据存储目录 +│ │ ├── raw/ # 原始数据集 +│ │ │ └── Absenteeism_at_work.csv # 从UCI下载的原始数据 +│ │ └── processed/ # 处理后的数据集 +│ │ └── clean_data.csv # 经过清洗、编码后的数据 +│ │ +│ ├── models/ # 模型存储目录 +│ │ ├── rf_model.pkl # 训练好的随机森林模型文件 +│ │ ├── xgb_model.pkl # 训练好的XGBoost模型文件 +│ │ └── kmeans_model.pkl # 聚类模型文件 +│ │ +│ ├── core/ # 核心算法模块 (对应论文的“多维特征挖掘”) +│ │ ├── __init__.py +│ │ ├── preprocessing.py # 数据预处理:缺失值填充、独热编码、归一化 +│ │ ├── feature_mining.py # 特征挖掘:相关性分析、特征重要性计算 +│ │ ├── train_model.py # 模型训练脚本:训练RF/XGBoost并保存模型 +│ │ └── clustering.py # 聚类分析:K-Means算法实现 +│ │ +│ ├── services/ # 业务逻辑层 +│ │ ├── __init__.py +│ │ ├── analysis_service.py # 分析服务:调用特征挖掘模块,返回图表数据 +│ │ ├── predict_service.py # 预测服务:加载模型,进行推理 +│ │ └── data_service.py # 数据服务:读取CSV,提供基础统计 +│ │ +│ ├── api/ # API接口层 (路由) +│ │ ├── __init__.py +│ │ ├── analysis_routes.py # 接口:获取特征重要性、相关性等 +│ │ ├── predict_routes.py # 接口:接收前端表单,返回预测结果 +│ │ └── cluster_routes.py # 接口:返回聚类结果/员工画像 +│ │ +│ └── utils/ # 工具函数 +│ └── common.py # 通用工具:JSON封装、CORS处理等 +│ +├── frontend/ # 前端项目目录 +│ ├── public/ # 静态资源 +│ ├── src/ +│ │ ├── assets/ # 资源文件 (图片、样式) +│ │ ├── components/ # 公共组件 +│ │ │ ├── ChartComponent.vue # ECharts图表封装组件 +│ │ │ └── ResultCard.vue # 预测结果展示卡片 +│ │ │ +│ │ ├── views/ # 页面视图 (对应你的前端设计) +│ │ │ ├── Dashboard.vue # 页面一:数据概览与统计 +│ │ │ ├── FactorAnalysis.vue # 页面二:影响因素分析 (核心) +│ │ │ ├── Prediction.vue # 页面三:缺勤预测 (输入表单+结果) +│ │ │ └── Clustering.vue # 页面四:员工画像与聚类 +│ │ │ +│ │ ├── router/ # 路由配置 +│ │ │ └── index.js +│ │ ├── api/ # 前端API调用封装 +│ │ │ └── request.js # 配置axios,连接后端接口 +│ │ ├── App.vue # 根组件 +│ │ └── main.js # 入口文件 +│ │ +│ ├── package.json # 前端依赖 (vue, element-plus, echarts) +│ └── vite.config.js # Vue构建配置 (如果用Vite) 或 vue.config.js +│ +└── README.md # 项目说明文档 +``` +--- +### 🔧 核心模块功能详解(对应论文) +#### 1. 后端 `core/` 模块详解 +这是你论文中“算法设计”部分的代码落地: +* **`preprocessing.py`**: + * 实现 `OneHotEncoder` 处理 `Reason for absence` 等类别。 + * 实现 `StandardScaler` 处理 `Transportation expense` 等数值。 + * 实现 `get_clean_data()` 函数,供其他模块调用。 +* **`feature_mining.py`**: + * 实现 `calculate_correlation()`: 使用 Pandas 计算相关系数矩阵。 + * 实现 `get_feature_importance()`: 加载随机森林模型,提取 `feature_importances_`。 +* **`train_model.py`**: + * 包含 `train_rf()` 和 `train_xgboost()` 函数。 + * 负责划分训练集/测试集,计算 MSE/R2,并保存 `.pkl` 文件。 +* **`clustering.py`**: + * 包含 `kmeans_analysis()` 函数。 + * 对员工进行分类,并返回每个簇的中心点数据(用于画雷达图)。 +#### 2. 后端 `api/` 模块详解 +这是前后端交互的桥梁: +* **`analysis_routes.py`**: + * 路由: `/api/analysis/importance` + * 作用: 调用 `feature_mining.py`,返回格式化后的 JSON (例如 `{'feature': 'Distance', 'score': 0.45}`) 供前端画柱状图。 +* **`predict_routes.py`**: + * 路由: `/api/predict/single` + * 作用: 接收前端 POST 过来的 JSON (表单数据),调用 `predict_service.py` 加载模型,返回预测的小时数。 +#### 3. 前端 `views/` 模块详解 +这是你论文中“系统实现”部分的展示: +* **`FactorAnalysis.vue`**: + * 引入 `ECharts`。 + * `mounted()` 生命周期里调用 `/api/analysis/importance`。 + * 渲染:横向柱状图(特征重要性)、热力图(相关性矩阵)。 +* **`Prediction.vue`**: + * 左侧:`el-form` 表单,字段包含 `age`, `distance`, `drink` 等。 + * 右侧:`el-card`,初始状态为空,点击“开始预测”后,显示返回的 `hours` 和风险等级(红/黄/绿标签)。 +--- +### 💡 为什么要这样设计? +1. **分层清晰(MVC模式)**:`core` 专心算算法,`services` 处理业务,`api` 管接口。这在答辩时非常有优势,老师会觉得你代码结构规范,软件工程素养好。 +2. **算法模块化**:你没有把算法代码直接写在接口文件里,而是单独封装了 `core` 目录。这意味着如果以后你想换算法(比如不用随机森林改用神经网络),只需要改 `core` 里的代码,不需要动接口,这叫**“高内聚低耦合”**。 +3. **数据隔离**:原始数据 (`raw`) 和 清洗数据 (`processed`) 分开,训练好的模型单独放在 `models`,这样项目管理非常干净,不会一团乱麻。 +按照这个结构去写代码和画图(系统架构图、时序图),你的论文逻辑会非常顺畅! diff --git a/docs/2.md b/docs/2.md new file mode 100644 index 0000000..ac61f58 --- /dev/null +++ b/docs/2.md @@ -0,0 +1,48 @@ +基于你的项目架构和题目《基于多维特征挖掘的员工缺勤分析与预测系统设计与实现》,预期实现的功能可以分为四个核心模块。你可以直接把这些内容写到开题报告的“研究内容”或“系统功能需求”章节里。 +--- +### 一、 数据概览与全局统计分析功能 +这是系统的“仪表盘”,让用户对整体情况一目了然。 +* **多维统计展示:** + * **功能描述:** 系统自动加载 UCI 考勤数据集,展示基础统计指标(样本总数、缺勤总时长、平均缺勤时长、最大/最小缺勤时长)。 + * **实现价值:** 帮助管理者快速了解企业整体考勤健康状况。 +* **时间维度趋势分析:** + * **功能描述:** 以折线图形式展示全年(1-12月)的缺勤变化趋势;以柱状图展示周一至周五的缺勤分布;以饼图展示不同季节(春夏秋冬)的缺勤比例。 + * **实现价值:** 识别出缺勤的高发时间段(例如:发现周五缺勤率最高,或夏季缺勤最多)。 +### 二、 多维特征挖掘与影响因素分析功能 +这是系统的核心亮点,对应题目中的“多维特征挖掘”,解决“为什么缺勤”的问题。 +* **特征重要性排序:** + * **功能描述:** 利用训练好的随机森林模型,计算并展示各维度特征对缺勤的影响权重。例如:柱状图显示“通勤距离”影响最大,“BMI指数”次之,“宠物数量”影响最小。 + * **实现价值:** 量化指标,让管理者直观看到哪些是导致缺勤的“罪魁祸首”。 +* **关联性热力图分析:** + * **功能描述:** 计算特征之间的相关系数矩阵,以热力图形式展示。重点突出“生活习惯”(如 Social drinker)与“缺勤时长”之间的强相关关系。 + * **实现价值:** 挖掘隐性规律,比如发现“爱喝酒的员工”更容易“无故缺勤”,为制定公司制度(如禁止酒后上岗)提供数据支持。 +* **群体特征对比:** + * **功能描述:** 提供分组统计功能,对比不同群体(如:高学历 vs 低学历,有子女 vs 无子女)的平均缺勤时长。 + * **实现价值:** 细分人群,实现精细化管理。 +### 三、 员工缺勤风险预测功能 +这是系统的实用工具,对应题目中的“预测”,解决“未来会怎样”的问题。 +* **单次缺勤时长预测:** + * **功能描述:** 提供一个交互式表单,用户输入(或选择)某员工的各项属性(年龄、距离、交通费、BMI、是否饮酒、月份、工作负荷等),系统调用后台预测模型(XGBoost/RF),实时返回预测的缺勤时长(例如:预测结果为 8 小时)。 + * **实现价值:** 当某个月工作负荷很大或季节变化时,可提前预判该员工的缺勤情况。 +* **缺勤风险等级评估:** + * **功能描述:** 根据预测时长,自动将员工标记为“低风险(绿色)”、“中风险(黄色)”或“高风险(红色)”。 + * **实现价值:** 快速筛选出需要重点关注的“刺头”员工或困难员工。 +* **新入职员工评估(扩展):** + * **功能描述:** 针对没有历史数据的新员工,仅凭其入职时的属性信息(如居住地、年龄、体检BMI等),系统给出其潜在缺勤风险的预估。 + * **实现价值:** 辅助HR在招聘环节进行人员筛选。 +### 四、 员工画像与群体聚类功能 +这是系统的高级分析功能,展示算法对人群的分类能力。 +* **K-Means 聚类分析:** + * **功能描述:** 系统利用 K-Means 算法自动将所有员工划分为 3-4 个类别(如:模范型、压力型、生活习惯型)。 +* **员工群体画像(雷达图):** + * **功能描述:** 对每个聚类群体的特征(工龄、负荷、BMI、距离、缺勤倾向)绘制雷达图。 + * **实现价值:** + * 比如识别出“压力型群体”(工龄短、负荷极大、缺勤多),建议HR减少加班; + * 识别出“生活习惯型群体”(BMI高、爱喝酒),建议HR关注体检。 +### 五、 系统管理功能 +基础功能,保证系统的可用性。 +* **数据导入与更新:** 支持上传新的 CSV 考勤文件,系统自动解析并更新数据库。 +* **模型管理:** 展示当前使用的算法模型(随机森林/XGBoost)以及该模型在测试集上的准确率、均方误差(MSE)等性能指标。 +--- +### 💡 总结一句话 +本系统预期实现从**“数据录入”**到**“可视化统计”**,再到**“深度归因分析”**,最后实现**“精准风险预测”**和**“人群画像划分”**的全流程功能,能够为企业提供一套完整的人力资源考勤数据智能分析解决方案。 diff --git a/docs/3.md b/docs/3.md new file mode 100644 index 0000000..b1ecb5b --- /dev/null +++ b/docs/3.md @@ -0,0 +1,221 @@ +Data Set Name: + +Absenteeism at work - Part I + +Abstract: + +The database was created with records of absenteeism at work from July 2007 to July 2010 at a courier company in Brazil. + +Source: + +Creators original owner and donors: Andrea Martiniano (1), Ricardo Pinto Ferreira (2), and Renato Jose Sassi (3). + +E-mail address: +andrea.martiniano'@'gmail.com (1) - PhD student; +log.kasparov'@'gmail.com (2) - PhD student; +sassi'@'uni9.pro.br (3) - Prof. Doctor. + +Universidade Nove de Julho - Postgraduate Program in Informatics and Knowledge Management. + +Address: Rua Vergueiro, 235/249 Liberdade, Sao Paulo, SP, Brazil. Zip code: 01504-001. + +Website: http://www.uninove.br/curso/informatica-e-gestao-do-conhecimento/ + + +Data Type: Multivariate   Univariate   Sequential   Time-Series   Text   Domain-Theory   +Task: Classification   Regression   Clustering   Causal Discovery +Attribute Type: Categorical   Integer   Real +Area: Life Sciences Physical Sciences CS / Engineering Social Sciences Business Game Other +Format Type: Matrix Non-Matrix +Does your data set contain missing values? Yes No + +Number of Instances (records in your data set):  +Number of Attributes (fields within each record):  +*-*-*-*-*-* + +Relevant Information: + + +The data set allows for several new combinations of attributes and attribute exclusions, or the modification of the attribute type (categorical, integer, or real) depending on the purpose of the research.The data set (Absenteeism at work - Part I) was used in academic research at the Universidade Nove de Julho - Postgraduate Program in Informatics and Knowledge Management. + +Attribute Information: + +1. Individual identification (ID) +2. Reason for absence (ICD). +Absences attested by the International Code of Diseases (ICD) stratified into 21 categories (I to XXI) as follows: + +I Certain infectious and parasitic diseases +II Neoplasms +III Diseases of the blood and blood-forming organs and certain disorders involving the immune mechanism +IV Endocrine, nutritional and metabolic diseases +V Mental and behavioural disorders +VI Diseases of the nervous system +VII Diseases of the eye and adnexa +VIII Diseases of the ear and mastoid process +IX Diseases of the circulatory system +X Diseases of the respiratory system +XI Diseases of the digestive system +XII Diseases of the skin and subcutaneous tissue +XIII Diseases of the musculoskeletal system and connective tissue +XIV Diseases of the genitourinary system +XV Pregnancy, childbirth and the puerperium +XVI Certain conditions originating in the perinatal period +XVII Congenital malformations, deformations and chromosomal abnormalities +XVIII Symptoms, signs and abnormal clinical and laboratory findings, not elsewhere classified +XIX Injury, poisoning and certain other consequences of external causes +XX External causes of morbidity and mortality +XXI Factors influencing health status and contact with health services. + +And 7 categories without (CID) patient follow-up (22), medical consultation (23), blood donation (24), laboratory examination (25), unjustified absence (26), physiotherapy (27), dental consultation (28). +3. Month of absence +4. Day of the week (Monday (2), Tuesday (3), Wednesday (4), Thursday (5), Friday (6)) +5. Seasons (summer (1), autumn (2), winter (3), spring (4)) +6. Transportation expense +7. Distance from Residence to Work (kilometers) +8. Service time +9. Age +10. Work load Average/day +11. Hit target +12. Disciplinary failure (yes=1; no=0) +13. Education (high school (1), graduate (2), postgraduate (3), master and doctor (4)) +14. Son (number of children) +15. Social drinker (yes=1; no=0) +16. Social smoker (yes=1; no=0) +17. Pet (number of pet) +18. Weight +19. Height +20. Body mass index +21. Absenteeism time in hours (target) + +.arff header for Weka: + +@relation Absenteeism_at_work + +@attribute ID {31.0, 27.0, 19.0, 30.0, 7.0, 20.0, 24.0, 32.0, 3.0, 33.0, 26.0, 29.0, 18.0, 25.0, 17.0, 14.0, 16.0, 23.0, 2.0, 21.0, 36.0, 15.0, 22.0, 5.0, 12.0, 9.0, 6.0, 34.0, 10.0, 28.0, 13.0, 11.0, 1.0, 4.0, 8.0, 35.0} +@attribute Reason_for_absence {17.0, 3.0, 15.0, 4.0, 21.0, 2.0, 9.0, 24.0, 18.0, 1.0, 12.0, 5.0, 16.0, 7.0, 27.0, 25.0, 8.0, 10.0, 26.0, 19.0, 28.0, 6.0, 23.0, 22.0, 13.0, 14.0, 11.0, 0.0} +@attribute Month_of_absence REAL +@attribute Day_of_the_week {5.0, 2.0, 3.0, 4.0, 6.0} +@attribute Seasons {4.0, 1.0, 2.0, 3.0} +@attribute Transportation_expense REAL +@attribute Distance_from_Residence_to_Work REAL +@attribute Service_time INTEGER +@attribute Age INTEGER +@attribute Work_load_Average/day_ REAL +@attribute Hit_target REAL +@attribute Disciplinary_failure {1.0, 0.0} +@attribute Education REAL +@attribute Son REAL +@attribute Social_drinker {1.0, 0.0} +@attribute Social_smoker {1.0, 0.0} +@attribute Pet REAL +@attribute Weight REAL +@attribute Height REAL +@attribute Body_mass_index REAL +@attribute Absenteeism_time_in_hours REAL + + +Relevant Papers: + + + +Martiniano, A., Ferreira, R. P., Sassi, R. J., & Affonso, C. (2012). Application of a neuro fuzzy network in prediction of absenteeism at work. In Information Systems and Technologies (CISTI), 7th Iberian Conference on (pp. 1-4). IEEE. + + +Citation Requests / Acknowledgements: + + + +Martiniano, A., Ferreira, R. P., Sassi, R. J., & Affonso, C. (2012). Application of a neuro fuzzy network in prediction of absenteeism at work. In Information Systems and Technologies (CISTI), 7th Iberian Conference on (pp. 1-4). IEEE. + +Acknowledgements: +Professor Gary Johns for contributing to the selection of relevant research attributes. +Professor Emeritus of Management +Honorary Concordia University Research Chair in Management +John Molson School of Business +Concordia University +Montreal, Quebec, Canada +Adjunct Professor, OB/HR Division +Sauder School of Business, +University of British Columbia +Vancouver, British Columbia, Canada + + +--------------------------------------------------------------------------- + + + + +Attribute Information: + +1. Individual identification (ID) +2. Reason for absence (ICD). +Absences attested by the International Code of Diseases (ICD) stratified into 21 categories (I to XXI) as follows: + +I Certain infectious and parasitic diseases +II Neoplasms +III Diseases of the blood and blood-forming organs and certain disorders involving the immune mechanism +IV Endocrine, nutritional and metabolic diseases +V Mental and behavioural disorders +VI Diseases of the nervous system +VII Diseases of the eye and adnexa +VIII Diseases of the ear and mastoid process +IX Diseases of the circulatory system +X Diseases of the respiratory system +XI Diseases of the digestive system +XII Diseases of the skin and subcutaneous tissue +XIII Diseases of the musculoskeletal system and connective tissue +XIV Diseases of the genitourinary system +XV Pregnancy, childbirth and the puerperium +XVI Certain conditions originating in the perinatal period +XVII Congenital malformations, deformations and chromosomal abnormalities +XVIII Symptoms, signs and abnormal clinical and laboratory findings, not elsewhere classified +XIX Injury, poisoning and certain other consequences of external causes +XX External causes of morbidity and mortality +XXI Factors influencing health status and contact with health services. + +And 7 categories without (CID) patient follow-up (22), medical consultation (23), blood donation (24), laboratory examination (25), unjustified absence (26), physiotherapy (27), dental consultation (28). +3. Month of absence +4. Day of the week (Monday (2), Tuesday (3), Wednesday (4), Thursday (5), Friday (6)) +5. Seasons +6. Transportation expense +7. Distance from Residence to Work (kilometers) +8. Service time +9. Age +10. Work load Average/day +11. Hit target +12. Disciplinary failure (yes=1; no=0) +13. Education (high school (1), graduate (2), postgraduate (3), master and doctor (4)) +14. Son (number of children) +15. Social drinker (yes=1; no=0) +16. Social smoker (yes=1; no=0) +17. Pet (number of pet) +18. Weight +19. Height +20. Body mass index +21. Absenteeism time in hours (target) + +.arff header for Weka: + +@relation Absenteeism_at_work + +@attribute ID {31.0, 27.0, 19.0, 30.0, 7.0, 20.0, 24.0, 32.0, 3.0, 33.0, 26.0, 29.0, 18.0, 25.0, 17.0, 14.0, 16.0, 23.0, 2.0, 21.0, 36.0, 15.0, 22.0, 5.0, 12.0, 9.0, 6.0, 34.0, 10.0, 28.0, 13.0, 11.0, 1.0, 4.0, 8.0, 35.0} +@attribute Reason_for_absence {17.0, 3.0, 15.0, 4.0, 21.0, 2.0, 9.0, 24.0, 18.0, 1.0, 12.0, 5.0, 16.0, 7.0, 27.0, 25.0, 8.0, 10.0, 26.0, 19.0, 28.0, 6.0, 23.0, 22.0, 13.0, 14.0, 11.0, 0.0} +@attribute Month_of_absence REAL +@attribute Day_of_the_week {5.0, 2.0, 3.0, 4.0, 6.0} +@attribute Seasons {4.0, 1.0, 2.0, 3.0} +@attribute Transportation_expense REAL +@attribute Distance_from_Residence_to_Work REAL +@attribute Service_time INTEGER +@attribute Age INTEGER +@attribute Work_load_Average/day_ REAL +@attribute Hit_target REAL +@attribute Disciplinary_failure {1.0, 0.0} +@attribute Education REAL +@attribute Son REAL +@attribute Drinker {1.0, 0.0} +@attribute Smoker {1.0, 0.0} +@attribute Pet REAL +@attribute Weight REAL +@attribute Height REAL +@attribute Body_mass_index REAL +@attribute Absenteeism_time_in_hours REAL \ No newline at end of file diff --git a/docs/开题报告.docx b/docs/开题报告.docx new file mode 100644 index 0000000..d9bd8a2 Binary files /dev/null and b/docs/开题报告.docx differ diff --git a/docs/开题报告.md b/docs/开题报告.md new file mode 100644 index 0000000..d54fd04 --- /dev/null +++ b/docs/开题报告.md @@ -0,0 +1,123 @@ +# 河南农业大学本科毕业论文(设计)开题报告 + +## 基本信息 + +- **学院**:软件学院 +- **专业**:数据科学与大数据技术 +- **班级**:22级11班 +- **学号**:2210121330 +- **学生姓名**:张硕 +- **指导教师**:孙昌霞、李天格 +- **题目名称**:基于多维特征挖掘的员工缺勤分析与预测系统设计与实现 + +--- + +## 选题目的与意义 + +**研究目的:** + +随着企业数字化转型的深入推进,人力资源管理正从经验驱动向数据驱动转变。员工缺勤作为影响企业运营效率的重要因素,其背后蕴含着丰富的多维度信息。本课题旨在利用机器学习算法,对UCI Absenteeism数据集中的740条员工考勤记录进行深入分析,挖掘影响缺勤的多维度特征,构建基于随机森林和XGBoost的缺勤预测模型,并设计实现一个完整的数据分析与预测系统。通过该系统,企业能够从数据中发现缺勤背后的规律,实现对员工缺勤风险的精准识别和预警,为人力资源管理提供科学、客观的决策支持。 + +**研究意义:** + +从理论层面来看,本课题探索了多维特征挖掘在人力资源数据分析领域的应用价值。传统的缺勤研究多侧重于单一因素分析或简单的统计描述,缺乏对多维度特征之间复杂关系的深入挖掘。本研究将特征工程、相关性分析、机器学习预测和聚类分析等方法有机结合,构建了一个完整的分析框架,为相关领域的研究提供了方法论参考。同时,通过对随机森林、XGBoost等算法在缺勤预测任务中的性能对比,丰富了机器学习在人力资源管理领域的应用案例。 + +从实践层面来看,本课题具有重要的现实意义。员工缺勤不仅直接影响企业的工作进度和运营成本,还可能反映员工的工作压力、健康状况、工作满意度等深层次问题。通过本系统,企业能够识别出影响缺勤的关键因素,如通勤距离、工作负荷、生活习惯等,从而有针对性地制定管理策略。例如,如果发现通勤距离是主要影响因素,企业可以考虑提供交通补贴或调整工作地点;如果发现工作负荷过高导致缺勤,可以优化工作分配或增加人力投入。此外,系统的预测功能能够帮助HR提前识别高风险员工,采取预防措施,降低缺勤带来的损失。聚类分析功能则能够将员工划分为不同群体,实现精细化管理,提升人力资源管理的效率和效果。 + +--- + +## 论文主要内容 + +### 1. 数据概览与全局统计分析 + +本研究的第一个核心内容是对UCI Absenteeism数据集进行全面的探索性数据分析。该数据集记录了巴西某快递公司2007年至2010年间的740条员工缺勤记录,包含21个特征字段。首先,系统将计算并展示关键统计指标,包括样本总数、缺勤总时长、平均缺勤时长、最大/最小缺勤时长、高风险员工占比等,帮助管理者快速了解企业整体考勤健康状况。其次,从时间维度进行深入分析,通过折线图展示全年12个月的缺勤变化趋势,识别季节性规律;通过柱状图展示周一至周五的缺勤分布,发现工作日缺勤的周期性特征;通过饼图展示春夏秋冬四个季节的缺勤比例,探索环境因素对缺勤的影响。最后,对缺勤原因进行分类统计,数据集包含28类缺勤原因,其中21类为国际疾病分类(ICD)代码,7类为非疾病原因(如医疗咨询、献血、无故缺勤等),通过可视化展示各类原因的占比,帮助企业了解缺勤的主要类型。 + +### 2. 多维特征挖掘与影响因素分析 + +这是本研究的核心内容,旨在回答"为什么缺勤"这一关键问题。首先,利用训练好的随机森林模型,计算各维度特征对缺勤的影响权重。随机森林算法能够输出每个特征的重要性得分,通过条形图降序排列,直观展示哪些特征是导致缺勤的主要因素,例如可能发现"通勤距离"、"工作负荷"、"饮酒习惯"等特征具有较高的重要性得分,而"宠物数量"、"身高"等特征影响较小。其次,计算特征之间的相关系数矩阵,以热力图形式展示特征间的关系,特别关注生活习惯特征(如Social drinker)与缺勤时长之间的相关关系,挖掘隐性规律。例如,可能发现饮酒员工与缺勤时长之间存在正相关,为制定公司制度提供数据支持。最后,进行群体对比分析,将员工按照不同维度分组,对比各组的平均缺勤时长,如饮酒者vs不饮酒者、高学历vs低学历、有子女vs无子女等,识别不同群体的缺勤特征,为精细化管理提供依据。 + +### 3. 员工缺勤风险预测 + +本研究的第三个核心内容是构建缺勤预测模型,解决"未来会怎样"的问题。基于XGBoost和随机森林两种回归算法,构建预测模型,输入员工的17个特征属性(包括年龄、通勤距离、交通费、工作负荷、BMI、饮酒习惯、月份等),输出预测的缺勤时长。模型训练过程中,将数据集划分为训练集和测试集,采用交叉验证方法优化模型参数,使用均方误差(MSE)、决定系数(R²)等指标评估模型性能。在系统层面,设计交互式预测界面,左侧为参数输入表单,用户可以输入或选择各项属性值,点击"开始预测"按钮后,右侧实时显示预测结果。预测结果包括预测的缺勤时长(如8小时)、风险等级(<4小时为低风险绿色,4-8小时为中风险黄色,>8小时为高风险红色)以及模型的可信度(如准确率85%)。此外,系统还支持新入职员工评估功能,针对没有历史数据的新员工,仅凭其入职时的属性信息,系统给出潜在缺勤风险的预估,辅助HR在招聘环节进行人员筛选。 + +### 4. 员工画像与群体聚类 + +本研究的第四个核心内容是利用K-Means聚类算法对员工进行分类,展示算法对人群的分类能力。K-Means算法能够将所有员工自动划分为3-4个类别,例如可能识别出"模范型"(工龄长、负荷适中、缺勤少)、"压力型"(工龄短、负荷极大、缺勤多)、"生活习惯型"(BMI高、爱喝酒)等不同群体。对于每个聚类群体,系统将绘制雷达图,展示其在年龄、工龄、工作负荷、BMI、缺勤倾向等维度上的特征分布,让管理者一目了然地看到不同群体的差异。例如,压力型群体可能在"工作负荷"轴上特别长,而"缺勤倾向"轴也较高,提示HR需要关注该群体的工作压力问题。同时,通过散点图展示聚类结果,横轴为年龄,纵轴为缺勤时长,不同颜色的点代表不同的聚类群体,直观展示群体的分布特征。基于聚类结果,系统将为HR提供针对性的管理建议,如对压力型群体建议减少加班、对生活习惯型群体建议关注体检等。 + +### 5. 系统设计与实现 + +本研究的最后一个核心内容是将上述算法和分析功能集成到一个完整的系统中。系统采用前后端分离架构,后端使用Python Flask框架,负责数据处理、模型训练和API接口提供;前端使用Vue 3框架配合Element Plus UI组件库和ECharts图表库,负责数据展示和用户交互。系统包含四个核心功能模块:数据概览模块(Dashboard)、影响因素分析模块(FactorAnalysis)、缺勤预测模块(Prediction)和员工画像模块(Clustering)。后端采用MVC分层架构,core层负责算法实现(数据预处理、特征挖掘、模型训练、聚类分析),services层负责业务逻辑,api层负责接口路由。前端采用组件化设计,封装ChartComponent和ResultCard等公共组件,提高代码复用性。系统开发完成后,将进行功能测试、性能测试和用户体验测试,确保系统的稳定性和可用性。最终,系统将为企业提供一套完整的人力资源考勤数据智能分析解决方案,实现从数据录入、可视化统计、深度归因分析到精准风险预测和人群画像划分的全流程功能。 + +--- + +## 主要技术路线或方法 + +### 技术架构 + +本研究采用前后端分离的架构设计,确保系统的可维护性和可扩展性。后端技术栈选择Python作为主要开发语言,使用Flask轻量级Web框架构建RESTful API接口,利用scikit-learn和XGBoost库实现机器学习算法,使用pandas和numpy进行数据处理和分析。前端技术栈选择Vue 3作为前端框架,配合Element Plus UI组件库实现美观的用户界面,使用ECharts图表库实现丰富的数据可视化效果。数据存储采用CSV文件格式,便于数据导入导出和模型训练。整个架构遵循MVC设计模式,后端分为core(算法层)、services(业务逻辑层)、api(接口层)三层,前端分为views(页面层)、components(组件层)、api(调用层)三层,各层职责清晰,便于开发和维护。 + +### 算法方法 + +在算法层面,本研究采用了多种机器学习技术,形成完整的分析流程。首先,数据预处理阶段,针对数据集中的21个特征字段,采用不同的处理方法:对于类别型特征(如Reason for absence、Education、Social drinker等),使用OneHotEncoder进行独热编码,将其转换为数值型特征;对于数值型特征(如Transportation expense、Age、Work load等),使用StandardScaler进行标准化处理,消除量纲差异,提高模型训练效果。其次,特征挖掘阶段,使用pandas计算特征间的皮尔逊相关系数,生成相关性矩阵,用于热力图展示;使用训练好的随机森林模型提取feature_importances_属性,计算各特征的重要性得分,用于特征重要性排序。再次,预测模型构建阶段,采用两种回归算法:随机森林和XGBoost,这两种算法都具有较好的泛化能力和抗过拟合能力,适合处理多维度特征。模型训练时采用交叉验证方法,使用网格搜索优化超参数,使用均方误差(MSE)、决定系数(R²)等指标评估模型性能。最后,聚类分析阶段,使用K-Means算法对员工进行无监督聚类,通过肘部法则确定最佳聚类数量,将员工划分为3-4个群体,并计算每个簇的中心点数据,用于雷达图展示。 + +### 开发流程 + +本研究采用敏捷开发方法,按照以下流程进行:首先进行需求分析,明确系统的功能需求和非功能需求,确定系统的核心功能模块和用户交互流程;然后进行系统设计,包括架构设计、数据库设计、接口设计和UI设计,绘制系统架构图、时序图等设计文档;接着进行数据预处理,对UCI数据集进行清洗、编码、归一化等处理,生成可用于模型训练的干净数据;随后进行模型训练,分别训练随机森林、XGBoost和K-Means模型,评估模型性能,保存训练好的模型文件;然后进行前端开发,使用Vue 3开发四个核心页面,实现数据可视化、表单交互等功能;接着进行接口对接,后端提供RESTful API接口,前端通过axios调用接口获取数据,实现前后端数据交互;最后进行测试优化,进行功能测试、性能测试和用户体验测试,修复bug,优化系统性能,确保系统稳定可用。整个开发过程中,采用迭代开发的方式,每个阶段完成后进行评审和调整,确保项目按时高质量完成。 + +--- + +## 预期结果 + +### 系统成果 + +本研究预期完成一个功能完整、界面美观、操作便捷的员工缺勤分析与预测系统。该系统将包含四个核心功能模块:数据概览模块(Dashboard)将展示KPI指标卡、缺勤原因分布饼图、月度趋势折线图、星期几热力图等可视化图表,让管理者一目了然地了解企业整体考勤状况;影响因素分析模块(FactorAnalysis)将展示特征重要性排序条形图、相关性热力图、群体对比分析柱状图,帮助管理者识别影响缺勤的关键因素;缺勤预测模块(Prediction)将提供交互式表单,支持17个特征输入,实时返回预测结果和风险等级,为HR提供决策支持;员工画像模块(Clustering)将展示K-Means聚类结果,通过雷达图和散点图呈现不同员工群体的特征画像,为精细化管理提供依据。系统将采用响应式设计,支持不同屏幕尺寸的访问,具有良好的用户体验。 + +### 模型性能 + +在模型性能方面,本研究预期达到以下目标:预测模型的准确率(R²)达到80%以上,均方误差(MSE)控制在合理范围内,模型具有良好的泛化能力,能够在测试集上保持稳定的预测效果。特征重要性排序结果将具有可解释性,能够识别出对缺勤影响最大的几个特征,如通勤距离、工作负荷、饮酒习惯等,这些发现将与实际业务场景相符,具有实践指导意义。相关性分析将揭示特征间的关系,特别是生活习惯特征与缺勤时长之间的关联,为企业制定管理制度提供数据支持。K-Means聚类结果将具有明显的群体差异,每个聚类群体在多个维度上呈现不同的特征分布,能够为HR提供针对性的管理建议。所有模型结果都将通过可视化图表直观展示,便于理解和应用。 + +### 论文成果 + +本研究预期完成一篇8000字以上的本科毕业论文,论文将包含以下几个核心部分:引言部分阐述研究背景、研究目的和研究意义,介绍国内外研究现状;系统设计部分详细描述系统的架构设计、功能模块设计、数据库设计和接口设计;算法实现部分详细介绍数据预处理、特征挖掘、预测模型和聚类分析的算法原理和实现过程;实验分析部分展示系统的功能演示、模型性能评估、特征重要性分析和聚类结果分析;结论与展望部分总结研究成果,指出研究的创新点和局限性,展望未来的研究方向。论文将采用规范的学术写作风格,逻辑清晰,论证充分,图表丰富,能够全面展示本研究的成果和价值。论文将通过查重检测,确保学术诚信,达到本科毕业论文的质量要求。 + +--- + +## 进度安排 + +本研究将严格按照以下时间表进行,确保项目按时高质量完成: + +**第一阶段:开题准备(2025.12.22-2026.01.18)** + +在此阶段,主要任务是确认论文题目,深入理解研究需求和目标。首先,广泛查阅国内外相关文献,了解员工缺勤分析、特征挖掘、机器学习预测等领域的研究现状,梳理相关理论和方法。其次,仔细研读UCI Absenteeism数据集的文档,理解数据集的字段含义、数据分布和特征类型,为后续分析奠定基础。然后,撰写开题报告,明确研究目的、研究意义、研究内容、技术路线和预期成果,与指导教师进行沟通,根据反馈意见修改完善。最后,制定详细的实施计划,确定系统的功能模块和技术选型,为后续开发做好准备。 + +**第二阶段:系统设计与原型开发(2026.01.19-2026.03.01)** + +在此阶段,主要任务是完成系统的详细设计和简单原型的开发。首先,进行系统架构设计,确定前后端分离的技术架构,绘制系统架构图和功能模块图。其次,进行数据库设计,确定数据存储方案,设计数据表结构。然后,进行接口设计,定义前后端交互的API接口规范。接着,搭建开发环境,配置Python开发环境、Vue开发环境和相关依赖库。随后,开始原型开发,首先实现数据预处理功能,对UCI数据集进行清洗和编码;然后实现简单的特征挖掘功能,计算特征重要性;接着实现前端的基础框架,搭建Vue项目,配置路由和UI组件库;最后,完成简单的预测功能原型,验证技术方案的可行性。 + +**第三阶段:系统开发与论文撰写(2026.03.02-2026.03.31)** + +在此阶段,主要任务是完成系统的完整开发和论文初稿的撰写。首先,完善后端算法模块,实现数据预处理、特征挖掘、模型训练和聚类分析的完整功能,训练随机森林、XGBoost和K-Means模型,保存模型文件。其次,完善后端API接口,提供数据统计、特征分析、预测推理和聚类结果的RESTful接口。然后,开发前端页面,完成Dashboard、FactorAnalysis、Prediction和Clustering四个核心页面,实现数据可视化、表单交互和结果展示功能。接着,进行前后端联调,确保数据交互正常,功能完整可用。同时,开始撰写论文,按照论文结构逐步完成各个章节的写作,包括引言、系统设计、算法实现、实验分析等部分。 + +**第四阶段:测试优化与答辩准备(2026.04.01-2026.05.10)** + +在此阶段,主要任务是完善系统、优化性能、完成论文和准备答辩。首先,进行系统测试,包括功能测试、性能测试和兼容性测试,修复发现的bug,优化系统性能,确保系统稳定可靠。其次,完善论文内容,根据系统实现情况调整论文描述,补充实验结果和分析,确保论文与实际成果一致。然后,进行论文格式调整,按照学校要求调整论文格式、字体、排版等,准备参考文献列表。接着,准备答辩材料,制作答辩PPT,梳理研究思路和成果,准备答辩演讲稿。最后,进行预答辩演练,与同学或指导教师进行模拟答辩,根据反馈意见调整PPT和演讲稿,确保答辩顺利进行。 + +--- + +## 参考文献 + +[1] Martiniano A, Ferreira R P, Sassi R J, et al. Application of a neuro fuzzy network in prediction of absenteeism at work[C]//Information Systems and Technologies (CISTI), 7th Iberian Conference on. IEEE, 2012: 1-4. + +[2] UCI Machine Learning Repository. Absenteeism at work Data Set[DB/OL]. https://archive.ics.uci.edu/ml/datasets/Absenteeism+at+work + +[3] Breiman L. Random forests[J]. Machine learning, 2001, 45(1): 5-32. + +[4] Chen T, Guestrin C. XGBoost: A scalable tree boosting system[C]//Proceedings of the 22nd ACM SIGKDD international conference on knowledge discovery and data mining. 2016: 785-794. + +[5] Lloyd S. Least squares quantization in PCM[J]. IEEE transactions on information theory, 1982, 28(2): 129-137. + +[6] Johns G. Presenteeism in the workplace: A review and research agenda[J]. Journal of organizational behavior, 2010, 31(4): 519-542. + +[7] Harrison D A, Martocchio J J. Time for absenteeism: A 20-year review of origins, offshoots, and outcomes[J]. Journal of management, 1998, 24(3): 305-350. + +[8] Ngai E W T, Chau D C K, Chan T L A. Information technology, operational, and management research on productivity: A study of executive perceptions[J]. International Journal of Production Economics, 2011, 133(2): 777-786. \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..55d7181 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,43 @@ +# Dependencies +node_modules/ +.pnpm-store/ + +# Build output +dist/ +dist-ssr/ +*.local + +# Environment variables +.env +.env.local +.env.*.local + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Editor directories and files +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Test coverage +coverage/ +.nyc_output/ diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..41a62eb --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + 员工缺勤分析与预测系统 + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..699bd08 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,22 @@ +{ + "name": "absenteeism-analysis-frontend", + "version": "1.0.0", + "description": "员工缺勤分析与预测系统前端", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.4.0", + "vue-router": "^4.2.5", + "element-plus": "^2.4.4", + "echarts": "^5.4.3", + "axios": "^1.6.2" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.5.2", + "vite": "^5.0.10" + } +} diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml new file mode 100644 index 0000000..98e72a5 --- /dev/null +++ b/frontend/pnpm-lock.yaml @@ -0,0 +1,1132 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + axios: + specifier: ^1.6.2 + version: 1.13.6 + echarts: + specifier: ^5.4.3 + version: 5.6.0 + element-plus: + specifier: ^2.4.4 + version: 2.13.5(vue@3.5.29) + vue: + specifier: ^3.4.0 + version: 3.5.29 + vue-router: + specifier: ^4.2.5 + version: 4.6.4(vue@3.5.29) + devDependencies: + '@vitejs/plugin-vue': + specifier: ^4.5.2 + version: 4.6.2(vite@5.4.21)(vue@3.5.29) + vite: + specifier: ^5.0.10 + version: 5.4.21 + +packages: + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@ctrl/tinycolor@4.2.0': + resolution: {integrity: sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==} + engines: {node: '>=14'} + + '@element-plus/icons-vue@2.3.2': + resolution: {integrity: sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==} + peerDependencies: + vue: ^3.2.0 + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@sxzz/popperjs-es@2.11.8': + resolution: {integrity: sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.24': + resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} + + '@types/web-bluetooth@0.0.20': + resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} + + '@vitejs/plugin-vue@4.6.2': + resolution: {integrity: sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.0.0 || ^5.0.0 + vue: ^3.2.25 + + '@vue/compiler-core@3.5.29': + resolution: {integrity: sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==} + + '@vue/compiler-dom@3.5.29': + resolution: {integrity: sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==} + + '@vue/compiler-sfc@3.5.29': + resolution: {integrity: sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==} + + '@vue/compiler-ssr@3.5.29': + resolution: {integrity: sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/reactivity@3.5.29': + resolution: {integrity: sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==} + + '@vue/runtime-core@3.5.29': + resolution: {integrity: sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==} + + '@vue/runtime-dom@3.5.29': + resolution: {integrity: sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==} + + '@vue/server-renderer@3.5.29': + resolution: {integrity: sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==} + peerDependencies: + vue: 3.5.29 + + '@vue/shared@3.5.29': + resolution: {integrity: sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==} + + '@vueuse/core@12.0.0': + resolution: {integrity: sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==} + + '@vueuse/metadata@12.0.0': + resolution: {integrity: sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==} + + '@vueuse/shared@12.0.0': + resolution: {integrity: sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==} + + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.13.6: + resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + echarts@5.6.0: + resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==} + + element-plus@2.13.5: + resolution: {integrity: sha512-dmY24fhSREfZN/PuUt0YZigMso7wWzl+B5o+YKNN15kQIn/0hzamsPU+ebj9SES0IbUqsLX1wkrzYmzU8VrVOQ==} + peerDependencies: + vue: ^3.3.0 + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + lodash-es@4.17.23: + resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + + lodash-unified@1.0.3: + resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==} + peerDependencies: + '@types/lodash-es': '*' + lodash: '*' + lodash-es: '*' + + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + normalize-wheel-es@1.2.0: + resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vue-router@4.6.4: + resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==} + peerDependencies: + vue: ^3.5.0 + + vue@3.5.29: + resolution: {integrity: sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + zrender@5.6.1: + resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==} + +snapshots: + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@ctrl/tinycolor@4.2.0': {} + + '@element-plus/icons-vue@2.3.2(vue@3.5.29)': + dependencies: + vue: 3.5.29 + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/utils@0.2.11': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@sxzz/popperjs-es@2.11.8': {} + + '@types/estree@1.0.8': {} + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.24 + + '@types/lodash@4.17.24': {} + + '@types/web-bluetooth@0.0.20': {} + + '@vitejs/plugin-vue@4.6.2(vite@5.4.21)(vue@3.5.29)': + dependencies: + vite: 5.4.21 + vue: 3.5.29 + + '@vue/compiler-core@3.5.29': + dependencies: + '@babel/parser': 7.29.0 + '@vue/shared': 3.5.29 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.29': + dependencies: + '@vue/compiler-core': 3.5.29 + '@vue/shared': 3.5.29 + + '@vue/compiler-sfc@3.5.29': + dependencies: + '@babel/parser': 7.29.0 + '@vue/compiler-core': 3.5.29 + '@vue/compiler-dom': 3.5.29 + '@vue/compiler-ssr': 3.5.29 + '@vue/shared': 3.5.29 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.8 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.29': + dependencies: + '@vue/compiler-dom': 3.5.29 + '@vue/shared': 3.5.29 + + '@vue/devtools-api@6.6.4': {} + + '@vue/reactivity@3.5.29': + dependencies: + '@vue/shared': 3.5.29 + + '@vue/runtime-core@3.5.29': + dependencies: + '@vue/reactivity': 3.5.29 + '@vue/shared': 3.5.29 + + '@vue/runtime-dom@3.5.29': + dependencies: + '@vue/reactivity': 3.5.29 + '@vue/runtime-core': 3.5.29 + '@vue/shared': 3.5.29 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.29(vue@3.5.29)': + dependencies: + '@vue/compiler-ssr': 3.5.29 + '@vue/shared': 3.5.29 + vue: 3.5.29 + + '@vue/shared@3.5.29': {} + + '@vueuse/core@12.0.0': + dependencies: + '@types/web-bluetooth': 0.0.20 + '@vueuse/metadata': 12.0.0 + '@vueuse/shared': 12.0.0 + vue: 3.5.29 + transitivePeerDependencies: + - typescript + + '@vueuse/metadata@12.0.0': {} + + '@vueuse/shared@12.0.0': + dependencies: + vue: 3.5.29 + transitivePeerDependencies: + - typescript + + async-validator@4.2.5: {} + + asynckit@0.4.0: {} + + axios@1.13.6: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + csstype@3.2.3: {} + + dayjs@1.11.19: {} + + delayed-stream@1.0.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + echarts@5.6.0: + dependencies: + tslib: 2.3.0 + zrender: 5.6.1 + + element-plus@2.13.5(vue@3.5.29): + dependencies: + '@ctrl/tinycolor': 4.2.0 + '@element-plus/icons-vue': 2.3.2(vue@3.5.29) + '@floating-ui/dom': 1.7.6 + '@popperjs/core': '@sxzz/popperjs-es@2.11.8' + '@types/lodash': 4.17.24 + '@types/lodash-es': 4.17.12 + '@vueuse/core': 12.0.0 + async-validator: 4.2.5 + dayjs: 1.11.19 + lodash: 4.17.23 + lodash-es: 4.17.23 + lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.23)(lodash@4.17.23) + memoize-one: 6.0.0 + normalize-wheel-es: 1.2.0 + vue: 3.5.29 + transitivePeerDependencies: + - typescript + + entities@7.0.1: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + estree-walker@2.0.2: {} + + follow-redirects@1.15.11: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + lodash-es@4.17.23: {} + + lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.23)(lodash@4.17.23): + dependencies: + '@types/lodash-es': 4.17.12 + lodash: 4.17.23 + lodash-es: 4.17.23 + + lodash@4.17.23: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + memoize-one@6.0.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + nanoid@3.3.11: {} + + normalize-wheel-es@1.2.0: {} + + picocolors@1.1.1: {} + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + proxy-from-env@1.1.0: {} + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + source-map-js@1.2.1: {} + + tslib@2.3.0: {} + + vite@5.4.21: + dependencies: + esbuild: 0.21.5 + postcss: 8.5.8 + rollup: 4.59.0 + optionalDependencies: + fsevents: 2.3.3 + + vue-router@4.6.4(vue@3.5.29): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.29 + + vue@3.5.29: + dependencies: + '@vue/compiler-dom': 3.5.29 + '@vue/compiler-sfc': 3.5.29 + '@vue/runtime-dom': 3.5.29 + '@vue/server-renderer': 3.5.29(vue@3.5.29) + '@vue/shared': 3.5.29 + + zrender@5.6.1: + dependencies: + tslib: 2.3.0 diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..07ab469 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/frontend/src/api/overview.js b/frontend/src/api/overview.js new file mode 100644 index 0000000..57e8446 --- /dev/null +++ b/frontend/src/api/overview.js @@ -0,0 +1,21 @@ +import request from './request' + +export function getStats() { + return request.get('/overview/stats') +} + +export function getTrend() { + return request.get('/overview/trend') +} + +export function getWeekday() { + return request.get('/overview/weekday') +} + +export function getReasons() { + return request.get('/overview/reasons') +} + +export function getSeasons() { + return request.get('/overview/seasons') +} diff --git a/frontend/src/api/request.js b/frontend/src/api/request.js new file mode 100644 index 0000000..21e6e9d --- /dev/null +++ b/frontend/src/api/request.js @@ -0,0 +1,21 @@ +import axios from 'axios' + +const request = axios.create({ + baseURL: '/api', + timeout: 10000 +}) + +request.interceptors.response.use( + response => { + const res = response.data + if (res.code !== 200) { + return Promise.reject(new Error(res.message || 'Error')) + } + return res.data + }, + error => { + return Promise.reject(error) + } +) + +export default request diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..f0bc866 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,12 @@ +import { createApp } from 'vue' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import App from './App.vue' +import router from './router' + +const app = createApp(App) + +app.use(ElementPlus) +app.use(router) + +app.mount('#app') diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 0000000..4978edc --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,44 @@ +import { createRouter, createWebHistory } from 'vue-router' + +const routes = [ + { + path: '/', + redirect: '/dashboard' + }, + { + path: '/dashboard', + name: 'Dashboard', + component: () => import('@/views/Dashboard.vue'), + meta: { title: '数据概览' } + }, + { + path: '/analysis', + name: 'FactorAnalysis', + component: () => import('@/views/FactorAnalysis.vue'), + meta: { title: '影响因素分析' } + }, + { + path: '/prediction', + name: 'Prediction', + component: () => import('@/views/Prediction.vue'), + meta: { title: '缺勤预测' } + }, + { + path: '/clustering', + name: 'Clustering', + component: () => import('@/views/Clustering.vue'), + meta: { title: '员工画像' } + } +] + +const router = createRouter({ + history: createWebHistory(), + routes +}) + +router.beforeEach((to, from, next) => { + document.title = to.meta.title || '员工缺勤分析与预测系统' + next() +}) + +export default router diff --git a/frontend/src/views/Clustering.vue b/frontend/src/views/Clustering.vue new file mode 100644 index 0000000..a8f89ed --- /dev/null +++ b/frontend/src/views/Clustering.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue new file mode 100644 index 0000000..0ca27da --- /dev/null +++ b/frontend/src/views/Dashboard.vue @@ -0,0 +1,189 @@ + + + + + diff --git a/frontend/src/views/FactorAnalysis.vue b/frontend/src/views/FactorAnalysis.vue new file mode 100644 index 0000000..1c496ea --- /dev/null +++ b/frontend/src/views/FactorAnalysis.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/frontend/src/views/Prediction.vue b/frontend/src/views/Prediction.vue new file mode 100644 index 0000000..14e961d --- /dev/null +++ b/frontend/src/views/Prediction.vue @@ -0,0 +1,398 @@ + + + + + diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..c220f09 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { resolve } from 'path' + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': resolve(__dirname, 'src') + } + }, + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:5000', + changeOrigin: true + } + } + } +})