48

抽样与模拟

随机抽样与蒙特卡洛

统计建模篇
R
library(tidyverse)

本章目的是在tidyverse的架构下,介绍一些模拟和抽样的知识。先回顾下Hadley Wickham提出的数据科学tidy原则,tidy思想体现在:

  • 任何数据都可以规整为数据框
  • 数据框的一列代表一个变量,数据框的一行代表一次观察
  • 函数处理数据时,数据框进、数据框出

模拟

生成随机数

比如生成5个高斯分布的随机数,高斯分布就是正态分布,R语言里我们用rnorm()函数产生正态分布的随机数

R
rnorm(n = 5, mean = 0, sd = 1)

事实上,R内置了很多随机数产生的函数

DistrutionNotationR
Uniform$\text{U}(a, b)$runif
Normal$\text{N}(\mu, \sigma)$rnorm
Binormal$\text{Bin}(n, p)$rbinorm
Piosson$\text{pois}(\lambda)$rpois
Beta$\text{Beta}(\alpha, \beta)$rbeta

如果大家查看帮助文档?runif,会发现每种分布都有对应的四个函数

  • d:density
  • p:cumulative probability
  • q:quantile
  • r:random
R
dnorm(seq(0.1, 0.5, length.out = 10), mean = 0, sd = 1)

在tidyverse的框架下,我们喜欢在数据框(data.frame)下运用这些函数,因为这样我们可以方便使用ggplot2来可视化,

  • 例子1,我们生成100个正态分布的点,然后看看其分布
R
tibble(
  x = rnorm(n = 100, mean = 0, sd = 1)
) %>%
  ggplot(aes(x = x)) +
  geom_density()

我们将模拟的正态分布和理论上正态分布画在一起

R
tibble(
  x = rnorm(n = 100, mean = 0, sd = 1)
) %>%
  ggplot(aes(x = x)) +
  geom_density() +
  stat_function(
    fun = dnorm,
    args = list(mean = 0, sd = 1),
    color = "red"
  )

如果我们模拟点再增加点,会越来越逼近理论上的分布。

  • 例子2,在数据框(data.frame)下,建立模拟$x$和$y$的线性关系

$$ y_i = 4 + 3.2\, x_i$$ 现实中,观察值往往会带入误差,假定误差服从正态分布,那么$x$和$y$的线性关系重新表述为 $$ y_i = \beta_0 + \beta_1\, x_i + \epsilon_i, \quad \epsilon \in \text{Normal}(\mu =0, \sigma =1) $$

R
beta_0 <- 4
beta_1 <- 3.2
epsilon <- rnorm(n = 1000, mean = 0, sd = 1)

sim_normal <- tibble(
  # x_vals = runif(1000, 0, 10)
  x_vals = seq(from = 0, to = 5, length.out = 1000),
  y_vals = beta_0 + beta_1 * x_vals + epsilon,
)

sim_normal %>% head()
R
sim_normal %>%
  ggplot(aes(x = x_vals, y = y_vals)) +
  geom_point()

有时候为了方便,可以写简练点

R
tibble(
  a = runif(1000, 0, 5),
  b = 4 + rnorm(1000, mean = 3.2 * a, sd = 1)
) %>%
  ggplot(aes(x = a, y = b)) +
  geom_point()

MASS::mvrnorm

MASS::mvrnorm(n = 1, mu, Sigma)产生多元高斯分布的随机数,每组随机变量高度相关。 比如人的身高服从正态分布,人的体重也服从正态分布,同时身高和体重又存在强烈的关联。

  • n: 随机样本的大小
  • mu: 多元高斯分布的均值向量
  • Sigma: 协方差矩阵,主要这里是大写的S (Sigma),提醒我们它是一个矩阵,不是一个数值
R
a <- 3.5
b <- -1
sigma_a <- 1
sigma_b <- 0.5
rho <- -0.7

mu <- c(a, b)
cov_ab <- sigma_a * sigma_b * rho # covariance

构建协方差矩阵

R
sigma <- matrix(c(
  sigma_a^2, cov_ab,
  cov_ab, sigma_b^2
), ncol = 2)
R
d <- MASS::mvrnorm(1000, mu = mu, Sigma = sigma) %>%
  data.frame() %>%
  set_names("group_a", "group_b")

head(d)
R
d %>%
  ggplot(aes(x = group_a)) +
  geom_density(
    color = "transparent",
    fill = "dodgerblue3",
    alpha = 1 / 2
  ) +
  stat_function(
    fun = dnorm,
    args = list(mean = 3.5, sd = 1),
    linetype = 2
  )
R
d %>%
  ggplot(aes(x = group_b)) +
  geom_density(
    color = "transparent",
    fill = "dodgerblue3",
    alpha = 1 / 2
  ) +
  stat_function(
    fun = dnorm,
    args = list(mean = -1, sd = 0.5),
    linetype = 2
  )
R
d %>%
  ggplot(aes(x = group_a, y = group_b)) +
  geom_point() +
  stat_ellipse(type = "norm", level = 0.95)

我们回头验算一下

R
d %>% summarise(
  a_mean = mean(group_a),
  b_mean = mean(group_b),
  a_sd = sd(group_a),
  b_sd = sd(group_b),
  cor = cor(group_a, group_b),
  cov = cov(group_a, group_b)
)

蒙特卡洛

这是我研究生时候老师布置的一个的题目,当时我用的是C语言代码,现在我们有强大的tidyverse

R
set.seed(2019)

n <- 50000
points <- tibble("x" = runif(n), "y" = runif(n))
R
points <- points %>%
  mutate(inside = map2_dbl(.x = x, .y = y, ~ if_else(.x**2 + .y**2 &lt; 1, 1, 0))) %>%
  rowid_to_column("N")

正方形的面积是1,圆的面积是$\pi r^2 = \frac{1}{4} \pi$,如果知道两者的比例,就可以估算$\pi$

R
points <- points %>%
  mutate(estimate = 4 * cumsum(inside) / N)
R
points %>% tail()
R
points %>%
  ggplot() +
  geom_line(aes(y = estimate, x = N), colour = "#82518c") +
  geom_hline(yintercept = pi)

抽样与样本

总体分布

假定一个事实,川师男生总体的平均身高和身高的标准差分别为

R
true.mean <- 175.7
true.sd <- 15.19

那么我们可以模拟分布情况如下

R
pop.distn <-
  tibble(
    height = seq(100, 250, 0.5),
    density = dnorm(height, mean = true.mean, sd = true.sd)
  )

ggplot(pop.distn) +
  geom_line(aes(height, density)) +
  geom_vline(
    xintercept = true.mean,
    color = "red",
    linetype = "dashed"
  ) +
  geom_vline(
    xintercept = true.mean + true.sd,
    color = "blue",
    linetype = "dashed"
  ) +
  geom_vline(
    xintercept = true.mean - true.sd,
    color = "blue",
    linetype = "dashed"
  ) +
  labs(
    x = "Height (cm)", y = "Density",
    title = "Height distribution of boys"
  )

样本

假定我们从中抽取30个男生身高样本

R
sample.a <-
  tibble(height = rnorm(n = 30, mean = true.mean, sd = true.sd))

然后看看样本的直方图

R
sample.a %>%
  ggplot(aes(x = height)) +
  geom_histogram(aes(y = stat(density)),
    fill = "steelblue",
    alpha = 0.75,
    bins = 10
  ) +
  geom_line(
    data = pop.distn,
    aes(x = height, y = density),
    alpha = 0.25, size = 1.5
  ) +
  geom_vline(xintercept = true.mean, linetype = "dashed", color = "red") +
  geom_vline(xintercept = mean(sample.a$height), linetype = "solid")

红色的虚线代表分布的总体的均值,黑色实线代表30个样本的均值,

R
sample.a %>%
  summarize(
    sample.mean = mean(height),
    sample.sd = sd(height)
  )

也就是说,基于这30个观察值的样本,我们认为川师男生的身高均值为[R表达式]cm,方差为[R表达式]

可能有同学说,这个样本太少了,计算的均值还不够科学,会以偏概全。于是又重新找了30个男生,和上次类似,用rnorm函数模拟,我们记为样本b

R
sample.b <-
  tibble(height = rnorm(30, mean = true.mean, sd = true.sd))

再来看看这次样本的分布

R
sample.b %>%
  ggplot(aes(x = height)) +
  geom_histogram(aes(y = stat(density)),
    fill = "steelblue", alpha = 0.75, bins = 10
  ) +
  geom_line(
    data = pop.distn, aes(x = height, y = density),
    alpha = 0.25, size = 1.5
  ) +
  geom_vline(xintercept = true.mean, linetype = "dashed", color = "red") +
  geom_vline(xintercept = mean(sample.a$height), linetype = "solid")

同样,我们计算样本b的均值和方差

R
sample.b %>%
  summarize(
    sample.mean = mean(height),
    sample.sd = sd(height)
  )

这次抽样的结果,均值为[R表达式]cm,方差为[R表达式]

和样本a比,有一点点变化。不经想问,我能否继续抽样呢?结果会有变化吗?为了避免重复写代码 ,我把上面的过程整合到一起,写一个子函数,专门模拟抽样过程

R
rnorm.stats <- function(n, mu, sigma) {
  the.sample <- rnorm(n, mu, sigma)
  tibble(
    sample.size = n,
    sample.mean = mean(the.sample),
    sample.sd = sd(the.sample)
  )
}

于是,我们又可以继续模拟了。注意我们之前设定的总体分布的均值和方差

R
true.mean <- 175.7
true.sd <- 15.19
R
rnorm.stats(30, true.mean, true.sd)

yes,代码工作的很好,但不过只是代码减少了一点点,仍然只是一次抽样(这里30个样本为一次抽样),我们的目的是反复抽样, 抽很多次的那种喔。

那我们用purrr包的rerun函数偷个懒,

R
df.samples.of.30 <-
  purrr::rerun(2500, rnorm.stats(30, true.mean, true.sd)) %>%
  dplyr::bind_rows()

哇,一下子抽了2500个样本,全部装进了df.sample.of.30这个数据框, 偷偷看一眼呢

R
df.samples.of.30 %>% head()

回过头看看df.samples.of.30是什么:

  • 从川师的男生中随机抽取30个,计算这30个人身高的均值和方差,这叫一次抽样
  • 把上面的工作,重复2500次,得到2500个均值和方差
  • 2500个均值和方差,组成了一个数据框

我们发现每次抽样的均值都不一样,感觉又像一个分布(抽样的均值分布),我们画出来看看吧

R
df.samples.of.30 %>%
  ggplot(aes(x = sample.mean, y = stat(density))) +
  geom_histogram(bins = 25, fill = "firebrick", alpha = 0.5) +
  geom_vline(xintercept = true.mean, linetype = "dashed", color = "red") +
  labs(
    title = "Distribution of mean heights for 2500 samples of size 30"
  )

注意到,这不是男生身高的分布,而是每次抽样计算的均值构成的分布.

为了更清楚的说明,我们把整体的分布(灰色曲线)、样本a(蓝色直方图)、抽样的均值分布(红色直方图)三者画在一起。

R
df.samples.of.30 %>%
  ggplot(aes(x = sample.mean, y = stat(density))) +
  geom_histogram(bins = 50, fill = "firebrick", alpha = 0.5) +
  geom_histogram(
    data = sample.a,
    aes(x = height, y = stat(density)),
    bins = 11, fill = "steelblue", alpha = 0.25
  ) +
  geom_vline(xintercept = true.mean, linetype = "dashed", color = "red") +
  geom_line(data = pop.distn, aes(x = height, y = density), alpha = 0.25, size = 1.5) +
  xlim(125, 225)

样本的均值分布,是个很有意思的结果,比如,我们再选30个男生再抽样一次,我们可以断定,这次抽样的均值会落在了红色的区间之内。

然而,注意到,必须限定再次抽样的大小仍然是30个男生,以上这句话才成立。

R
df.samples.of.30 %>%
  summarize(
    mean.of.means = mean(sample.mean),
    sd.of.means = sd(sample.mean)
  )

这里计算的是抽样(样本大小为30)均值分布,而不是整体的均值分布。言外之意,样本大小可以是其它的呗, 那就把样本调整为50、100、250、500分别试试看

R
df.samples.of.50 <-
  rerun(2500, rnorm.stats(50, true.mean, true.sd)) %>%
  bind_rows()

df.samples.of.100 <-
  rerun(2500, rnorm.stats(100, true.mean, true.sd)) %>%
  bind_rows()

df.samples.of.250 <-
  rerun(2500, rnorm.stats(250, true.mean, true.sd)) %>%
  bind_rows()

df.samples.of.500 <-
  rerun(2500, rnorm.stats(500, true.mean, true.sd)) %>%
  bind_rows()

忍不住想画图看看,每次抽取的男生数量不同,均值的分布会有不同?

R
df.combined <-
  bind_rows(
    df.samples.of.30,
    df.samples.of.50,
    df.samples.of.100,
    df.samples.of.250,
    df.samples.of.500
  ) %>%
  mutate(sample.sz = as.factor(sample.size))
R
df.combined %>%
  ggplot(aes(x = sample.mean, y = stat(density), fill = sample.sz)) +
  geom_histogram(bins = 25, alpha = 0.5) +
  geom_vline(xintercept = true.mean, linetype = "dashed") +
  facet_wrap(vars(sample.sz), nrow = 1) +
  scale_fill_brewer(palette = "Set1") +
  labs(
    x = "Sample means", y = "Density",
    title = "Distribution of mean heights for samples of varying size"
  )

随着样本大小由30增加到500,抽样的均值分布围绕着越来越聚合到实际的均值,或者说随着样本大小的增多,对均值估计的不确定性越小。

R
sampling.distn.mean.table <-
  df.combined %>%
  group_by(sample.size) %>%
  summarize(
    mean.of.means = mean(sample.mean),
    sd.of.means = sd(sample.mean)
  )
sampling.distn.mean.table

有个统计学上的概念需要明确。

输出结果的第三列sd.of.means 是不同样本大小(30,50,100,250,500)下,反复抽样后平均数分布的标准差。

数学上,如果已知总体的标准差($\sigma$),那么抽取无限多份大小为 $n$ 的样本,每个样本各有一个平均值,所有这个大小的样本之平均值的标准差可证明为

$$ \frac{\sigma}{\sqrt{n}} $$ 即,平均值的标准误差

下面我们画图看看,模拟出来的$sd.of.means$和理论值$\frac{\sigma}{\sqrt{n}}$是否一致。

注意到这里的$\sigma$是总体的标准差,即最开始我们设定的川师男生身高的标准差true.sd. 也就说,理论上

R
df.se.mean.theory <- tibble(
  sample.size = seq(10, 500, 10)
) %>%
  mutate(std.error = true.sd / sqrt(sample.size))

df.se.mean.theory

平均值标准误差随样本大小变化

R
sampling.distn.mean.table %>%
  ggplot(aes(x = sample.size, y = sd.of.means)) +
  geom_point() +
  geom_line(aes(x = sample.size, y = std.error),
    data = df.se.mean.theory,
    color = "red"
  ) +
  labs(
    x = "Sample size", y = "Std Error of Mean",
    title = "Standard error of mean value varies with sample size (comparison between theoretical value and simulated value)"
  )

两者吻合的很好。

刚刚我们看到的,抽样均值分布随着样本大小变化而变化。可以试想下,抽样的其他统计量分布(方差,中位数),是不是也随着样本大小变化而变化呢?

R
sampling.distn.sd.table <-
  df.combined %>%
  group_by(sample.size) %>%
  summarize(
    mean.of.sds = mean(sample.sd),
    sd.of.sds = sd(sample.sd)
  )

sampling.distn.sd.table

答案是肯定的,样本量的增多,抽样方差的不确定性减少。

扩展阅读

  • <https://learningstatisticswithr.com/book/>
R
# remove the objects
# rm(list=ls())
rm(
  a, b,
  beta_0, beta_1,
  cov_ab, d,
  df.combined, df.samples.of.100,
  df.samples.of.250, df.samples.of.30,
  df.samples.of.50, df.samples.of.500,
  df.se.mean.theory, epsilon,
  mu, n,
  points, pop.distn,
  rho, rnorm.stats,
  sample.a, sample.b,
  sampling.distn.mean.table, sampling.distn.sd.table,
  sigma, sigma_a,
  sigma_b, sim_normal,
  true.mean, true.sd
)
R
pacman::p_unload(pacman::p_loaded(), character.only = TRUE)