
简介面向具备一定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是 L1Lasso能把系数压到 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×28 组还扛得住如果每个都开三档就是 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 和 MAERMSE 对大误差惩罚重MAE 更稳健两个一起看能判断是否存在少量极端错例。5. 避坑与排查R 机器学习里最容易翻车的五个地方这一章是我自己踩过、也帮别人排查过的真实故障基本覆盖了 80% 的“跑不通”和“结果不对”。5.1 报错 there is no package called xxx现象脚本第一行library(caret)就报错但明明刚刚install.packages(caret)成功了。原因九成是装到了另一个库路径。R 有多个 library pathlibPaths()可以看到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 1Lasso标准化必须做中小样本、非线性随机森林mtry、ntree别用默认 mtry 直接上追求精度、样本充足xgbTreeeta、max_depth、nrounds参数多分步调需要可视化决策规则rpartcp、maxdepth单棵树精度有限最后说个教训。我最早做这类项目时习惯把所有处理写在一个脚本里跑通就交差结果两个月后数据更新重跑报错花了整整一天才定位到是某个字段的因子水平变了导致的predict失败。从那以后我每次交付 R 语言机器学习代码都强制走一遍“换一台干净机器 换一份新数据 只改路径”的回归测试跑不通就不算完。希望帮到你。本文还有配套的精品资源点击获取