commit 0026c4bf849c31863d44dcf835980b296362430c Author: luochen570 <1160510664@qq.com> Date: Mon Jun 8 23:15:05 2026 +0800 feat: 完成共享单车课程设计 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..70a3873 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.venv/ +__pycache__/ +*.py[cod] +*.zip +*.tar.gz +outputs/rerun.log +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..c768c57 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# 共享单车日骑行量预测与可视化系统 + +## 项目内容 + +本项目完成《大数据分析技术》课程设计中“基于机器学习的共享单车日骑行量预测与可视化系统”的全部任务: + +1. 多模型构建与初步筛选 +2. 特征重要性解析 +3. 模型评估与 2013 年前 100 天预测输出 +4. 轻量级 GUI 预测可视化系统 + +## 目录说明 + +- `analyze_bike_sharing.py`:完整数据分析、建模、评估、预测、图表生成脚本 +- `bike_demand_gui.py`:tkinter 轻量级预测系统 +- `generate_report.py`:课程设计报告生成脚本 +- `outputs/model_comparison.csv`:模型对比结果 +- `outputs/cross_validation_summary.csv`:交叉验证结果 +- `outputs/feature_importance.csv`:特征重要性 +- `outputs/2013前100天骑行量预测结果.xlsx`:最终预测结果 +- `outputs/models/best_bike_demand_model.pkl`:最佳模型文件 +- `outputs/figures/`:报告图表 +- `outputs/共享单车日骑行量预测课程设计报告.docx`:课程设计报告 + +## 运行环境 + +```bash +cd /root/bike_course_design +python3 -m venv .venv +.venv/bin/pip install pymupdf openpyxl pandas matplotlib seaborn scikit-learn python-docx joblib +``` + +## 重新生成分析结果 + +```bash +cd /root/bike_course_design +.venv/bin/python analyze_bike_sharing.py +``` + +## 启动 GUI + +```bash +cd /root/bike_course_design +.venv/bin/python bike_demand_gui.py +``` + +GUI 支持输入季节、年月、节假日、星期、工作日、天气、气温、体感温度、湿度、风速等特征,输出单场景预测值,并可加入多场景对比表。 + +## 本次运行核心结论 + +- 数据量:731 条历史日度记录,预测集 100 条记录。 +- 最佳模型:梯度提升树。 +- 测试集指标:RMSE 约 651.14,MAE 约 449.00,R² 约 0.8943。 +- 五折交叉验证:RMSE 均值约 655.12,R² 均值约 0.8819。 +- 关键特征:气温、年份、季节、体感温度、湿度。 diff --git a/analyze_bike_sharing.py b/analyze_bike_sharing.py new file mode 100644 index 0000000..f3566c8 --- /dev/null +++ b/analyze_bike_sharing.py @@ -0,0 +1,265 @@ +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() diff --git a/bike_demand_gui.py b/bike_demand_gui.py new file mode 100644 index 0000000..8196e7c --- /dev/null +++ b/bike_demand_gui.py @@ -0,0 +1,129 @@ +import tkinter as tk +from tkinter import ttk, messagebox +from pathlib import Path + +import joblib +import pandas as pd + +BASE_DIR = Path(__file__).resolve().parent +MODEL_PATH = BASE_DIR / 'outputs' / 'models' / 'best_bike_demand_model.pkl' +FEATURES = ['季节', '年', '月', '节假日', '星期', '工作日', '天气', '气温', '体感温度', '湿度', '风速'] + + +class BikeDemandApp: + def __init__(self, root): + self.root = root + self.root.title('共享单车日骑行量预测系统') + self.root.geometry('820x560') + self.model = joblib.load(MODEL_PATH) + self.entries = {} + self.history = [] + self.build_ui() + + def build_ui(self): + container = ttk.Frame(self.root, padding=16) + container.pack(fill=tk.BOTH, expand=True) + + left = ttk.LabelFrame(container, text='输入特征', padding=12) + left.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 12)) + + fields = [ + ('季节', '1=春 2=夏 3=秋 4=冬', '1'), + ('年', '0=2011 1=2012 2=2013', '2'), + ('月', '1~12', '1'), + ('节假日', '0=否 1=是', '0'), + ('星期', '0=周日 ... 6=周六', '1'), + ('工作日', '0=否 1=是', '1'), + ('天气', '1=晴好 2=多云 3=小雨雪 4=恶劣', '1'), + ('气温', '归一化 0~1', '0.32'), + ('体感温度', '归一化 0~1', '0.34'), + ('湿度', '归一化 0~1', '0.70'), + ('风速', '归一化 0~1', '0.15'), + ] + for row, (name, hint, default) in enumerate(fields): + ttk.Label(left, text=name).grid(row=row, column=0, sticky='w', pady=4) + entry = ttk.Entry(left, width=14) + entry.insert(0, default) + entry.grid(row=row, column=1, sticky='ew', pady=4) + ttk.Label(left, text=hint, foreground='#666').grid(row=row, column=2, sticky='w', padx=6) + self.entries[name] = entry + + ttk.Button(left, text='预测单场景', command=self.predict_one).grid(row=len(fields), column=0, columnspan=3, sticky='ew', pady=(12, 4)) + ttk.Button(left, text='加入对比列表', command=self.add_to_history).grid(row=len(fields) + 1, column=0, columnspan=3, sticky='ew', pady=4) + ttk.Button(left, text='清空对比列表', command=self.clear_history).grid(row=len(fields) + 2, column=0, columnspan=3, sticky='ew', pady=4) + + right = ttk.Frame(container) + right.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True) + + result_box = ttk.LabelFrame(right, text='预测结果', padding=16) + result_box.pack(fill=tk.X) + self.result_var = tk.StringVar(value='请输入特征后点击预测') + ttk.Label(result_box, textvariable=self.result_var, font=('Arial', 22, 'bold'), foreground='#0f766e').pack(anchor='w') + self.level_var = tk.StringVar(value='') + ttk.Label(result_box, textvariable=self.level_var, font=('Arial', 12)).pack(anchor='w', pady=(8, 0)) + + table_box = ttk.LabelFrame(right, text='多场景对比', padding=8) + table_box.pack(fill=tk.BOTH, expand=True, pady=(12, 0)) + columns = ('序号', '月', '天气', '气温', '工作日', '预测总骑行量') + self.tree = ttk.Treeview(table_box, columns=columns, show='headings', height=12) + for col in columns: + self.tree.heading(col, text=col) + self.tree.column(col, width=90, anchor='center') + self.tree.pack(fill=tk.BOTH, expand=True) + + def collect_features(self): + values = {} + for feature, entry in self.entries.items(): + try: + values[feature] = float(entry.get()) + except ValueError as exc: + raise ValueError(f'{feature} 必须是数字') from exc + for feature in ['季节', '年', '月', '节假日', '星期', '工作日', '天气']: + values[feature] = int(values[feature]) + for feature in ['气温', '体感温度', '湿度', '风速']: + if not 0 <= values[feature] <= 1: + raise ValueError(f'{feature} 应在 0~1 之间') + return values + + def predict_value(self): + values = self.collect_features() + frame = pd.DataFrame([values], columns=FEATURES) + prediction = int(max(round(self.model.predict(frame)[0]), 0)) + return values, prediction + + def predict_one(self): + try: + _, prediction = self.predict_value() + except Exception as exc: + messagebox.showerror('输入错误', str(exc)) + return + self.result_var.set(f'预计日骑行量:{prediction:,} 次') + if prediction >= 5000: + level = '需求等级:高,需要提前调度车辆。' + elif prediction >= 3000: + level = '需求等级:中,维持常规运维。' + else: + level = '需求等级:低,可减少热点补车频率。' + self.level_var.set(level) + + def add_to_history(self): + try: + values, prediction = self.predict_value() + except Exception as exc: + messagebox.showerror('输入错误', str(exc)) + return + self.history.append((values, prediction)) + idx = len(self.history) + self.tree.insert('', 'end', values=(idx, values['月'], values['天气'], values['气温'], values['工作日'], prediction)) + self.predict_one() + + def clear_history(self): + self.history.clear() + for item in self.tree.get_children(): + self.tree.delete(item) + + +if __name__ == '__main__': + root = tk.Tk() + app = BikeDemandApp(root) + root.mainloop() diff --git a/course_design_requirements.txt b/course_design_requirements.txt new file mode 100644 index 0000000..19e80c9 --- /dev/null +++ b/course_design_requirements.txt @@ -0,0 +1,64 @@ +6 +三、基于机器学习的共享单车日骑行量预测与可视化系统 +1. 课题背景 +在构建绿色、低碳城市的进程中,“最后一公里”出行难题一直是阻碍公共交通 +高效利用的关键瓶颈。共享单车作为一种创新的分时租赁模式,有效填补了这一空 +白,不仅提升了居民使用公共交通的整体意愿,还促进了绿色出行理念的普及。然 +而,共享单车的运营效率高度依赖于对用户需求的精准预判。骑行需求受多种复杂 +因素影响,如天气状况、季节变化、是否为工作日或节假日等。因此,利用历史运 +营数据,通过数据挖掘与机器学习技术建立精准的需求预测模型,对于优化车辆调 +度、提升用户体验及实现企业精细化运营具有重要的现实意义。 +2. 数据描述 +本课题采用的数据集源自美国华盛顿特区的Capital Bikeshare 系统,是UCI 机器 +学习库中的经典数据集(Bike Sharing Dataset)。我们选用其中的日度统计数据,见“共 +享单车骑行量数据.xlsx”的第一张工作表“每天数据”,数据集共包含731 条记录,覆 +盖了2011 年与2012 年两年完整的骑行数据,共16 列,各列含义见第二张工作表“特 +征说明”。 +此外,数据集还提供了待预测的2013 年前100 天的相关特征,见第三张工作表 +“预测集”,用于最终模型的骑行量预测。 +3. 设计任务 +本课题围绕共享单车每日总骑行量预测展开,要求完成“模型构建与对比→特征 +影响剖析→性能评估与预测→可视化系统开发”的完整技术研究流程,全面锻炼数据 +建模、特征分析、模型优化及GUI 开发的综合能力。 +任务一:多模型构建与初步筛选 +目标:建立包含基线模型和候选模型的完整模型池,为后续分析提供严谨的性 +能基准。 +核心步骤: +(1)数据准备:从数据集第一张工作表中提取特征(12 维)与目标变量,并将 +数据集划分为训练集和测试集; +(2)模型选型: +基线模型:选择线性回归模型作为基准; +候选模型:至少选择两类非线性回归模型(如树模型+ 核方法/神经网络),体 +现方法多样性; +(3)基础训练:对每类模型使用默认或合理初始参数进行训练; +(4)对比:在验证集上计算各模型的RMSE/MAE/R²,形成初步性能排序; +(5)输出:保存各模型对象及初步评估结果,为任务三的深入评估和任务二的 +特征分析提供候选模型池。 + +7 +任务二:特征重要性解析 +目标:挖掘影响骑行量的关键因子,解释模型决策逻辑。 +核心步骤: +(1)选定最优模型:基于任务一结果,选取性能最佳模型; +(2)量化与可视化:通过内置属性(树模型)或排列重要性(其他模型)计算 +特征贡献,绘制条形图; +(3)业务解读:结合领域知识分析关键特征的影响机制; +(4)输出:特征重要性报告,为模型精简提供依据。 +任务三:模型评估与预测输出 +目标:验证模型泛化能力,生成最终预测结果。 +核心步骤: +(1)全面评估:通过交叉验证计算RMSE/MAE/R²,绘制真实值-预测值散点图 +及残差分布; +(2)确定最优模型:综合指标与稳定性,选定部署模型; +(3)预测与可视化:基于2013 年前100 天特征数据生成预测值,输出表格及 +趋势折线图; +(4)输出:保存模型文件(如.pkl)和预测结果,作为GUI 系统的数据源。 +任务四:轻量级预测可视化系统开发 +目标:实现分析成果的交互式应用。 +核心步骤: +(1)框架选型:采用轻量GUI 工具(如Streamlit、tkinter、PyQT); +(2)界面设计:左侧输入区(日期、天气、温度等)+右侧结果展示区; +(3)模型集成:加载最优模型,编写特征处理与预测函数; +(4)动态可视化:让用户直观理解预测结果,同时支持单场景查询(数字+仪 +表盘)和多场景对比(折线图)两种核心需求; +(5)优化与输出:实现输入校验与实时响应,交付可运行程序及使用说明。 diff --git a/generate_report.py b/generate_report.py new file mode 100644 index 0000000..e89496f --- /dev/null +++ b/generate_report.py @@ -0,0 +1,124 @@ +import json +from pathlib import Path + +import pandas as pd +from docx import Document +from docx.enum.text import WD_ALIGN_PARAGRAPH +from docx.shared import Inches, Pt + +BASE_DIR = Path(__file__).resolve().parent +OUT_DIR = BASE_DIR / 'outputs' +FIG_DIR = OUT_DIR / 'figures' +REPORT_PATH = OUT_DIR / '共享单车日骑行量预测课程设计报告.docx' + + +def add_heading(document, text, level): + heading = document.add_heading(text, level=level) + for run in heading.runs: + run.font.name = '宋体' + return heading + + +def add_paragraph(document, text): + paragraph = document.add_paragraph(text) + paragraph.paragraph_format.first_line_indent = Pt(24) + paragraph.paragraph_format.line_spacing = 1.5 + for run in paragraph.runs: + run.font.name = '宋体' + run.font.size = Pt(11) + return paragraph + + +def add_table_from_df(document, df, title=None): + if title: + add_paragraph(document, title) + table = document.add_table(rows=1, cols=len(df.columns)) + table.style = 'Table Grid' + hdr = table.rows[0].cells + for i, col in enumerate(df.columns): + hdr[i].text = str(col) + for _, row in df.iterrows(): + cells = table.add_row().cells + for i, value in enumerate(row): + if isinstance(value, float): + cells[i].text = f'{value:.4f}' + else: + cells[i].text = str(value) + document.add_paragraph('') + + +def main(): + summary = json.loads((OUT_DIR / 'analysis_summary.json').read_text(encoding='utf-8')) + comparison = pd.read_csv(OUT_DIR / 'model_comparison.csv') + cv = pd.read_csv(OUT_DIR / 'cross_validation_summary.csv') + importance = pd.read_csv(OUT_DIR / 'feature_importance.csv') + future = pd.read_excel(OUT_DIR / '2013前100天骑行量预测结果.xlsx') + + document = Document() + section = document.sections[0] + section.top_margin = Inches(0.8) + section.bottom_margin = Inches(0.8) + section.left_margin = Inches(0.9) + section.right_margin = Inches(0.9) + + title = document.add_heading('基于机器学习的共享单车日骑行量预测与可视化系统', 0) + title.alignment = WD_ALIGN_PARAGRAPH.CENTER + subtitle = document.add_paragraph('《大数据分析技术》课程设计报告') + subtitle.alignment = WD_ALIGN_PARAGRAPH.CENTER + document.add_paragraph('') + + add_heading(document, '一、课题背景与研究目标', 1) + add_paragraph(document, '共享单车是解决城市公共交通“最后一公里”问题的重要方式。其运营效率依赖于车辆供需匹配能力,而日骑行量会受到季节、天气、温度、湿度、风速、工作日等多因素共同影响。本课程设计以 Capital Bikeshare 日度数据为基础,构建机器学习模型预测共享单车每日总骑行量,并形成可视化分析结果与轻量级预测系统。') + add_paragraph(document, '本项目围绕“模型构建与对比、特征影响剖析、性能评估与预测、GUI 系统开发”四项任务展开,最终输出模型文件、评估结果、预测表格、图表和可运行程序。') + + add_heading(document, '二、数据说明与预处理', 1) + data = summary['data'] + add_paragraph(document, f'历史数据来自 Excel 第一张工作表“每天数据”,共 {data["rows"]} 条记录、{data["columns"]} 列,覆盖 2011—2012 年完整日度骑行数据;预测集来自第三张工作表“预测集”,包含 2013 年前 {data["future_rows"]} 天的待预测特征。') + add_paragraph(document, f'目标变量为“总骑行量”,历史样本最小值为 {data["target_min"]},最大值为 {data["target_max"]},均值为 {data["target_mean"]:.2f}。经检查,建模字段不存在缺失值,日期字段无重复记录。') + add_paragraph(document, '建模特征选取季节、年、月、节假日、星期、工作日、天气、气温、体感温度、湿度、风速等 11 个字段。类别型特征采用 One-Hot 编码,数值型特征在对线性回归、SVR 和神经网络建模时进行标准化处理。') + document.add_picture(str(FIG_DIR / 'historical_trend.png'), width=Inches(6.2)) + + add_heading(document, '三、多模型构建与初步筛选', 1) + add_paragraph(document, '按照课程设计要求,模型池包含一个基线模型和多类非线性候选模型。基线模型采用线性回归;候选模型包括随机森林、梯度提升树、支持向量回归和神经网络,覆盖树模型、核方法和神经网络方法。数据按 8:2 划分训练集和测试集,使用 RMSE、MAE、R² 进行比较。') + add_table_from_df(document, comparison, '表 1 模型初步对比结果') + best = summary['best_test_metrics'] + add_paragraph(document, f'从测试集结果看,{summary["best_model"]} 的综合表现最佳,RMSE={best["RMSE"]:.2f},MAE={best["MAE"]:.2f},R²={best["R2"]:.4f}。因此后续特征解释、交叉验证与 2013 年预测均以该模型作为最终模型。') + + add_heading(document, '四、特征重要性解析', 1) + add_paragraph(document, f'由于最优模型为梯度提升树,项目采用模型内置特征重要性计算特征贡献,并将 One-Hot 后的同源类别特征聚合回原始字段。特征重要性结果显示,气温、年、季节、体感温度、湿度对骑行量影响最大。') + document.add_picture(str(FIG_DIR / 'feature_importance.png'), width=Inches(6.2)) + add_table_from_df(document, importance.head(8), '表 2 前 8 个重要特征') + add_paragraph(document, '业务上,气温和体感温度直接影响用户骑行意愿,舒适天气下出行需求更高;年份特征反映共享单车系统规模和用户习惯随时间增长;季节与月份体现周期性需求变化;湿度、风速和天气体现不良气象条件对骑行行为的抑制作用。') + + add_heading(document, '五、模型全面评估', 1) + add_paragraph(document, '为了验证模型泛化能力,对最佳模型进行五折交叉验证,并绘制测试集真实值-预测值散点图和残差分布图。') + add_table_from_df(document, cv, '表 3 五折交叉验证结果') + cv_row = summary['cross_validation'] + add_paragraph(document, f'交叉验证 RMSE 均值为 {cv_row["RMSE均值"]:.2f},标准差为 {cv_row["RMSE标准差"]:.2f};R² 均值为 {cv_row["R2均值"]:.4f},说明模型在不同数据划分下较稳定。') + document.add_picture(str(FIG_DIR / 'actual_vs_predicted.png'), width=Inches(5.8)) + document.add_picture(str(FIG_DIR / 'residual_distribution.png'), width=Inches(5.8)) + add_paragraph(document, '真实值-预测值散点大体分布在对角线附近,说明模型能较好拟合总体变化趋势。残差分布集中在 0 附近,表明模型不存在明显系统性偏差,但在极端低需求或高需求日期仍可能出现较大误差。') + + add_heading(document, '六、2013 年前 100 天预测结果', 1) + fp = summary['future_prediction'] + add_paragraph(document, f'使用最终梯度提升树模型对预测集进行推断,2013 年前 100 天预测骑行量最小值为 {fp["min"]},最大值为 {fp["max"]},均值为 {fp["mean"]:.2f}。完整结果已保存为 Excel 文件“2013前100天骑行量预测结果.xlsx”。') + document.add_picture(str(FIG_DIR / 'future_prediction_trend.png'), width=Inches(6.2)) + sample_future = future[['日期', '季节', '月', '天气', '气温', '湿度', '预测总骑行量']].head(10).copy() + sample_future['日期'] = sample_future['日期'].dt.strftime('%Y-%m-%d') + add_table_from_df(document, sample_future, '表 4 预测结果样例(前 10 天)') + + add_heading(document, '七、轻量级预测可视化系统', 1) + add_paragraph(document, '系统采用 Python tkinter 开发,程序文件为 bike_demand_gui.py。界面左侧提供季节、年份、月份、节假日、星期、工作日、天气、气温、体感温度、湿度、风速等输入项,右侧展示单场景预测结果与多场景对比表。') + add_paragraph(document, '系统启动时加载 outputs/models/best_bike_demand_model.pkl,并对用户输入进行数字类型和归一化范围校验。用户可点击“预测单场景”获得预计日骑行量,也可将多组输入加入对比列表,用于比较不同天气或温度条件下的需求变化。') + add_paragraph(document, '运行方式:在项目目录执行 `.venv/bin/python bike_demand_gui.py`。') + + add_heading(document, '八、结论', 1) + add_paragraph(document, '本课程设计完成了从数据读取、预处理、模型训练、模型比较、特征解释、交叉验证、未来预测到 GUI 系统开发的完整流程。实验结果表明,梯度提升树在该共享单车日骑行量预测任务中优于线性回归、随机森林、SVR 和神经网络,能够较好刻画天气、季节和时间因素与骑行需求之间的非线性关系。') + add_paragraph(document, '后续可进一步引入节假日类型、降水量、重大活动、站点级空间信息等外部变量,并采用时间序列交叉验证或集成学习优化模型,以提升极端天气和异常日期下的预测鲁棒性。') + + document.save(REPORT_PATH) + print(REPORT_PATH) + + +if __name__ == '__main__': + main() diff --git a/outputs/2013前100天骑行量预测结果.xlsx b/outputs/2013前100天骑行量预测结果.xlsx new file mode 100644 index 0000000..58232f8 Binary files /dev/null and b/outputs/2013前100天骑行量预测结果.xlsx differ diff --git a/outputs/analysis_summary.json b/outputs/analysis_summary.json new file mode 100644 index 0000000..39b73e5 --- /dev/null +++ b/outputs/analysis_summary.json @@ -0,0 +1,100 @@ +{ + "data": { + "rows": 731, + "columns": 16, + "future_rows": 100, + "missing": { + "季节": 0, + "年": 0, + "月": 0, + "节假日": 0, + "星期": 0, + "工作日": 0, + "天气": 0, + "气温": 0, + "体感温度": 0, + "湿度": 0, + "风速": 0, + "总骑行量": 0 + }, + "duplicate_dates": 0, + "target_min": 22, + "target_max": 8714, + "target_mean": 4504.3488372093025 + }, + "initial_comparison": [ + { + "模型": "梯度提升树", + "RMSE": 651.1440567037434, + "MAE": 448.99523765137667, + "R2": 0.8942640825672749 + }, + { + "模型": "随机森林", + "RMSE": 703.9966504917254, + "MAE": 450.0664823777129, + "R2": 0.8764025354772573 + }, + { + "模型": "支持向量回归", + "RMSE": 738.1033329574317, + "MAE": 529.5340809792003, + "R2": 0.8641365279788986 + }, + { + "模型": "线性回归", + "RMSE": 796.4617765623753, + "MAE": 583.0197588300691, + "R2": 0.8418029967286178 + }, + { + "模型": "神经网络", + "RMSE": 809.7923656712804, + "MAE": 609.9584660642089, + "R2": 0.836463110808386 + } + ], + "best_model": "梯度提升树", + "best_test_metrics": { + "模型": "梯度提升树", + "RMSE": 651.1440567037434, + "MAE": 448.99523765137667, + "R2": 0.8942640825672749 + }, + "cross_validation": { + "模型": "梯度提升树", + "RMSE均值": 655.1166606641779, + "RMSE标准差": 42.75460340121525, + "MAE均值": 466.577603572732, + "MAE标准差": 14.719481812429436, + "R2均值": 0.8818566774188483, + "R2标准差": 0.023087453303387278 + }, + "importance_method": "GradientBoostingRegressor 内置重要性", + "top_features": { + "气温": 0.38973625494728426, + "年": 0.2984069767787937, + "季节": 0.0932052430981216, + "体感温度": 0.07101028400532663, + "湿度": 0.05891447646010377, + "天气": 0.029590235728090393, + "风速": 0.026177907657402667, + "月": 0.01607120463407045 + }, + "future_prediction": { + "min": 1319, + "max": 6025, + "mean": 3775.15 + }, + "outputs": { + "figures": [ + "/root/bike_course_design/outputs/figures/actual_vs_predicted.png", + "/root/bike_course_design/outputs/figures/feature_importance.png", + "/root/bike_course_design/outputs/figures/future_prediction_trend.png", + "/root/bike_course_design/outputs/figures/historical_trend.png", + "/root/bike_course_design/outputs/figures/residual_distribution.png" + ], + "best_model": "/root/bike_course_design/outputs/models/best_bike_demand_model.pkl", + "prediction_excel": "/root/bike_course_design/outputs/2013前100天骑行量预测结果.xlsx" + } +} \ No newline at end of file diff --git a/outputs/cross_validation_summary.csv b/outputs/cross_validation_summary.csv new file mode 100644 index 0000000..4b6ba77 --- /dev/null +++ b/outputs/cross_validation_summary.csv @@ -0,0 +1,2 @@ +模型,RMSE均值,RMSE标准差,MAE均值,MAE标准差,R2均值,R2标准差 +梯度提升树,655.1166606641779,42.75460340121525,466.577603572732,14.719481812429436,0.8818566774188483,0.023087453303387278 diff --git a/outputs/feature_importance.csv b/outputs/feature_importance.csv new file mode 100644 index 0000000..feb5818 --- /dev/null +++ b/outputs/feature_importance.csv @@ -0,0 +1,12 @@ +,重要性 +气温,0.38973625494728426 +年,0.2984069767787937 +季节,0.0932052430981216 +体感温度,0.07101028400532663 +湿度,0.05891447646010377 +天气,0.029590235728090393 +风速,0.026177907657402667 +月,0.01607120463407045 +星期,0.00832340663049113 +工作日,0.004855667817542203 +节假日,0.003708342242773187 diff --git a/outputs/figures/actual_vs_predicted.png b/outputs/figures/actual_vs_predicted.png new file mode 100644 index 0000000..718e661 Binary files /dev/null and b/outputs/figures/actual_vs_predicted.png differ diff --git a/outputs/figures/feature_importance.png b/outputs/figures/feature_importance.png new file mode 100644 index 0000000..5db2562 Binary files /dev/null and b/outputs/figures/feature_importance.png differ diff --git a/outputs/figures/future_prediction_trend.png b/outputs/figures/future_prediction_trend.png new file mode 100644 index 0000000..dff65c5 Binary files /dev/null and b/outputs/figures/future_prediction_trend.png differ diff --git a/outputs/figures/historical_trend.png b/outputs/figures/historical_trend.png new file mode 100644 index 0000000..717d547 Binary files /dev/null and b/outputs/figures/historical_trend.png differ diff --git a/outputs/figures/residual_distribution.png b/outputs/figures/residual_distribution.png new file mode 100644 index 0000000..0df497d Binary files /dev/null and b/outputs/figures/residual_distribution.png differ diff --git a/outputs/model_comparison.csv b/outputs/model_comparison.csv new file mode 100644 index 0000000..ed99b08 --- /dev/null +++ b/outputs/model_comparison.csv @@ -0,0 +1,6 @@ +模型,RMSE,MAE,R2 +梯度提升树,651.1440567037434,448.99523765137667,0.8942640825672749 +随机森林,703.9966504917254,450.0664823777129,0.8764025354772573 +支持向量回归,738.1033329574317,529.5340809792003,0.8641365279788986 +线性回归,796.4617765623753,583.0197588300691,0.8418029967286178 +神经网络,809.7923656712804,609.9584660642089,0.836463110808386 diff --git a/outputs/models/best_bike_demand_model.pkl b/outputs/models/best_bike_demand_model.pkl new file mode 100644 index 0000000..9a8a3ba Binary files /dev/null and b/outputs/models/best_bike_demand_model.pkl differ diff --git a/outputs/models/支持向量回归.pkl b/outputs/models/支持向量回归.pkl new file mode 100644 index 0000000..5e74e0f Binary files /dev/null and b/outputs/models/支持向量回归.pkl differ diff --git a/outputs/models/梯度提升树.pkl b/outputs/models/梯度提升树.pkl new file mode 100644 index 0000000..9a8a3ba Binary files /dev/null and b/outputs/models/梯度提升树.pkl differ diff --git a/outputs/models/神经网络.pkl b/outputs/models/神经网络.pkl new file mode 100644 index 0000000..4b2a8a5 Binary files /dev/null and b/outputs/models/神经网络.pkl differ diff --git a/outputs/models/线性回归.pkl b/outputs/models/线性回归.pkl new file mode 100644 index 0000000..687b5e6 Binary files /dev/null and b/outputs/models/线性回归.pkl differ diff --git a/outputs/models/随机森林.pkl b/outputs/models/随机森林.pkl new file mode 100644 index 0000000..2fdddbf Binary files /dev/null and b/outputs/models/随机森林.pkl differ diff --git a/outputs/test_set_predictions.csv b/outputs/test_set_predictions.csv new file mode 100644 index 0000000..05f52d7 --- /dev/null +++ b/outputs/test_set_predictions.csv @@ -0,0 +1,148 @@ +日期,真实总骑行量,预测总骑行量,残差 +2011-01-03,1349,1577,-228.44 +2011-01-11,1263,1199,64.1 +2011-01-24,1416,1465,-49.06 +2011-01-31,1501,1554,-52.9 +2011-02-01,1360,950,410.36 +2011-02-03,1550,1504,46.2 +2011-02-09,1605,1455,150.06 +2011-02-14,1913,2320,-406.86 +2011-02-19,1635,1886,-251.38 +2011-02-24,1807,1201,606.29 +2011-02-25,1461,1134,327.36 +2011-03-02,2134,2191,-56.9 +2011-03-05,2077,2130,-53.29 +2011-03-07,1872,1694,177.93 +2011-03-08,2133,2248,-115.27 +2011-03-11,1977,1676,300.91 +2011-03-12,2132,2349,-217.47 +2011-03-14,2046,2226,-179.8 +2011-03-18,3239,4044,-804.76 +2011-03-19,3117,3370,-253.27 +2011-03-20,2471,2064,407.14 +2011-03-21,2077,2692,-615.29 +2011-03-23,2121,2103,18.4 +2011-03-26,2496,2591,-94.52 +2011-03-28,2028,2460,-431.86 +2011-04-01,2227,1954,273.39 +2011-04-08,1471,1773,-301.99 +2011-04-12,2034,3717,-1682.75 +2011-04-20,3944,4423,-478.72 +2011-04-29,4595,4603,-8.29 +2011-05-01,3351,3850,-499.24 +2011-05-12,4864,4534,330.31 +2011-05-14,3409,4328,-918.66 +2011-05-16,3958,3809,148.76 +2011-05-29,4788,4370,417.83 +2011-06-05,4906,4317,588.98 +2011-06-08,4401,4109,291.88 +2011-06-14,4891,4949,-58.17 +2011-06-15,5180,4954,225.68 +2011-06-24,4991,5067,-75.7 +2011-07-01,5362,5370,-7.84 +2011-07-12,4258,4201,57.37 +2011-07-16,5923,5163,760.0 +2011-07-18,4458,4614,-156.27 +2011-07-19,4541,4128,413.2 +2011-07-30,4475,3982,493.17 +2011-07-31,4302,3854,448.1 +2011-08-01,4266,4499,-232.75 +2011-08-07,3785,4459,-673.9 +2011-08-09,4602,4177,424.81 +2011-08-16,4725,4796,-70.79 +2011-08-23,5895,4925,970.15 +2011-08-24,5130,4796,334.21 +2011-09-02,4727,4418,309.5 +2011-09-05,3351,3770,-419.29 +2011-09-12,4713,5118,-404.54 +2011-09-17,4511,4179,331.63 +2011-09-23,2395,3520,-1124.77 +2011-09-24,5423,4669,754.39 +2011-10-03,3570,3909,-338.53 +2011-10-14,3644,4314,-669.66 +2011-10-18,4748,4912,-164.41 +2011-10-28,3747,3665,82.18 +2011-10-30,3331,3567,-235.88 +2011-11-03,3974,3630,344.25 +2011-11-11,3368,2273,1094.97 +2011-11-16,1817,2447,-630.11 +2011-11-23,2566,3439,-873.31 +2011-11-24,1495,2390,-895.02 +2011-11-25,2792,3982,-1189.72 +2011-11-28,3867,4444,-576.52 +2011-12-02,3940,3851,88.98 +2011-12-05,3811,3527,284.14 +2011-12-07,705,1651,-946.28 +2011-12-09,3620,3786,-165.78 +2011-12-11,2743,2912,-169.37 +2011-12-17,2739,2998,-259.32 +2011-12-18,2431,2814,-383.19 +2011-12-19,3403,3491,-88.36 +2011-12-22,3068,2795,272.56 +2011-12-23,2209,1710,498.59 +2011-12-24,1011,2143,-1131.82 +2011-12-27,1162,1523,-360.64 +2012-01-04,2368,2742,-374.41 +2012-01-05,3272,3353,-81.08 +2012-01-11,2177,2724,-547.45 +2012-01-16,2298,2465,-167.13 +2012-02-04,2832,2833,-0.88 +2012-02-09,3830,3233,597.12 +2012-02-28,4363,4302,60.68 +2012-03-01,4990,6011,-1020.55 +2012-03-06,3956,3165,791.34 +2012-03-10,4118,3709,408.86 +2012-03-12,5298,5856,-558.23 +2012-03-19,6153,6305,-151.59 +2012-03-21,6230,6325,-95.16 +2012-03-24,3372,5652,-2279.64 +2012-03-25,4996,4696,300.25 +2012-04-01,6041,4352,1688.79 +2012-04-10,5918,6416,-497.99 +2012-04-12,5409,4941,468.22 +2012-04-13,6398,6410,-12.48 +2012-04-14,7460,6934,526.08 +2012-04-18,4367,5904,-1537.08 +2012-04-25,6196,6642,-445.52 +2012-05-28,6043,6531,-487.78 +2012-05-29,5743,6794,-1050.6 +2012-06-07,7494,7312,181.84 +2012-06-11,6664,6500,164.39 +2012-06-20,6211,6244,-32.85 +2012-06-26,7442,6989,453.43 +2012-06-29,5463,5797,-334.07 +2012-07-04,7403,5172,2230.86 +2012-07-11,7264,7181,82.86 +2012-07-13,7499,7513,-13.89 +2012-07-22,7410,6212,1198.03 +2012-07-23,6966,6820,145.94 +2012-07-26,6861,6310,551.18 +2012-08-01,7580,6910,669.83 +2012-08-03,7175,6090,1085.44 +2012-08-18,7865,7339,526.24 +2012-08-20,6530,6418,112.26 +2012-08-24,7582,7232,350.11 +2012-08-25,6053,6488,-435.36 +2012-08-28,7040,7101,-60.93 +2012-08-29,7697,7241,455.71 +2012-08-30,7713,7287,425.85 +2012-09-16,7333,7350,-16.69 +2012-09-21,8167,7771,396.26 +2012-09-24,7436,7609,-172.87 +2012-09-25,7538,7702,-164.34 +2012-10-05,8156,7832,323.83 +2012-10-12,7282,7113,168.73 +2012-10-29,22,3514,-3491.84 +2012-11-06,5686,4806,879.7 +2012-11-09,5992,5555,436.88 +2012-11-14,5495,4837,657.66 +2012-11-21,5146,5562,-416.47 +2012-11-28,5260,4875,385.29 +2012-11-30,5668,5264,404.17 +2012-12-04,6606,6580,26.22 +2012-12-12,5319,5046,272.6 +2012-12-18,5557,5442,115.19 +2012-12-19,5267,4985,281.86 +2012-12-27,2114,1858,255.82 +2012-12-28,3095,2515,579.82 +2012-12-30,1796,1942,-145.6 diff --git a/outputs/共享单车日骑行量预测课程设计报告.docx b/outputs/共享单车日骑行量预测课程设计报告.docx new file mode 100644 index 0000000..3cbc81a Binary files /dev/null and b/outputs/共享单车日骑行量预测课程设计报告.docx differ