R 语言入门实战|第九章 循环与模拟:用自动化任务解锁数据科学概率思维
引言:循环 —— 数据科学的 “重复性任务自动化” 利器
在数据分析中,我们经常需要处理重复性任务:比如模拟 1000 次抛硬币、计算 10000 种符号组合的奖金、估算老虎机的长期返还率。手动完成这些任务既低效又易出错,而 R 语言的循环功能正是解决这类问题的核心工具。
《R 语言入门与实践》第九章围绕 “循环” 展开,通过 “计算期望值→生成组合→循环模拟” 的逻辑,教我们用for
/while
/repeat
循环实现自动化任务,最终通过老虎机模拟案例揭露 “返还率” 的计算逻辑。本章的核心价值在于:掌握循环的高效用法,理解 “统计模拟” 的本质,为后续复杂数据分析(如蒙特卡洛模拟)打下基础。
一、核心基础:期望值与expand.grid
函数
在学习循环前,我们需要先掌握 “期望值计算” 和 “全组合生成” 两个前置技能 —— 前者是模拟的理论基础,后者是循环的常见处理对象。
1.1 期望值:模拟的 “理论锚点”
书中案例:不均匀骰子的期望值
假设骰子点数 1-5 的概率为 1/8,点数 6 的概率为 3/8,计算单次掷骰子的期望值:
# 1. 定义点数和对应概率
die <- 1:6
prob <- c(rep(1/8, 5), 3/8) # 1-5概率1/8,6概率3/8# 2. 计算期望值
expected_value <- sum(die * prob)
expected_value # 输出:4.125
解析:通过 “点数 × 概率” 求和,得到长期平均点数为 4.125,高于均匀骰子的 3.5。
拓展案例:抽奖游戏的期望收益
某抽奖游戏:10 元抽 1 次,中 100 元概率 0.05,中 20 元概率 0.1,不中概率 0.85,计算期望收益:
# 1. 定义收益(成本10元,所以中100元实际收益90元)
earnings <- c(90, 10, -10) # 中100元、中20元、不中
prob <- c(0.05, 0.1, 0.85)# 2. 计算期望收益
expected_earning <- sum(earnings * prob)
expected_earning # 输出:-4.5
解析:长期来看,每次抽奖平均亏损 4.5 元,揭示 “抽奖对玩家不利” 的本质。
1.2 expand.grid
:全组合生成的 “效率工具”
expand.grid
是第九章的核心新函数,用于生成多个向量的所有可能组合(笛卡尔积),是处理 “多变量组合” 的必备工具(如老虎机 3 个符号的所有组合、骰子的点数组合)。
核心参数
参数 | 作用说明 | 类型要求 | 默认值 |
---|---|---|---|
... | 输入向量(需生成组合的多个向量) | 任意类型向量 | 无 |
stringsAsFactors | 是否将字符型向量转换为因子 | 逻辑型(TRUE/FALSE) | TRUE |
书中案例:生成两个骰子的所有点数组合
# 1. 定义单个骰子
die <- 1:6# 2. 生成两个骰子的所有组合(36种)
dice_combos <- expand.grid(die1 = die, die2 = die, stringsAsFactors = FALSE)# 3. 查看前5行
head(dice_combos)
# die1 die2
# 1 1 1
# 2 2 1
# 3 3 1
# 4 4 1
# 5 5 1
# 6 6 1
解析:expand.grid
返回数据框,每行对应一种组合,列名默认为Var1
/Var2
,可手动指定(如die1
/die2
)。
拓展案例:生成商品属性的所有组合
某商品有 “颜色”(红、蓝)和 “尺寸”(S、M、L),生成所有属性组合:
# 1. 定义属性向量
color <- c("red", "blue")
size <- c("S", "M", "L")# 2. 生成所有组合
product_combos <- expand.grid(颜色 = color, 尺寸 = size, stringsAsFactors = FALSE)# 3. 查看结果
product_combos
# 颜色 尺寸
# 1 red S
# 2 blue S
# 3 red M
# 4 blue M
# 5 red L
# 6 blue L
解析:快速生成 6 种组合,可直接用于 “商品 SKU 创建”“实验设计” 等场景。
二、循环核心:for
/while
/repeat
的实战用法
循环的本质是 “重复执行一段代码”,R 提供三种循环结构,适用场景不同,其中for
循环最常用。
2.1 for
循环:“固定次数” 的重复性任务
for
循环适用于已知重复次数的场景(如模拟 1000 次游戏、计算 343 种符号组合的奖金)。
语法结构
for (变量 in 序列) {# 重复执行的代码块
}
- 变量:每次循环取 “序列” 中的一个值(如 1:1000);
- 序列:循环的范围(如整数序列、字符向量)。
书中案例:计算老虎机所有符号组合的奖金
老虎机有 7 种符号,生成 3 个符号的 343 种组合,用for
循环计算每种组合的奖金:
# 1. 加载之前定义的score函数(处理钻石百搭逻辑)
score <- function(symbols) {diamonds <- sum(symbols == "DD")cherries <- sum(symbols == "C")slots <- symbols[symbols != "DD"]same <- length(unique(slots)) == 1bars <- all(slots %in% c("B", "BB", "BBB"))if (diamonds == 3) {prize <- 100} else if (same) {payouts <- c("7" = 80, "BBB" = 40, "BB" = 25, "B" = 10, "C" = 10, "0" = 0)prize <- payouts[slots[1]]} else if (bars) {prize <- 5} else if (cherries > 0) {prize <- c(0, 2, 5)[cherries + diamonds + 1]} else {prize <- 0}prize * (2 ^ diamonds)
}# 2. 生成3个符号的所有组合
wheel <- c("DD", "7", "BBB", "BB", "B", "C", "0")
combos <- expand.grid(wheel, wheel, wheel, stringsAsFactors = FALSE)
colnames(combos) <- c("sym1", "sym2", "sym3")# 3. 用for循环计算奖金(关键:提前创建存储对象)
combos$prize <- NA # 提前创建空列存储奖金
for (i in 1:nrow(combos)) {# 提取第i行的符号组合symbols <- c(combos$sym1[i], combos$sym2[i], combos$sym3[i])# 计算奖金并存储combos$prize[i] <- score(symbols)
}# 4. 查看前5行结果
head(combos)
# sym1 sym2 sym3 prize
# 1 DD DD DD 800
# 2 7 DD DD 0
# 3 BBB DD DD 0
# 4 BB DD DD 0
# 5 B DD DD 0
# 6 C DD DD 10
关键技巧:提前用combos$prize <- NA
创建存储对象,避免循环中 “动态扩展向量”(会导致速度极慢)。
拓展案例:模拟 1000 次抛硬币,计算正面概率
# 1. 定义模拟参数
n <- 1000 # 模拟次数
results <- character(n) # 提前存储每次结果# 2. for循环模拟抛硬币(1=正面,0=反面)
set.seed(123) # 设置随机种子,结果可复现
for (i in 1:n) {results[i] <- sample(c("正面", "反面"), size = 1, prob = c(0.5, 0.5))
}# 3. 计算正面概率
head(results) # 前5次结果:正面 反面 正面 正面 反面 正面
正面次数 <- sum(results == "正面")
正面概率 <- 正面次数 / n
正面概率 # 输出:0.502(接近理论概率0.5)
解析:通过固定次数的循环模拟,验证 “大数定律”—— 次数越多,频率越接近概率。
2.2 while
循环:“条件触发” 的重复性任务
while
循环适用于未知重复次数、仅知终止条件的场景(如模拟 “直到输光所有钱” 的游戏次数)。
语法结构
while (条件) {# 重复执行的代码块(需包含改变条件的逻辑)
}
- 条件:逻辑测试(
TRUE
时循环继续,FALSE
时终止); - 注意:必须在代码块中加入 “改变条件” 的逻辑(如减少现金),否则会陷入无限循环。
书中案例:模拟 “输光现金” 的老虎机游戏次数
# 1. 定义play函数(返回单次奖金)
play <- function() {symbols <- sample(wheel, size = 3, replace = TRUE, prob = c(0.03, 0.03, 0.06, 0.1, 0.25, 0.01, 0.52))score(symbols)
}# 2. while循环模拟“输光100元”
plays_till_broke <- function(start_cash) {cash <- start_cashn_plays <- 0 # 记录游戏次数while (cash > 0) {cash <- cash - 1 + play() # 每次投入1元,加奖金n_plays <- n_plays + 1}return(n_plays)
}# 3. 运行模拟
set.seed(456)
plays_till_broke(100) # 输出:268(输光100元需268次游戏)
解析:循环终止条件是cash > 0
,每次循环更新现金和游戏次数,避免无限循环。
拓展案例:模拟 “直到连续出现 2 次正面” 的抛硬币次数
# 函数:计算直到连续2次正面的抛硬币次数
till_two_heads <- function() {count <- 0 # 总次数last <- "反面" # 上一次结果while (TRUE) {current <- sample(c("正面", "反面"), size = 1)count <- count + 1# 终止条件:当前和上一次都是正面if (current == "正面" && last == "正面") {break # 强制终止循环}last <- current # 更新上一次结果}return(count)
}# 运行5次模拟
set.seed(789)
sapply(1:5, function(x) till_two_heads()) # 输出:3 2 5 2 4
解析:用while(TRUE)
创建 “无限循环”,通过break
在满足条件时终止,灵活处理 “未知次数” 场景。
2.3 repeat
循环:“强制终止” 的重复性任务
repeat
循环是 “无条件循环”,必须通过break
强制终止,适用场景与while
类似,但语法更简洁(无需初始条件)。
语法结构
repeat {# 重复执行的代码块if (条件) {break # 强制终止循环}
}
书中案例:用repeat
重写 “输光现金” 模拟
plays_till_broke_repeat <- function(start_cash) {cash <- start_cashn_plays <- 0repeat {# 终止条件:现金≤0if (cash <= 0) {break}cash <- cash - 1 + play()n_plays <- n_plays + 1}return(n_plays)
}# 运行模拟
set.seed(456)
plays_till_broke_repeat(100) # 输出:268(与while循环结果一致)
解析:repeat
循环先执行代码块,再判断条件,逻辑上与while
的 “先判断后执行” 略有差异,但结果一致。
拓展案例:生成 “直到和为 10” 的两个骰子点数
# 用repeat生成和为10的骰子组合
repeat {die1 <- sample(1:6, 1)die2 <- sample(1:6, 1)if (die1 + die2 == 10) {cat(paste("找到组合:", die1, "+", die2, "=10"))break}
}
# 输出:找到组合: 4 + 6 =10
解析:无需预设循环次数,直到满足 “和为 10” 的条件才终止,适合 “目标导向” 的任务。
三、综合实战:用循环计算老虎机的真实返还率
结合第九章核心知识点,通过 “生成组合→计算概率→循环算奖金→算期望值” 四步,揭露老虎机的返还率真相。
步骤 1:生成所有符号组合
# 老虎机符号及概率
wheel <- c("DD", "7", "BBB", "BB", "B", "C", "0")
prob <- c(0.03, 0.03, 0.06, 0.1, 0.25, 0.01, 0.52)# 生成3个符号的所有组合(343种)
combos <- expand.grid(wheel, wheel, wheel, stringsAsFactors = FALSE)
colnames(combos) <- c("s1", "s2", "s3")
步骤 2:计算每种组合的概率
由于符号生成独立,组合概率 = 各符号概率的乘积:
# 为每个符号列匹配概率
combos$p1 <- prob[match(combos$s1, wheel)]
combos$p2 <- prob[match(combos$s2, wheel)]
combos$p3 <- prob[match(combos$s3, wheel)]# 计算组合总概率
combos$total_prob <- combos$p1 * combos$p2 * combos$p3# 验证概率和为1(确保计算正确)
sum(combos$total_prob) # 输出:1.0(正确)
步骤 3:用for
循环计算每种组合的奖金
# 提前创建奖金列
combos$prize <- NA# 循环计算奖金
for (i in 1:nrow(combos)) {symbols <- c(combos$s1[i], combos$s2[i], combos$s3[i])combos$prize[i] <- score(symbols)
}
步骤 4:计算返还率(期望值)
返还率 = 奖金 × 概率的总和:
payout_rate <- sum(combos$prize * combos$total_prob)
payout_rate # 输出:0.934(即93.4%,与制造商声称的92%接近)
拓展实战:用循环模拟 10 万次游戏验证返还率
# 模拟10万次游戏
set.seed(101)
n_sim <- 100000
winnings <- numeric(n_sim) # 提前存储奖金for (i in 1:n_sim) {symbols <- sample(wheel, size = 3, replace = TRUE, prob = prob)winnings[i] <- score(symbols)
}# 计算模拟返还率
sim_payout_rate <- mean(winnings)
sim_payout_rate # 输出:0.936(与理论值0.934接近)
解析:通过 “理论计算” 和 “模拟验证” 双重确认返还率,体现循环在统计模拟中的核心价值。
四、循环避坑指南:新手必知的 3 个高效技巧
坑 1:循环内动态扩展存储对象(速度极慢)
错误案例:
# 错误:每次循环扩展向量
start_time <- Sys.time()
winnings <- c() # 空向量
for (i in 1:100000) {symbols <- sample(wheel, 3, prob = prob)winnings <- c(winnings, score(symbols)) # 动态扩展
}
Sys.time() - start_time # 耗时:~20秒
正确案例:提前创建固定长度的存储对象
# 正确:提前分配空间
start_time <- Sys.time()
winnings <- numeric(100000) # 预设长度
for (i in 1:100000) {symbols <- sample(wheel, 3, prob = prob)winnings[i] <- score(symbols)
}
Sys.time() - start_time # 耗时:~1秒(快20倍!)
坑 2:忽略set.seed()
导致结果不可复现
解决:在循环前设置随机种子,确保每次运行结果一致:
set.seed(123) # 固定种子
sample(wheel, 3, prob = prob) # 每次运行都返回:"B" "0" "0"
坑 3:过度依赖循环(R 的强项是向量化)
提示:循环适合 “复杂逻辑”(如score
函数的多条件判断),但简单计算优先用向量化(如vec * 2
),速度更快。
五、第九章核心小结
核心工具与场景:
expand.grid
:生成多向量全组合(如符号、骰子、商品属性);for
循环:已知次数的重复任务(如模拟固定次数游戏、计算组合奖金);while
/repeat
:未知次数的条件触发任务(如输光现金、寻找目标组合)。
高效循环三原则:
- 提前创建存储对象(避免动态扩展);
- 用
set.seed()
确保可复现; - 复杂逻辑用循环,简单计算用向量化。
实战价值:循环是 “统计模拟” 的基石,通过第九章的老虎机案例,我们掌握了 “从理论期望值到实际模拟” 的完整流程,这一技能可直接迁移到蒙特卡洛模拟、A/B 测试、风险评估等高级数据分析场景。
下一章(第十章)将聚焦 “代码提速”,教我们如何用 “向量化编程” 优化循环,让 R 代码快如闪电!