Files
bike-course-design/analyze_bike_sharing.py
2026-06-08 23:15:05 +08:00

266 lines
11 KiB
Python

import json
from pathlib import Path
import joblib
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.inspection import permutation_importance
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
from sklearn.model_selection import KFold, cross_validate, train_test_split
from sklearn.neural_network import MLPRegressor
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.svm import SVR
BASE_DIR = Path(__file__).resolve().parent
DATA_PATH = Path('/root/.hermes-web-ui/upload/default/28752a235eb6ef9d.xlsx')
OUT_DIR = BASE_DIR / 'outputs'
FIG_DIR = OUT_DIR / 'figures'
MODEL_DIR = OUT_DIR / 'models'
for directory in [OUT_DIR, FIG_DIR, MODEL_DIR]:
directory.mkdir(parents=True, exist_ok=True)
plt.rcParams['font.sans-serif'] = ['Noto Sans CJK SC', 'WenQuanYi Zen Hei', 'SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
sns.set_theme(style='whitegrid')
FEATURES = ['季节', '', '', '节假日', '星期', '工作日', '天气', '气温', '体感温度', '湿度', '风速']
CATEGORICAL_FEATURES = ['季节', '', '', '节假日', '星期', '工作日', '天气']
NUMERIC_FEATURES = ['气温', '体感温度', '湿度', '风速']
TARGET = '总骑行量'
def rmse(y_true, y_pred):
return float(np.sqrt(mean_squared_error(y_true, y_pred)))
def evaluate_model(model, x_train, x_test, y_train, y_test):
model.fit(x_train, y_train)
pred = model.predict(x_test)
return {
'RMSE': rmse(y_test, pred),
'MAE': float(mean_absolute_error(y_test, pred)),
'R2': float(r2_score(y_test, pred)),
}, pred
def make_preprocessor(scale_numeric=False):
numeric_transformer = StandardScaler() if scale_numeric else 'passthrough'
return ColumnTransformer(
transformers=[
('cat', OneHotEncoder(handle_unknown='ignore'), CATEGORICAL_FEATURES),
('num', numeric_transformer, NUMERIC_FEATURES),
],
remainder='drop',
verbose_feature_names_out=False,
)
def make_models():
return {
'线性回归': Pipeline([
('preprocess', make_preprocessor(scale_numeric=True)),
('model', LinearRegression()),
]),
'随机森林': Pipeline([
('preprocess', make_preprocessor()),
('model', RandomForestRegressor(n_estimators=500, random_state=42, min_samples_leaf=2, n_jobs=-1)),
]),
'梯度提升树': Pipeline([
('preprocess', make_preprocessor()),
('model', GradientBoostingRegressor(random_state=42, n_estimators=300, learning_rate=0.05, max_depth=3)),
]),
'支持向量回归': Pipeline([
('preprocess', make_preprocessor(scale_numeric=True)),
('model', SVR(C=200, gamma='scale', epsilon=50)),
]),
'神经网络': Pipeline([
('preprocess', make_preprocessor(scale_numeric=True)),
('model', MLPRegressor(hidden_layer_sizes=(80, 40), activation='relu', alpha=0.001, max_iter=3000, random_state=42, early_stopping=True)),
]),
}
def load_data():
daily = pd.read_excel(DATA_PATH, sheet_name='每天数据')
future = pd.read_excel(DATA_PATH, sheet_name='预测集')
daily['日期'] = pd.to_datetime(daily['日期'])
future['日期'] = pd.to_datetime(future['日期'])
return daily, future
def feature_importance(best_model, x_test, y_test):
model_name = type(best_model.named_steps['model']).__name__
if hasattr(best_model.named_steps['model'], 'feature_importances_'):
transformed_names = best_model.named_steps['preprocess'].get_feature_names_out()
raw_importances = pd.Series(best_model.named_steps['model'].feature_importances_, index=transformed_names)
collapsed = {}
for feature in FEATURES:
collapsed[feature] = raw_importances[[idx == feature or idx.startswith(feature + '_') for idx in raw_importances.index]].sum()
importance = pd.Series(collapsed).sort_values(ascending=False)
method = f'{model_name} 内置重要性'
else:
result = permutation_importance(best_model, x_test, y_test, n_repeats=30, random_state=42, scoring='neg_root_mean_squared_error', n_jobs=-1)
importance = pd.Series(result.importances_mean, index=FEATURES).sort_values(ascending=False)
method = '排列重要性'
return importance, method
def save_figures(daily, y_test, y_pred, residuals, importance, future_pred_df):
plt.figure(figsize=(12, 6))
sns.lineplot(data=daily, x='日期', y=TARGET, color='#2563eb')
plt.title('2011-2012 年共享单车每日总骑行量趋势')
plt.xlabel('日期')
plt.ylabel('总骑行量')
plt.tight_layout()
plt.savefig(FIG_DIR / 'historical_trend.png', dpi=180)
plt.close()
plt.figure(figsize=(8, 6))
sns.scatterplot(x=y_test, y=y_pred, color='#16a34a', edgecolor='white')
low, high = min(y_test.min(), y_pred.min()), max(y_test.max(), y_pred.max())
plt.plot([low, high], [low, high], '--', color='#ef4444', linewidth=1.5)
plt.title('测试集真实值-预测值散点图')
plt.xlabel('真实总骑行量')
plt.ylabel('预测总骑行量')
plt.tight_layout()
plt.savefig(FIG_DIR / 'actual_vs_predicted.png', dpi=180)
plt.close()
plt.figure(figsize=(8, 5))
sns.histplot(residuals, kde=True, color='#f97316')
plt.axvline(0, color='#111827', linestyle='--', linewidth=1)
plt.title('测试集残差分布')
plt.xlabel('残差(真实值 - 预测值)')
plt.ylabel('频数')
plt.tight_layout()
plt.savefig(FIG_DIR / 'residual_distribution.png', dpi=180)
plt.close()
plt.figure(figsize=(9, 6))
plot_imp = importance.sort_values(ascending=True)
plt.barh(plot_imp.index, plot_imp.values, color='#7c3aed')
plt.title('特征重要性排序')
plt.xlabel('重要性')
plt.tight_layout()
plt.savefig(FIG_DIR / 'feature_importance.png', dpi=180)
plt.close()
plt.figure(figsize=(12, 6))
sns.lineplot(data=future_pred_df, x='日期', y='预测总骑行量', marker='o', color='#0891b2')
plt.title('2013 年前 100 天共享单车骑行量预测趋势')
plt.xlabel('日期')
plt.ylabel('预测总骑行量')
plt.tight_layout()
plt.savefig(FIG_DIR / 'future_prediction_trend.png', dpi=180)
plt.close()
def main():
daily, future = load_data()
missing = daily[FEATURES + [TARGET]].isna().sum().to_dict()
duplicates = int(daily.duplicated(subset=['日期']).sum())
x = daily[FEATURES]
y = daily[TARGET]
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=42)
models = make_models()
initial_rows = []
predictions = {}
for name, model in models.items():
metrics, pred = evaluate_model(model, x_train, x_test, y_train, y_test)
initial_rows.append({'模型': name, **metrics})
predictions[name] = pred
joblib.dump(model, MODEL_DIR / f'{name}.pkl')
initial_df = pd.DataFrame(initial_rows).sort_values('RMSE')
best_name = initial_df.iloc[0]['模型']
best_model = models[best_name]
best_pred = predictions[best_name]
residuals = y_test.to_numpy() - best_pred
cv = KFold(n_splits=5, shuffle=True, random_state=42)
cv_result = cross_validate(
best_model,
x,
y,
cv=cv,
scoring={'rmse': 'neg_root_mean_squared_error', 'mae': 'neg_mean_absolute_error', 'r2': 'r2'},
n_jobs=-1,
)
cv_summary = {
'模型': best_name,
'RMSE均值': float(-cv_result['test_rmse'].mean()),
'RMSE标准差': float(cv_result['test_rmse'].std()),
'MAE均值': float(-cv_result['test_mae'].mean()),
'MAE标准差': float(cv_result['test_mae'].std()),
'R2均值': float(cv_result['test_r2'].mean()),
'R2标准差': float(cv_result['test_r2'].std()),
}
importance, importance_method = feature_importance(best_model, x_test, y_test)
future_pred = best_model.predict(future[FEATURES])
future_pred_df = future.copy()
future_pred_df['预测总骑行量'] = np.maximum(np.round(future_pred), 0).astype(int)
save_figures(daily, y_test, best_pred, residuals, importance, future_pred_df)
initial_df.to_csv(OUT_DIR / 'model_comparison.csv', index=False, encoding='utf-8-sig')
pd.DataFrame([cv_summary]).to_csv(OUT_DIR / 'cross_validation_summary.csv', index=False, encoding='utf-8-sig')
importance.rename('重要性').to_csv(OUT_DIR / 'feature_importance.csv', encoding='utf-8-sig')
future_pred_df.to_excel(OUT_DIR / '2013前100天骑行量预测结果.xlsx', index=False)
test_pred_df = pd.DataFrame({
'日期': daily.loc[x_test.index, '日期'].values,
'真实总骑行量': y_test.values,
'预测总骑行量': np.round(best_pred).astype(int),
'残差': np.round(residuals, 2),
}).sort_values('日期')
test_pred_df.to_csv(OUT_DIR / 'test_set_predictions.csv', index=False, encoding='utf-8-sig')
joblib.dump(best_model, MODEL_DIR / 'best_bike_demand_model.pkl')
summary = {
'data': {
'rows': int(len(daily)),
'columns': int(daily.shape[1]),
'future_rows': int(len(future)),
'missing': missing,
'duplicate_dates': duplicates,
'target_min': int(y.min()),
'target_max': int(y.max()),
'target_mean': float(y.mean()),
},
'initial_comparison': initial_df.to_dict(orient='records'),
'best_model': best_name,
'best_test_metrics': initial_df.iloc[0].to_dict(),
'cross_validation': cv_summary,
'importance_method': importance_method,
'top_features': importance.head(8).to_dict(),
'future_prediction': {
'min': int(future_pred_df['预测总骑行量'].min()),
'max': int(future_pred_df['预测总骑行量'].max()),
'mean': float(future_pred_df['预测总骑行量'].mean()),
},
'outputs': {
'figures': [str(p) for p in sorted(FIG_DIR.glob('*.png'))],
'best_model': str(MODEL_DIR / 'best_bike_demand_model.pkl'),
'prediction_excel': str(OUT_DIR / '2013前100天骑行量预测结果.xlsx'),
}
}
(OUT_DIR / 'analysis_summary.json').write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding='utf-8')
print(json.dumps(summary, ensure_ascii=False, indent=2))
if __name__ == '__main__':
main()