feat: 初始化员工缺勤分析系统项目
搭建完整的前后端分离架构,实现数据概览、预测分析、聚类分析等核心功能模块 详细版: feat: 初始化员工缺勤分析系统项目 - 后端:基于 Flask 搭建 RESTful API,包含数据概览、特征分析、预测模型、聚类分析四大模块 - 前端:基于 Vue.js 构建单页应用,实现 Dashboard、预测、聚类、因子分析等页面 - 模型:集成随机森林、XGBoost、LightGBM、Stacking 等多种机器学习模型 - 文档:完成需求规格说明、系统架构设计、接口设计、数据设计、UI原型设计等文档
This commit is contained in:
69
backend/.gitignore
vendored
Normal file
69
backend/.gitignore
vendored
Normal file
@@ -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
|
||||
11
backend/api/__init__.py
Normal file
11
backend/api/__init__.py
Normal file
@@ -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)
|
||||
64
backend/api/analysis_routes.py
Normal file
64
backend/api/analysis_routes.py
Normal file
@@ -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
|
||||
68
backend/api/cluster_routes.py
Normal file
68
backend/api/cluster_routes.py
Normal file
@@ -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
|
||||
90
backend/api/overview_routes.py
Normal file
90
backend/api/overview_routes.py
Normal file
@@ -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
|
||||
102
backend/api/predict_routes.py
Normal file
102
backend/api/predict_routes.py
Normal file
@@ -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
|
||||
68
backend/app.py
Normal file
68
backend/app.py
Normal file
@@ -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)
|
||||
148
backend/config.py
Normal file
148
backend/config.py
Normal file
@@ -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': '缺勤时长'
|
||||
}
|
||||
4
backend/core/__init__.py
Normal file
4
backend/core/__init__.py
Normal file
@@ -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
|
||||
229
backend/core/clustering.py
Normal file
229
backend/core/clustering.py
Normal file
@@ -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()
|
||||
151
backend/core/feature_mining.py
Normal file
151
backend/core/feature_mining.py
Normal file
@@ -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)
|
||||
105
backend/core/preprocessing.py
Normal file
105
backend/core/preprocessing.py
Normal file
@@ -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())
|
||||
590
backend/core/train_model.py
Normal file
590
backend/core/train_model.py
Normal file
@@ -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()
|
||||
741
backend/data/raw/Absenteeism_at_work.csv
Normal file
741
backend/data/raw/Absenteeism_at_work.csv
Normal file
@@ -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
|
||||
|
16
backend/requirements.txt
Normal file
16
backend/requirements.txt
Normal file
@@ -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
|
||||
4
backend/services/__init__.py
Normal file
4
backend/services/__init__.py
Normal file
@@ -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
|
||||
119
backend/services/analysis_service.py
Normal file
119
backend/services/analysis_service.py
Normal file
@@ -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()
|
||||
18
backend/services/cluster_service.py
Normal file
18
backend/services/cluster_service.py
Normal file
@@ -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()
|
||||
162
backend/services/data_service.py
Normal file
162
backend/services/data_service.py
Normal file
@@ -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()
|
||||
373
backend/services/predict_service.py
Normal file
373
backend/services/predict_service.py
Normal file
@@ -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()
|
||||
1
backend/utils/__init__.py
Normal file
1
backend/utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .common import format_response, format_error
|
||||
14
backend/utils/common.py
Normal file
14
backend/utils/common.py
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user