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

125 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()