☰
R语言机器学习实战:从数据预处理到随机森林与XGBoost建模评估
2026/10/24 6:47:08 网站建设 项目流程

简介:面向具备一定R语言基础、希望系统练习机器学习算法实现的数据分析与建模学习者,这份压缩包围绕R语言与监督/非监督学习算法展开,涵盖简单线性回归、多元线性回归、多项式回归、决策树回归、支持向量回归等回归模型,以及数据预处理模板与示例数据,适合对照代码理解建模流程、复现实验和补充算法实践。包内共20个文件,以8个R脚本和6个CSV数据文件为主,另含许可说明等辅助文件,压缩包约16KB,体量轻便,便于按回归与预处理模块查阅。已有245人学习下载,可作为课程作业、项目入门或算法复现的参考素材,帮助读者从数据清洗、特征处理到模型训练与评估逐步搭建R语言机器学习实践框架。

1. 从一份跑不动的 R 脚本说起:这套算法合集到底解决什么问题

上周一个做数据分析的朋友把压缩包丢给我,说里面全是.R文件,双击打开不是library()就是fit(),跑到第三行就报“找不到包”。这大概是很多人接触 R 语言机器学习的第一现场:算法本身门槛不高,难的是环境、数据、评估三段路各自埋着坑,而网上能搜到的示例大多是孤立的几十行代码,彼此接不上。这份资源做的就是把断掉的路接起来——它把分类、回归、聚类这几类常见任务用 R 从头实现了一遍,从数据读入、缺失值处理、特征编码,到线性模型、决策树、随机森林、提升树,再到混淆矩阵、ROC、交叉验证,每个环节都有对应脚本,可以一段一段跑通。它适合两类人:刚学完统计、想把课本上的算法真正跑在数据上的新手;以及想把 R 建模流程固化成模板、以后换数据集直接套的熟手。

2. 目录结构与运行链路:从装包到跑通第一个脚本

拿到一份陌生的 R 项目,我最先看的不是算法,而是文件怎么分层、脚本按什么顺序执行。顺序错了,后面调参调得再细都是白费——这是踩过坑之后养成的习惯。

2.1 脚本按数据流编号,而不是按算法分类

很多初学者整理的 R 代码目录是按算法分的:线性回归.R、随机森林.R、xgboost.R,每个文件里从读数据到出图全写一遍。这种组织方式看着直观,但改一个字段名要改十几个文件。这份资源的目录是按数据流排的,我把它整理成下面这样:

ml-in-r/ ├── data/ │ ├── train.csv # 原始训练集 │ └── test.csv # 留出的测试集 ├── R/ │ ├── 01_load.R # 读入 + 字段体检 │ ├── 02_clean.R # 缺失值、异常值、编码 │ ├── 03_split.R # 训练/测试切分 + CV 折 │ ├── 04_train_linear.R # 线性 / 逻辑回归 │ ├── 04_train_tree.R # rpart / 随机森林 │ ├── 04_train_boost.R # gbm / xgboost │ └── 05_eval.R # 指标汇总、ROC 出图 ├── output/ │ ├── models/ # 训练好的 .rds 模型 │ └── figures/ # 混淆矩阵、ROC 曲线 ├── run_all.R # 顶层入口 └── packages.R # 依赖清单

分层的好处很实在:01到03是所有算法共用的,换个模型只要动04_train_*.R;中间的train、test两个数据框在全局环境里传递,不用每个脚本重复 read。我一般会在run_all.R里只写source(),不写任何业务逻辑:

# run_all.R —— 顶层入口,只负责按顺序执行,不放业务代码 # 这样任何一步出错,报错行号能直接定位到具体分段脚本 source("packages.R") # 先保证依赖齐 source("R/01_load.R") # 读数据、字段体检 source("R/02_clean.R") # 缺失值、异常值、编码 source("R/03_split.R") # 切分 + 交叉验证折 source("R/04_train_linear.R") source("R/04_train_tree.R") source("R/04_train_boost.R") source("R/05_eval.R") # 统一评估、出图

这里的逻辑是:source()相当于把被调用脚本的代码原地展开执行,所以run_all.R里的全局变量(比如train、test、ctrl)在后面每个脚本里都能直接用。参数上唯一需要注意的是工作目录——RStudio 里点“Source”默认以脚本所在目录为工作目录,命令行Rscript run_all.R则是以当前 shell 目录为准,两者不一致就会报“cannot open file”。我一般会在入口最前面加一句setwd(dirname(rstudioapi::getSourceEditorContext()$path)),或者干脆约定所有路径都相对项目根目录写。

2.2 依赖包一次装齐,版本留个档

R 的包管理是新手最容易卡住的地方。install.packages()默认从镜像拉最新版,今天装的和三个月后装的版本可能不兼容,predict()的行为都会变。这份资源在packages.R里集中管理依赖,我按这个思路改成了带存在性检查的版本:

# packages.R —— 依赖集中声明,装过就跳过,避免每次都重装 pkgs <- c( "tidyverse", # 数据处理与绘图 "caret", # 统一的建模接口与交叉验证 "rpart", "rpart.plot", # 决策树 "randomForest", # 随机森林 "glmnet", # 带正则的线性模型 "xgboost", # 提升树 "pROC", # ROC / AUC "ggplot2" ) install_if_missing <- function(p) { if (!requireNamespace(p, quietly = TRUE)) { install.packages(p, dependencies = TRUE) } } invisible(lapply(pkgs, install_if_missing))

requireNamespace(p, quietly = TRUE)返回布尔值,不加载包、不打印噪音,比require()干净;dependencies = TRUE会把包依赖的底层库一起装上,省掉后面“装了 A 却缺 B”的连锁报错。真正要做长期项目,建议再装一个renv,在项目根目录执行renv::init()生成renv.lock,把每个包的精确版本记下来,换机器时renv::restore()一键还原——这套流程在 R 语言机器学习项目里几乎是标配,尤其是模型结果要复现的时候。

2.3 先跑通最小闭环,再谈调参

新人常见的做法是先把所有算法文件都打开读一遍,读完就晕了。我的建议是先只跑01到03再加一个最简单的模型,确认整条链路通,再往里加复杂度。判断“通”的标准很简单:str(train)能看到数据框、table(train$label)两个类别都有样本、ctrl对象打印出来是 5 折交叉验证。这三个都对了,后面换什么算法都是同一套骨架。

3. 数据预处理模块:缺失值、编码与切分这三步别省

算法部分其实是整份资源里最“标准化”的——glm、randomForest、xgboost的调用格式十年没大变过。真正拉开结果差距的是预处理,这也是最多人跳过的一段。

3.1 读入之后先做一次字段体检

不要急着summary()。summary()对高维数据是一屏糊,看不出哪列全是空值、哪列只有一个取值。我一般先跑一遍结构化体检:

# 01_load.R —— 读入并生成字段体检表 df <- read.csv("data/train.csv", stringsAsFactors = FALSE, na.strings = c("", "NA", "NULL", "-")) # 统一空值写法 profile <- data.frame( col = names(df), type = sapply(df, function(x) class(x)[1]), na_ratio = round(colMeans(is.na(df)), 4), # 每列缺失比例 n_unique = sapply(df, function(x) length(unique(x))) # 唯一值个数 ) # 按缺失率降序看,最脏的字段排最前面 profile[order(-profile$na_ratio), ]

na.strings这一步很关键。原始 CSV 里的空值可能写成空字符串、NA、NULL、-、?,不统一声明的话read.csv只会把空字符串当缺失,其余都变成字符型的“正常值”,后面is.na()判断全失效。n_unique == 1的列是常量列,直接删;n_unique接近行数的列大概率是 ID,也要排除,否则模型会拿 ID 当特征,训练集 AUC 能刷到 0.99,测试集惨不忍睹。

3.2 缺失值:先判断缺失机制,再决定填什么

填充方法本身不难,难的是判断该不该填。缺失比例超过 40% 的字段,填出来的值基本是噪声,我倾向于整列丢掉;剩下的小比例缺失,数值型用中位数、类别型用众数,是最稳的默认选项——均值会被极端值带偏,中位数不会。

# 02_clean.R —— 缺失值处理 threshold <- 0.4 drop_cols <- names(df)[colMeans(is.na(df)) > threshold] df <- df[, setdiff(names(df), drop_cols)] fill_na <- function(x) { if (is.numeric(x)) { x[is.na(x)] <- median(x, na.rm = TRUE) # 中位数抗极端值 } else { # 类别型取众数:table 排序后第一个 x[is.na(x)] <- names(sort(table(x), decreasing = TRUE))[1] } x } df[] <- lapply(df, fill_na) # 注意用 df[] 保留数据框结构

df[] <- lapply(...)和df <- lapply(...)的区别是个经典陷阱:前者把结果按列写回原数据框,仍是 data.frame;后者会得到一个 list,后面train$label直接报错。参数上median(x, na.rm = TRUE)里的na.rm不能省,否则返回NA,填了个寂寞。

3.3 编码与标准化:只缩放该缩放的东西

R 的建模函数对因子(factor)有原生支持,所以类别变量要先转 factor,而不是手动 one-hot。caret的preProcess能做中心化和标准化,但千万别把已经 one-hot 的 0/1 列也一起缩放——缩放后变成负数,树模型的分裂点会变得难解释。

# 目标变量必须是因子,caret 的分类模型才认得 df$label <- as.factor(df$label) # 只挑数值列做标准化,因子列原样保留 num_cols <- names(df)[sapply(df, is.numeric)] pre_proc <- preProcess(df[, num_cols], method = c("center", "scale")) df[, num_cols] <- predict(pre_proc, df[, num_cols])

method = c("center", "scale")是先减均值再除标准差,也就是 z-score。这里有个容易忽略的点:pre_proc是在训练集上算出来的均值和标准差,测试集必须用同一个pre_proc去predict,不能重新算一遍。重新算等于把测试集的信息泄漏进流程,指标会虚高。

3.4 切分与交叉验证折:种子和分层都要管

# 03_split.R —— 切分与训练控制 set.seed(2024) # 切分和建模前都得设,不然每次结果都不同 idx <- createDataPartition(df$label, p = 0.8, list = FALSE) train <- df[ idx, ] test <- df[-idx, ] ctrl <- trainControl( method = "cv", number = 5, # 5 折,样本少时可调到 10 classProbs = TRUE, # 要算 AUC 必须打开 summaryFunction = twoClassSummary,# 二分类用 ROC 作为选参指标 savePredictions = "final" # 保存折外预测,方便后面画图 )

createDataPartition是按label分层抽样的,能保证训练集和测试集的类别比例一致;如果用sample()随机抽,遇到不平衡数据(比如正样本只有 5%)很可能测试集里一个正样本都没有,confusionMatrix直接报错。classProbs = TRUE和summaryFunction = twoClassSummary是配套的:前者让模型输出概率,后者让caret用 AUC 而不是准确率来挑超参数——不平衡数据下准确率是个骗子指标。

4. 算法实现拆解:从线性模型到提升树的一条主线

这份资源覆盖的算法不少,但它们其实共享同一套调用骨架:给定公式和数据、给定超参数网格、给定训练控制对象,剩下的交给caret。看懂一个,其余的照葫芦画瓢就行。

4.1 线性模型:公式接口和矩阵接口各管一段

# 04_train_linear.R # 公式接口:可读性好,适合需要解释系数的场景 fit_logit <- glm(label ~ ., data = train, family = binomial()) summary(fit_logit)$coefficients # 看 p 值和系数方向 # 矩阵接口:字段多、要做正则时用,速度快、省内存 x <- model.matrix(label ~ . - 1, data = train) # -1 去掉截距列 y <- as.numeric(train$label) - 1 # 因子转 0/1 fit_ridge <- cv.glmnet(x, y, family = "binomial", alpha = 0, # 0 = 岭回归 lambda = 10^seq(-3, 2, length = 50))

两套接口的差别值得说清楚。glm的公式接口会把因子自动展开成哑变量,summary()直接给出每个系数的显著性和优势比,适合做业务解释;但它没法做正则化,字段上百个、共线性严重时系数会爆炸。glmnet走矩阵接口,alpha = 0是 L2 正则(岭回归),alpha = 1是 L1(Lasso,能把系数压到 0 做特征筛选),0 < alpha < 1是弹性网。cv.glmnet会自动做交叉验证选lambda,取fit_ridge$lambda.min即可。注意model.matrix遇到测试集里出现训练集没有的因子水平会报错,这个坑在第 5 章细说。

4.2 树模型:剪枝参数比树的数量更重要

决策树在 R 里就是rpart,参数少但每个都影响很大:

# 04_train_tree.R —— 单棵树 fit_tree <- rpart( label ~ ., data = train, method = "class", control = rpart.control( cp = 0.01, # 复杂度参数,越小树越深 minsplit = 20, # 节点最少样本数才允许分裂 maxdepth = 6 # 限制深度,防过拟合 ) ) printcp(fit_tree) # 看 CP 表,选 xerror 最小那一行的 cp 剪枝

printcp()输出的表里,xerror是交叉验证误差,xstd是它的标准差。经验做法是选xerror最小、且满足“1-SE 规则”(误差不超过最小值加一个标准差)的最简树,这样能砍掉一堆只带来微小提升的冗余分支。cp设太小(比如 0.001)会得到一棵几乎把每个样本都分开的树,训练集完美、测试集崩盘,这是最典型的过拟合现场。

随机森林用caret包一层,主要调mtry(每次分裂随机抽的特征数):

fit_rf <- train( label ~ ., data = train, method = "rf", trControl = ctrl, tuneGrid = expand.grid(mtry = c(2, 4, 6)), # 一般取 sqrt(p) 附近 ntree = 500, importance = TRUE ) varImp(fit_rf, scale = FALSE) # 变量重要性,可以据此砍特征

mtry的默认经验值是分类任务取sqrt(特征数),回归任务取特征数/3,但具体数据上还是得网格搜一下。ntree = 500通常够用,树的数量增加到一定程度指标就平了,继续加只是浪费时间。importance = TRUE才会计算变量重要性,代价是训练稍慢,但换来的是能看出哪些字段在拖后腿。

4.3 提升树:参数多,但只有三个真正需要调

xgboost在caret里的参数名和原生接口不一样,这是新手最容易懵的地方:

# 04_train_boost.R —— 提升树参数网格 grid_xgb <- expand.grid( nrounds = c(200, 500), # 迭代轮数,对应原生接口的 nrounds max_depth = c(3, 5), # 树深,3 起步最稳 eta = c(0.05, 0.1), # 学习率,越小需要越多轮 gamma = 0, # 分裂所需的最小损失下降 colsample_bytree = 0.8, # 每棵树随机用的特征比例 min_child_weight = 1, # 叶子节点最小样本权重 subsample = 0.8 # 每轮随机用的样本比例 ) fit_xgb <- train( label ~ ., data = train, method = "xgbTree", trControl = ctrl, tuneGrid = grid_xgb, verbose = FALSE )

七个参数全网格搜是 2×2×2=8 组,还扛得住;如果每个都开三档就是 3^7 组,跑一晚上都出不来。实战里的顺序是:先固定eta = 0.1、max_depth = 3,只调nrounds;收敛之后再降eta到 0.05 并相应加nrounds;最后微调colsample_bytree和subsample控制随机性。gamma和min_child_weight一般在数据噪声大时才动。

4.4 统一评估口径:混淆矩阵、ROC 和 RMSE 各看什么

不同算法训完之后必须用同一套指标比较,否则没有可比性:

# 05_eval.R —— 分类任务评估 pred_prob <- predict(fit_rf, test, type = "prob")[, "yes"] # 正类概率 pred_cls <- predict(fit_rf, test) cm <- confusionMatrix(pred_cls, test$label, positive = "yes") cm$byClass[c("Sensitivity", "Specificity", "Balanced Accuracy")] roc_obj <- roc(response = test$label, predictor = pred_prob, levels = c("no", "yes"), direction = "<") # 控制方向,否则 AUC 可能是 1-AUC auc(roc_obj)

positive = "yes"必须显式指定,否则caret按字母序把第一个水平当正类,敏感度和特异度会对调。direction = "<"是pROC的方向参数,意思是“预测概率越大越可能是正类”;如果写反了,AUC 会变成1 - 真实AUC,一个 0.85 的模型显示成 0.15,排查半天以为是模型坏了。回归任务则看 RMSE 和 MAE:RMSE 对大误差惩罚重,MAE 更稳健,两个一起看能判断是否存在少量极端错例。

5. 避坑与排查:R 机器学习里最容易翻车的五个地方

这一章是我自己踩过、也帮别人排查过的真实故障,基本覆盖了 80% 的“跑不通”和“结果不对”。

5.1 报错 there is no package called 'xxx'

现象:脚本第一行library(caret)就报错,但明明刚刚install.packages("caret")成功了。

原因:九成是装到了另一个库路径。R 有多个 library path(libPaths()可以看到),RStudio 图形界面装包和命令行install.packages可能落在不同目录;另一个常见原因是包名拼写或大小写错误,R 的包名是大小写敏感的。

解决:先跑.libPaths()确认路径,再find.package("caret")看能不能找到;找不到就install.packages("caret", lib = .libPaths()[1])显式指定路径。装完library()还报错的话,重启一次 R session,之前加载失败的缓存有时会残留。

5.2 预测时报 factor has new levels

现象:训练一切正常,predict(fit, test)时报factor has new levels,或者model.matrix报contrasts can be applied only to factors with 2 or more levels。

原因:测试集的某个因子列里出现了训练集没有的类别,比如训练集的城市只有北上广,测试集冒出来一个“杭州”;或者反过来,测试集里某列只有一个取值,model.matrix无法构造对比。

解决:预测前强制对齐因子水平,把测试集里训练集没有的类别归到NA或一个“其他”桶里:

# 对齐因子水平,训练集没有的类别统一归为 NA align_levels <- function(new_df, ref_df) { for (col in names(new_df)) { if (is.factor(ref_df[[col]])) { new_df[[col]] <- factor(new_df[[col]], levels = levels(ref_df[[col]])) } } new_df } test <- align_levels(test, train)

更好的做法是在02_clean.R阶段就把稀有类别合并掉,从源头上避免。

5.3 每次跑结果都不一样

现象:同样的代码,跑两遍随机森林,AUC 一次 0.82 一次 0.79,汇报的时候说不清哪个是真的。

原因:切分、交叉验证折、随机森林的特征抽样、xgboost 的行列抽样全都是随机的,没有固定种子就是随机结果。

解决:在run_all.R最前面设一次set.seed(2024),并且在每个训练脚本开头再设一次——因为caret内部会消耗随机数流,前面的步骤不同会导致后面的种子状态不同。要更严格的话用withr::with_seed()包住每段训练,保证互不干扰。

5.4 训练集 AUC 0.99,测试集只有 0.6

现象:模型在自己训过的数据上近乎完美,一上测试集就现原形。

原因:三种可能——特征里混进了 ID 或时间戳这类“未来信息”;在切分之前就做了标准化或缺失值填充,测试集信息泄漏进了训练;样本量太小而特征维度太高。

解决:先把n_unique接近行数的列全部剔除;把标准化、填充这类操作统统挪到切分之后,并且只在训练集上fit、在测试集上transform;特征数超过样本数十分之一时,优先上正则化模型(glmnet)或降维,别硬上随机森林。

5.5 读进来的中文全是乱码

现象:read.csv之后中文字段名变成X.U.E4.B8.AD这种形式,或者内容显示成问号。

原因:CSV 文件是 UTF-8 编码,而 R 在部分系统上默认按本地编码读取;另一种情况是 Excel 导出的 CSV 带 BOM 头。

解决:read.csv(..., fileEncoding = "UTF-8")显式指定编码;字段名乱码的话加check.names = FALSE保留原始列名。Windows 上如果还不行,用readr::read_csv()代替,它对编码的容错更好,而且不会自动把字符串转成因子。

6. 二次开发:把散装脚本收成一条能复用的建模流水线

这份资源的脚本是“一次一分析”的写法,换个数据集就得改一堆硬编码的列名。真正要拿去干活,我建议做一件事:把“切分—预处理—训练—评估”四步包成一个函数,数据集作为参数传进去。改造的核心是把预处理对象和模型一起存下来,避免预测时重新 fit:

# pipeline.R —— 一条可复用的二分类流水线 run_pipeline <- function(data, label_col, methods = c("glm", "rf")) { set.seed(2024) # 1. 切分(分层) idx <- createDataPartition(data[[label_col]], p = 0.8, list = FALSE) train <- data[ idx, ] test <- data[-idx, ] # 2. 预处理对象只在训练集上 fit,随模型一起保存 num_cols <- names(train)[sapply(train, is.numeric)] pre <- preProcess(train[, num_cols], method = c("center", "scale")) train[, num_cols] <- predict(pre, train[, num_cols]) test[, num_cols] <- predict(pre, test[, num_cols]) # 复用同一个 pre # 3. 训练控制 ctrl <- trainControl(method = "cv", number = 5, classProbs = TRUE, summaryFunction = twoClassSummary) # 4. 批量训练并汇总指标 results <- lapply(methods, function(m) { fit <- train(as.formula(paste(label_col, "~ .")), data = train, method = m, trControl = ctrl) prob <- predict(fit, test, type = "prob")[, levels(test[[label_col]])[2]] list(model = fit, pre = pre, auc = as.numeric(pROC::auc(test[[label_col]], prob))) }) names(results) <- methods results }

这个函数有三个设计点值得保留:第一,preProcess的结果作为返回值的一部分,部署时直接拿它对新样本做predict,不会出现训练/推理两套标准;第二,as.formula(paste(label_col, "~ ."))让标签列名可以传参,不用改代码;第三,返回结构里同时带模型和 AUC,批量比较模型时直接sapply(res, function(x) x$auc)就能出表。

再往下做一步,是把results里表现最好的模型用saveRDS()落盘,预处理对象单独存一个文件,推理脚本只readRDS()后predict(),整个流程就和训练完全解耦了。我在项目里一般还会加一张算法选型对照表,省得每次都要重新想:

场景首选关键参数注意点
需要解释系数、字段少glm / 逻辑回归family = binomial共线性严重时系数不稳
字段多、需要特征筛选cv.glmnetalpha = 1(Lasso)标准化必须做
中小样本、非线性随机森林mtry、ntree别用默认 mtry 直接上
追求精度、样本充足xgbTreeeta、max_depth、nrounds参数多,分步调
需要可视化决策规则rpartcp、maxdepth单棵树精度有限

最后说个教训。我最早做这类项目时,习惯把所有处理写在一个脚本里,跑通就交差,结果两个月后数据更新,重跑报错,花了整整一天才定位到是某个字段的因子水平变了导致的predict失败。从那以后我每次交付 R 语言机器学习代码,都强制走一遍“换一台干净机器 + 换一份新数据 + 只改路径”的回归测试,跑不通就不算完。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询