ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

R语言回归实战:首尔自行车需求预测与Cubist模型调优

R语言回归实战:首尔自行车需求预测与Cubist模型调优

简介:这份资源面向具备一定R语言基础、希望上手真实回归建模的数据分析学习者与从业者,围绕首尔自行车共享系统的每小时租赁需求预测展开。数据源自首尔自行车共享计划,核心目标是借助温度、时间、假期、季节等变量,预测每小时所需自行车数量,以支撑租赁车辆的稳定供给。压缩包共4个文件,约1.71MB,包含R脚本、Markdown说明、Word分析报告与CSV数据集,分别对应建模代码、项目说明、结果记录与原始数据。资源完整呈现了从数据读取到模型评估的分析链路,涉及CUBIST、正则化随机森林、分类与回归树、K最近邻、条件推断树等模型,并采用重复交叉验证与测试集评估,配合R2、均方根误差、平均绝对误差和变异系数等指标衡量性能。其中基于规则的CUBIST模型在测试集上可解释约95%的方差,变量重要性分析显示温度与一天中的小时最具影响力。目前已有257人学习,适合作为回归建模与特征重要性分析的实战参考。

1. 首尔自行车共享需求回归:一份能跑通 Cubist 与随机森林的 R 项目

首尔市政府把每小时租借的自行车数量、温度、湿度、风速、季节、节假日全部记录在案,攒出一份 8760 行的 SeoulBikeData.csv。问题很直接:明天下午 6 点、气温 23 度、非节假日,该在江南站备多少辆车?备少了用户扫不到车,备多了调度成本白烧。这个项目就是拿 R 语言做回归分析,把「每小时所需自行车数量」当成因变量,用温度、小时、季节这些自变量去拟合,最终 Cubist 模型在测试集上能解释约 95% 的方差。整包资源里有 FinalProject.R 主脚本、SeoulBikeData.csv 数据、Data Analysis Report.docx 报告和 README.md,适合正在做回归课程设计、想拿真实城市数据练手、或者需要一套可复现建模流程的从业者。R 语言做回归分析的门槛不在语法,而在数据清洗和模型选型的判断,这份资源把这两块都摆出来了。

2. 数据读取与清洗:从 8760 行原始记录到可建模数据框

2.1 先看清 SeoulBikeData.csv 的列结构

拿到数据第一步不是急着 lm(),而是把每一列的类型和分布摸清楚。SeoulBikeData.csv 的列名带空格和括号,R 读进来默认会变成点号,后面写公式时容易翻车。常见做法是 read.csv 时加 check.names = FALSE,保留原始列名,再用 names() 确认一遍。

# 读取首尔自行车数据,保留原始列名 bike <- read.csv("SeoulBikeData.csv", check.names = FALSE, fileEncoding = "UTF-8") # 查看维度与列名 dim(bike) # 应为 8760 行、14 列左右 names(bike) # 确认 Date、Rented Bike Count、Temperature 等列存在 str(bike) # 检查每列类型,字符型需要转换

逻辑说明:check.names = FALSE 防止 R 把「Rented Bike Count」改成「Rented.Bike.Count」,这样后续公式里可以直接用反引号引用。fileEncoding 指定 UTF-8 是因为原始 CSV 可能含韩文或特殊符号,不指定会读出乱码。str() 是血泪经验——很多回归报错源于某列被读成 character,比如温度列混入了「°C」后缀。

参数说明:如果 dim() 返回的不是 8760 行,说明分隔符或编码有问题,改用 read.csv2 或指定 sep = ","。如果 Date 列是字符型,需要 as.Date(bike$Date, format = "%d/%m/%Y") 转换,具体格式看原始文件。

2.2 缺失值、异常值与特征工程

这份数据相对干净,但仍有几处必须处理。第一,Functioning Day 列标记当天是否运营,为 No 时 Rented Bike Count 恒为 0,这些行对预测需求没有信息量,建模时应剔除或单独标记。第二,温度列存在负值,首尔冬天零下十几度是真实的,不要当异常值删掉。第三,小时信息藏在 Date 里,需要拆出 Hour 列,这是后续变量重要性排第二的关键特征。

# 剔除未运营日 bike <- bike[bike$`Functioning Day` == "Yes", ] # 从 Date 拆出小时 bike$Hour <- as.integer(format(as.POSIXct(bike$Date, format = "%d/%m/%Y"), "%H")) # 把季节、节假日转为因子 bike$Seasons <- as.factor(bike$Seasons) bike$Holiday <- as.factor(bike$Holiday) # 检查缺失值 colSums(is.na(bike))

逻辑说明:Functioning Day 为 No 的行 Rented Bike Count 全是 0,留着会让模型学到「不运营就没人骑」这种废话,直接剔除。Hour 列用 format 从 Date 提取,注意 as.POSIXct 的格式要和原始日期一致,否则全变 NA。因子化季节和节假日是因为回归模型对无序分类变量的处理方式不同,Cubist 和随机森林能直接吃因子,lm 则需要手动转哑变量。

参数说明:format 里的 %d/%m/%Y 对应「日/月/年」,如果原始是「年-月-日」要改成 %Y-%m-%d。colSums(is.na(bike)) 返回每列缺失数量,若某列缺失超过 5%,考虑插补或剔除该列,而不是无脑 na.omit。

提示:Hour 提取后建议 table(bike$Hour) 看一眼分布,确认 0 到 23 每个小时都有记录,否则可能是日期解析漏了部分行。

3. 回归模型训练:Cubist、随机森林与 KNN 的 R 实现

3.1 训练集测试集划分与重复交叉验证

回归分析最怕过拟合,所以不能拿全部数据训练再拿全部数据评估。这个项目用了重复交叉验证加独立测试集的组合。常见做法是 70% 训练、30% 测试,训练集内部再做 5 折交叉验证调参。R 里用 caret 包的 createDataPartition 能保持因变量分布一致,比 sample() 更稳。

library(caret) set.seed(123) # 按因变量分层划分训练测试集 idx <- createDataPartition(bike$`Rented Bike Count`, p = 0.7, list = FALSE) train <- bike[idx, ] test <- bike[-idx, ] # 设置重复交叉验证 ctrl <- trainControl(method = "repeatedcv", number = 5, repeats = 3)

逻辑说明:createDataPartition 按 Rented Bike Count 的分位数分层抽样,避免训练集全是高需求时段、测试集全是低需求时段。set.seed 保证结果可复现,换台机器跑出来一样。trainControl 里 repeatedcv 表示重复交叉验证,number = 5 是 5 折,repeats = 3 是重复 3 次,比单次 5 折更稳定,代价是训练时间翻三倍。

参数说明:p = 0.7 是训练集比例,数据量 8000 多行时 70/30 够用;若数据少于 500 行,建议 p = 0.8。repeats 设 3 是精度和耗时的折中,赶时间可以设 1,但模型评估方差会大。

3.2 Cubist 与正则化随机森林的调参

项目摘要里 Cubist 解释了约 95% 的方差,是表现最好的模型。Cubist 本质是基于规则的回归树集成,每条规则附带一个线性模型,对温度和小时这种连续加分类混合的特征特别友好。随机森林则用正则化控制树深,防止过拟合。

library(Cubist) library(ranger) # Cubist 模型,committee 控制规则数 cubist_fit <- train(`Rented Bike Count` ~ ., data = train[, -which(names(train) == "Date")], method = "cubist", trControl = ctrl, tuneGrid = expand.grid(committees = c(1, 5, 10), neighbors = c(0, 1, 3))) # 正则化随机森林 rf_fit <- train(`Rented Bike Count` ~ ., data = train[, -which(names(train) == "Date")], method = "ranger", trControl = ctrl, tuneGrid = expand.grid(mtry = c(2, 4, 6), splitrule = "variance", min.node.size = c(5, 10)))

逻辑说明:Cubist 的 committees 参数控制集成中规则集的数量,越大拟合能力越强但可能过拟合;neighbors 是预测时用的最近邻修正,0 表示不用。随机森林用 ranger 引擎比 randomForest 快,mtry 是每次分裂随机选的变量数,min.node.size 控制叶节点最小样本,两者共同决定正则化强度。公式里去掉 Date 列是因为日期本身不直接预测需求,小时已经提取出来了。

参数说明:committees 从 1 到 10 搜索,neighbors 从 0 到 3,网格不大是因为 Cubist 训练快。mtry 取 2、4、6 是因为总特征约 12 个,sqrt(12) 约 3.5,围绕这个值搜索。splitrule = "variance" 是回归任务的默认分裂准则。

3.3 KNN 与条件推断树的对比

KNN 对量纲敏感,必须先标准化;条件推断树对分类变量处理更自然,但连续变量多时不如 Cubist。这两个模型在这个项目里是陪跑,但能帮你理解不同算法的边界。

# KNN 需要标准化 knn_fit <- train(`Rented Bike Count` ~ ., data = train[, -which(names(train) == "Date")], method = "knn", trControl = ctrl, preProcess = c("center", "scale"), tuneLength = 10) # 条件推断树 library(party) ctree_fit <- train(`Rented Bike Count` ~ ., data = train[, -which(names(train) == "Date")], method = "ctree", trControl = ctrl)

逻辑说明:preProcess = c("center", "scale") 对 KNN 是必须的,否则温度(0-40)和小时(0-23)量纲差异会让距离计算偏向温度。tuneLength = 10 让 caret 自动在 10 个 k 值里搜索。ctree 不需要调参,但训练慢,适合作为基准模型。

参数说明:KNN 的 k 值通过 tuneLength 自动搜索,一般最优在 5 到 15 之间。ctree 的 mincriterion 默认 0.95,可通过 tuneGrid 调整,但项目里保持默认即可。

4. 模型评估与变量重要性:R2、RMSE 与温度小时的主导地位

4.1 四个评估指标的计算与解读

项目用了 R2、RMSE、MAE 和变异系数四个指标。R2 看解释方差比例,RMSE 和 MAE 看预测误差绝对值,变异系数看误差相对于均值的比例。四个一起看才能避免被单一指标误导。

# 在测试集上预测 pred_cubist <- predict(cubist_fit, test) pred_rf <- predict(rf_fit, test) # 计算指标 postResample(pred_cubist, test$`Rented Bike Count`) postResample(pred_rf, test$`Rented Bike Count`) # 手动算变异系数 cv <- function(actual, pred) sd(actual - pred) / mean(actual) cv(test$`Rented Bike Count`, pred_cubist)

逻辑说明:postResample 一次性返回 RMSE、Rsquared 和 MAE,省去手写公式。变异系数用误差标准差除以实际均值,能看出误差相对于需求规模的比例,比 RMSE 更直观。Cubist 的 R2 约 0.95,意味着温度和小时等特征能解释 95% 的需求波动,剩下 5% 可能是突发活动或天气骤变。

参数说明:postResample 第一个参数是实际值,第二个是预测值,顺序反了结果会错。变异系数没有内置函数,手动算时注意 sd 和 mean 都基于实际值与误差。

4.2 变量重要性分析与业务解释

变量重要性告诉你哪些特征在推动预测。项目结果显示温度和小时排前二,这符合直觉:温度决定人们愿不愿意出门,小时决定通勤高峰还是深夜。

# Cubist 变量重要性 cubist_imp <- varImp(cubist_fit) plot(cubist_imp, top = 10) # 随机森林变量重要性 rf_imp <- varImp(rf_fit) plot(rf_imp, top = 10)

逻辑说明:varImp 对 Cubist 返回的是条件变量重要性,对随机森林返回的是基于不纯度减少的重要性。两个模型的重要性排序可能略有差异,但温度和小时通常都在前二。plot 的 top = 10 只画前十个,避免图太挤。

参数说明:varImp 对不同模型返回结构不同,Cubist 返回的是 0 到 100 的归一化分数,随机森林返回的是原始重要性值。比较时看排序而非绝对值。

注意:变量重要性高不代表因果,温度高需求高是相关性,不能得出「加热城市就能增加骑行」的结论。

5. 避坑与排查:R 回归建模里最容易翻车的五件事

5.1 现象:read.csv 读进来列名全变点号,公式报错「object not found」

原因:R 默认把列名里的空格和特殊字符替换成点号,Rented Bike Count变成 Rented.Bike.Count,公式里写原始名就找不到。

解决:读取时加 check.names = FALSE,或者用反引号包裹列名,如Rented Bike Count。更稳的做法是读进来后统一重命名,用 gsub 把空格换成下划线。

5.2 现象:Cubist 训练报错「cannot allocate vector of size」

原因:Cubist 的 committees 设太大,或者数据行数多、特征维度高,内存不够。

解决:先把 committees 降到 5 以内,neighbors 设 0 或 1。如果还不行,用 data.table 或 arrow 包读数据减少内存占用,或者对训练集下采样。

5.3 现象:KNN 的 RMSE 比其他模型高一大截

原因:忘了标准化,温度的量纲远大于小时,距离计算被温度主导。

解决:trainControl 里加 preProcess = c("center", "scale"),或者手动 scale() 后再建模。注意测试集也要用同样的均值和标准差标准化,不能各自标准化。

5.4 现象:随机森林的 R2 在训练集 0.99、测试集 0.7

原因:过拟合,树太深或 min.node.size 太小,模型把训练集噪声也学进去了。

解决:增大 min.node.size,从 5 调到 10 甚至 20;减小 mtry;增加交叉验证的 repeats。用 ranger 时还可以设 max.depth 限制树深。

5.5 现象:Date 列没剔除,模型报错「invalid type (list) for variable」

原因:Date 是 Date 类型,回归模型不认,或者被当成字符型后无法处理。

解决:建模前用 train[, -which(names(train) == "Date")] 剔除,或者把 Date 转成数值型时间戳。更彻底的做法是特征工程阶段就把 Date 拆成 Hour、DayOfWeek、Month 后删掉原列。

6. 进阶技巧:用 Cubist 规则集做小时级需求预测与调度建议

Cubist 最大的好处是模型可解释——它输出的不是黑匣子,而是一组「如果温度在 X 到 Y 之间、小时在 Z 区间,则需求 = a×温度 + b×小时 + c」的规则。你可以直接把规则打印出来,交给调度团队当决策依据。

# 查看 Cubist 规则集 summary(cubist_fit$finalModel) # 构造新数据做预测 new_data <- data.frame( Temperature = c(15, 25, 5), Hour = c(8, 18, 22), Humidity = c(60, 40, 70), Seasons = factor(c("Spring", "Summer", "Winter"), levels = levels(bike$Seasons)), Holiday = factor(c("No", "No", "No"), levels = levels(bike$Holiday)) ) predict(cubist_fit, new_data)

逻辑说明:summary 打印规则集,每条规则的条件和线性系数都能看到。构造 new_data 时因子水平必须和训练集一致,否则 predict 会报错或给出错误结果。预测结果可以直接解读为「早上 8 点 15 度备 X 辆,下午 6 点 25 度备 Y 辆」。

参数说明:new_data 的列名必须和训练集完全一致,包括因子水平顺序。如果训练集有 12 个特征,new_data 也要有 12 列,不能只给部分。predict 对 Cubist 返回的是数值向量,可以直接 round() 取整作为车辆数。

我一般会在部署前做一件事:把测试集按小时分组,算每个小时的平均预测误差,看模型在哪个时段最不准。通常早晚高峰误差大,因为需求波动剧烈;凌晨误差小,因为需求接近零。这个分组误差表比单一 RMSE 有用得多,能告诉你模型在哪些场景下不能信。

从那以后我每次做完回归模型,都强制走一遍「按关键分组算误差」的流程,不再只看一个总体 R2。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进