当前位置: 首页 > news >正文

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),速度更快。

五、第九章核心小结

  1. 核心工具与场景

    • expand.grid:生成多向量全组合(如符号、骰子、商品属性);
    • for循环:已知次数的重复任务(如模拟固定次数游戏、计算组合奖金);
    • while/repeat:未知次数的条件触发任务(如输光现金、寻找目标组合)。
  2. 高效循环三原则

    • 提前创建存储对象(避免动态扩展);
    • set.seed()确保可复现;
    • 复杂逻辑用循环,简单计算用向量化。
  3. 实战价值:循环是 “统计模拟” 的基石,通过第九章的老虎机案例,我们掌握了 “从理论期望值到实际模拟” 的完整流程,这一技能可直接迁移到蒙特卡洛模拟、A/B 测试、风险评估等高级数据分析场景。

下一章(第十章)将聚焦 “代码提速”,教我们如何用 “向量化编程” 优化循环,让 R 代码快如闪电!

http://www.dtcms.com/a/392373.html

相关文章:

  • [论文阅读] 人工智能 + 软件工程 | 4907个用户故事验证!SEEAgent:解决敏捷估计“黑箱+不协作”的终极方案
  • 鸿蒙Next ArkTS卡片开发指南:从入门到实战
  • 【绕过disable_function】
  • 使用云手机运行手游的注意事项有哪些?
  • 【数据结构】利用堆解决 TopK 问题
  • 2025陇剑杯现场检测
  • openharmony之充电空闲状态定制开发
  • 【开题答辩全过程】以 python的线上订餐系统为例,包含答辩的问题和答案
  • (附源码)基于Spring Boot的校园心理健康服务平台的设计与实现
  • 微信小程序开发教程(十八)
  • 寰宇光锥舟架构图
  • Spring Bean生命周期全面解析
  • [vibe code追踪] 侧边栏UI管理器 | showSidebarContent
  • 嵌入式ARM架构学习9——IIC
  • 多线程——线程安全的练习加感悟
  • 使用 TwelveLabs 的 Marengo 视频嵌入模型与 Amazon Bedrock 和 Elasticsearch
  • Windows 11 下 Notepad++ 等应用无法启动问题排查修复
  • 面向口齿不清者的语音识别新突破:用大模型拯救“听不懂”的声音
  • 服装企业优化信息化管理系统的最佳软件选择
  • 多阶段构建镜像
  • 推荐一个开源服务器一键自动重装系统脚本:reinstall
  • 【C++进阶】C++11 的新特性 | lambda | 包装器
  • 2.【QT 5.12.12 安装 Windows 版本】
  • Rust_2025:阶段1:day6.3 macro
  • 【Qt开发】输入类控件(一)-> QLineEdit
  • python10——组合数据类型(集合)
  • 分布式专题——14 RabbitMQ之集群实战
  • WEEX唯客的多维度安全守护
  • 深度学习环境配置
  • 生鲜速递:HTTP 的缓存控制