R 中如何使用 left_join 按累积概率随机分配职业?
可以用 left_join() 配合 join_by() 中的 closest():先按 homeworld 和 species 匹配,再找到大于或等于当前随机数的最小累积概率上限。分配规则可以放在独立表中,无需逐条编写 case_when()。
可直接使用的代码
这段代码需要 dplyr 1.1.0 或更高版本。为区分抽样值和规则阈值,规则表中的累积概率列命名为 cumulative_prob。
library(dplyr)
occupations <- tribble(
~homeworld, ~species, ~occupation, ~cumulative_prob,
"Tatooine", "Human", "moisture farmer", 0.5,
"Tatooine", "Human", "pilot", 0.8,
"Tatooine", "Human", "mechanic", 1.0
)
set.seed(123)
result <- starwars %>%
mutate(random_draw = runif(n())) %>%
left_join(
occupations,
by = join_by(
homeworld,
species,
closest(random_draw <= cumulative_prob)
),
na_matches = "never"
)
runif(n()) 会为每行生成一个随机数,这里不需要 rowwise()。set.seed() 用于复现抽样:输入数据和行顺序保持一致时,重新运行代码会得到相同的分配结果。
为什么需要 closest()
连接条件中的这一行决定了如何选择职业:
closest(random_draw <= cumulative_prob)
它先找出满足 random_draw <= cumulative_prob 的规则,再从中选择最接近随机数的上限,也就是满足条件的最小上限。
对于 Tatooine 和 Human 这一组,结果与原来的 case_when() 一致:
| 随机数范围 | 匹配上限 | occupation |
|---|---|---|
0 <= random_draw <= 0.5 | 0.5 | moisture farmer |
0.5 < random_draw <= 0.8 | 0.8 | pilot |
0.8 < random_draw < 1 | 1.0 | mechanic |
如果只写 random_draw <= cumulative_prob,当随机数为 0.4 时,三个上限都满足条件,连接结果就会出现多行。加上 closest() 后,会选择上限为 0.5 的规则,前提是该组没有重复记录这个上限。
规则表需要满足的条件
cumulative_prob 保存的是累积概率。职业概率为 0.5、0.3、0.2 时,对应的上限应为 0.5、0.8、1.0,不能直接把三项职业概率作为连接阈值。
每个 homeworld、species 组合内的累积概率应位于 [0, 1],按职业分配顺序严格递增,最后一个上限为 1。同一组合下如果存在重复上限,closest() 仍可能返回多条匹配记录;它不会从并列规则中随机选一条。
没有对应规则的输入行会由 left_join() 保留,新增的 occupation 和 cumulative_prob 为 NA。代码中的 na_matches = "never" 表示缺失的连接键不相互匹配,避免把未知的 homeworld 或 species 当成有效规则。
如果最终只需要职业,可以在连接后删除辅助列:
result <- result %>%
select(-random_draw, -cumulative_prob)
备注:内容仅供参考。
