1 基础篇
设 \(X_1,X_2,\ldots,X_n\) 是相互独立且同为正态分布 \(N(\mu,\sigma^2)\) 的一组随机变量,\(x_1,x_2,\ldots,x_n\) 是抽取自该正态分布的一组样本观察值。
1.1 问题背景
数据来源于 2021 年邵阳市统计年鉴发布的常住人口(修订版),2010-2020年邵阳市常住人口数(单位:万人)、城市化率(单位:%)。
| 年份 | 2010 | 2011 | 2012 | 2013 | 2014 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 常住人口 | 707.17 | 704.01 | 702.91 | 698.78 | 693.63 | 690.33 | 688.84 | 687.21 | 679.04 | 665.39 | 656.15 |
| 城市化率 | 32.84 | 34.16 | 35.77 | 37.94 | 39.97 | 42.44 | 44.71 | 46.76 | 48.46 | 49.74 | 52.16 |
一座城市的常住人口数受复杂的政治、经济因素影响,具有不确定性,可以看作一个随机变量,在相对稳定的内外部环境下,不考虑时间趋势性,每年的常住人口数近似服从正态分布。简单起见,将以上这组常住人口数据视为来自某正态总体的一组样本观察值。
import pandas as pd
shaoyang = pd.DataFrame(
{
"年份": [2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020],
"常住人口": [707.17, 704.01, 702.91, 698.78, 693.63, 690.33, 688.84, 687.21, 679.04, 665.39, 656.15],
"城市化率": [32.84, 34.16, 35.77, 37.94, 39.97, 42.44, 44.71, 46.76, 48.46, 49.74, 52.16]
}
)
1.2 提出假设
在方差 \(\sigma^2\) 未知的情况下,检验正态分布的均值 \(\mu\) 的情况。
\[ \begin{aligned} \quad H_0: \mu \geq \mu_0 \quad vs. \quad H_1: \mu < \mu_0 \end{aligned} \]
上式中 \(H_0\) 称为零假设(或称原假设),\(H_1\) 称为备择假设。不妨设 \(\mu_0 = 700\) 。
1.3 构造检验统计量
在样本量充足的情况下,根据中心极限定理,可以推导出如下结果,此处推导过程复杂,略。
\[ \begin{aligned} & \frac{\bar{x} - \mu}{\sigma / \sqrt{n}} \sim \mathcal{N}(0,1) \quad \frac{(n-1)s^2}{\sigma^2} \sim \chi^2(n-1) \\ & \mathsf{E}\{s^2\} = \sigma^2 \quad \mathsf{Var}\{s^2\} = \frac{2\sigma^4}{n-1} \end{aligned} \]
其中,样本均值 \(\bar{x}=\frac{1}{n}\sum_{i=1}^{n}x_i\) ,样本方差 \(s^2 = \frac{1}{n-1}\sum_{i=1}^{n}(x_i-\bar{x})^2\) 。
为了检验上面提出的问题,在总体的方差\(\sigma^2\)未知的情况下,用样本方差来替换,构造检验统计量
\[ T=\frac{\bar{x} - \mu_0}{s / \sqrt{n}} \]
该检验统计量服从自由度为 \(n-1\) 的 \(t\) 分布,此处推导过程复杂,略。
对于上述检验问题,拒绝域的形式是根据备择假设来定的。
\[ \{T < t_{\alpha}(n-1)\} \]
其中,\(\alpha\) 称为检验的显著性水平,一般取 0.05。控制犯第一类错误(拒真错误)的概率,即原假设成立的情况下,样本观察值落入拒绝域的概率,这个概率要控制在0.05 以内。只有这样,检验的结果才有说服力。说服力来源于小概率原理,即小概率事件不可能在一次实验中发生。若发生了,就不是小概率事件。有点反证法的味道!其中这个小概率的量化,一般取 0.05,事件发生的概率小于或等于 0.05,即认为此事件为小概率事件。
1.4 计算检验统计量的值和 P 值
t 分布的数学公式是非常复杂的,常见的统计软件中内置了有关的计算函数。t 分布的概率密度函数如下:
\[ f(x; n) = \frac{\Gamma\left(\frac{n+1}{2}\right)}{\sqrt{n\pi} \Gamma\left(\frac{n}{2}\right)} \left(1 + \frac{x^2}{n}\right)^{-\frac{n+1}{2}} \]
其中,\(n\) 是自由度。t 分布的累积分布函数如下:
\[ F(x; n) = \int_{-\infty}^{x} f(w; n) dw \]
# 样本均值
shaoyang["常住人口"].mean()
## np.float64(688.4963636363636)
# 样本标准差
shaoyang["常住人口"].std()
## np.float64(16.13687409179812)
# 检验统计量
T = (shaoyang["常住人口"].mean() - 700) / (shaoyang["常住人口"].std()/len(shaoyang) ** 0.5)
T
## np.float64(-2.3643516907814464)
# 临界值
import scipy.stats as stats
# 计算 alpha 处的分位数
ppf = stats.t.ppf(0.05, df=len(shaoyang)-1) # 5% 分位数
ppf
## np.float64(-1.8124611228116767)
注意到检验统计量 T 的值确实小于临界值,样本观察值落在拒绝域中。说明,要拒绝原假设,即原正态总体的均值是小于 700 的。
# 计算 P 值
stats.t.cdf(T, df=len(shaoyang)-1)
## np.float64(0.019828842503383064)
其实,scipy 模块的统计部分有一些假设检验的函数。下面直接调用单样本 T 检验函数stats.ttest_1samp来计算 T 值和 P 值。
t_stat, p_value = stats.ttest_1samp(a=shaoyang["常住人口"], popmean=700, alternative='less')
# T 检验统计量
print(t_stat)
## -2.364351690781446
# P 值
print(p_value)
## 0.019828842503383095
计算结果和前面手搓的一样。邵阳市常住人口数总体上大于 700 万吗?根据现有的数据,这个问题的答案是否定的。
2 进阶篇
还是前面这个数据集,邵阳市常住人口数还是服从参数未知的正态分布 \(N(\mu,\sigma^2)\) 。现在考察的问题是邵阳市常住人口数在置信水平为 \(1-\alpha\) 下的置信区间,\(\alpha\) 通常取 0.05,那么置信水平就是 0.95。
2.1 置信区间
现在的问题是根据这组样本观察值,构造一个区间,使得正态总体的均值/真值落入该区间的可能性/概率为 95% 。换句话说,我们要找一个区间,这个区间覆盖总体的真值的可能性/概率不低于 95% 。置信区间覆盖总体的真值的概率即覆盖概率,这个覆盖概率要大于 95% ,或者接近置信水平。
res = stats.ttest_1samp(a=shaoyang["常住人口"], popmean=700, alternative='less')
# 置信区间
res.confidence_interval(confidence_level=0.95)
## ConfidenceInterval(low=np.float64(-inf), high=np.float64(697.3148037023432))
补充在 R 软件中的代码实现,如下:
x <- c(
707.17, 704.01, 702.91, 698.78, 693.63,
690.33, 688.84, 687.21, 679.04, 665.39, 656.15
)
sd(x)
## [1] 16.14
t.test(x, mu = 700, alternative = "less")
##
## One Sample t-test
##
## data: x
## t = -2.4, df = 10, p-value = 0.02
## alternative hypothesis: true mean is less than 700
## 95 percent confidence interval:
## -Inf 697.3
## sample estimates:
## mean of x
## 688.5
计算结果与前面完全一致。做假设检验的同时,可以获得参数真值的置信区间。
2.2 功效水平
一个检验的功效函数(Power function)是样本观察值落在拒绝域的概率。功效是用来衡量检验统计量的检验效果的,功效越大,表示检验统计量越灵敏,实际表现是用更少的样本量就可以对原假设问题作出结论。在业务中,减少样本量意味着减少时间和实验成本。在 Python 模块 scipy 的统计部分中有功效计算函数 stats.power,归在重抽样和蒙特卡罗方法栏目下。
在这个单样本的 t 检验问题下,检验的对象是正态分布的均值参数 \(\mu\),功效函数记为 \(g(\mu)\) ,它与显著性水平 \(\alpha\) (也是犯第一类错误的概率)和犯第二类错误的概率 \(\beta\) 的关系。
\[ \begin{aligned} g(\mu) = \left\{ \begin{array}{l} \alpha(\mu) & H_0 ~\mathrm{成立} \\ 1-\beta(\mu) & H_1 ~\mathrm{成立} \end{array} \right. \end{aligned} \]
\[ \begin{aligned} g(\mu) &= P_{\mu}\{\bar{x} < \mu_0\} \\ &= P_{\mu}\{\frac{\bar{x}-\mu}{s/\sqrt{n}} < \frac{ \mu_0-\mu}{s/\sqrt{n}}\} \\ &= P_{\mu}\{t < \frac{ \mu_0-\mu}{s/\sqrt{n}} \} \\ &= \int_{-\infty}^{\frac{ \mu_0-\mu}{s/\sqrt{n}}} f(x;n-1)dx \end{aligned} \] 其中,将 \(\frac{\bar{x}-\mu}{s/\sqrt{n}}\) 看作自由度为 \(n-1\) 的 \(t\) 分布的随机变量,而 \(f(x)\) 是相应的 \(t\) 分布的概率密度函数。当将样本均值 \(\bar{x}\) 代入 \(\mu\) 时,Power 值就是前面计算的 P 值。
2.2.1 Python 语言实现
首先接着用 scipy 内的 stats.power() 函数来计算功效,在这里,Power 的计算是通过抽样,结果存在随机性,设置随机数种子,以便可复现。
import numpy as np
rng = np.random.default_rng(seed=20262026)
# 总体(常住人口)的分布是正态分布
# 样本均值和样本标准差来替代总体均值和总体标准差
# 将从这个分布中生成随机数
rvs = lambda size: rng.normal(loc=shaoyang["常住人口"].mean(), scale=shaoyang["常住人口"].std(), size=size)
# 单样本 t 检验
test = stats.ttest_1samp
# 样本量
n_obs = len(shaoyang["常住人口"])
# 默认样本量是 n_resamples=10000
res = stats.power(test=test, rvs=rvs, n_observations=n_obs, significance=0.05, kwargs={'popmean': 700, 'alternative': 'less'})
# 输出结果
res
## PowerResult(power=np.float64(0.7109), pvalues=array([0.01801163, 0.08560967, 0.00089626, ..., 0.03789451, 0.02300109,
## 0.00028921], shape=(10000,)))
# 打印 power 值
res.power
## np.float64(0.7109)
# P 值小于 0.05 的比例表示功效
np.mean(res.pvalues < 0.05)
## np.float64(0.7109)
Python 里的 statsmodels 模块也有计算功效的方法。
from statsmodels.stats.power import TTestPower
# Cohen's d,即均值差除以标准差
effect_size = (shaoyang["常住人口"].mean()-700)/shaoyang["常住人口"].std()
effect_size
## np.float64(-0.7128788573422227)
# 显著性水平
alpha=0.05
res = TTestPower().power(
effect_size=effect_size,
nobs=n_obs,
alpha=alpha,
alternative='smaller' # 注意与 effect_size、备择假设的符号保持一致
)
print(res)
## 0.710660157004632
2.2.2 R 语言实现
给定各参数值的情况下,计算功效的大小。在 R 软件中,有类似的函数 power.t.test() 。
power.t.test(
n = length(x), delta = abs(mean(x) - 700),
sd = sd(x), sig.level = 0.05,
type = "one.sample",
alternative = "one.sided" # 这里没有方向可选,delta 必须为正,计算结果才对
)
##
## One-sample t test power calculation
##
## n = 11
## delta = 11.5
## sd = 16.14
## sig.level = 0.05
## power = 0.7107
## alternative = one.sided
# 前面 t 检验等价的功效计算
library(pwr)
pwr.t.test(
d = (mean(x)-700)/sd(x), # Cohen's d
n = length(x),
sig.level = 0.05,
type = "one.sample",
alternative = "less" # 注意与备择假设、参数 d 的方向保持一致
)
##
## One-sample t test power calculation
##
## n = 11
## d = -0.7129
## sig.level = 0.05
## power = 0.7107
## alternative = less
pwr.t.test(
d = (700-mean(x))/sd(x), # Cohen's d
n = length(x),
sig.level = 0.05,
type = "one.sample",
alternative = "greater" # 同上
)
##
## One-sample t test power calculation
##
## n = 11
## d = 0.7129
## sig.level = 0.05
## power = 0.7107
## alternative = greater
综上,在 Python 环境中,推荐使用 statsmodels 模块来计算功效,而在 R 环境中,推荐使用 pwr 包来计算功效。
3 思考题
根据邵阳市统计局发布的统计年鉴显示,2023 年邵阳市年末常住人口 635.88 万人,其中,城镇人口 345.46 万人,农村人口 290.42 万人,城市化率 54.33% 。邵阳市的城市化率大于 50% 吗?根据这个问题,提出一个原假设(也叫零假设)和备择假设。
历年邵阳市的城市化水平数据如下:
import pandas as pd
shaoyang = pd.DataFrame(
{
"年份": [2017, 2018, 2019, 2020, 2021, 2022, 2023],
"常住人口": [737.54, 737.05, 730.24, 656.15, 646.83, 641.78, 635.88],
"城镇人口": [338.46, 350.02, 356.2, 342.22, 343.05, 344.29, 345.46],
"农村人口": [399.08, 387.03, 374.04, 313.93, 303.78, 297.49, 290.42],
"城市化率": [45.89, 47.49, 48.78, 52.16, 53.04, 53.65, 54.33]
}
)
from great_tables import GT
(GT(shaoyang))
| 年份 | 常住人口 | 城镇人口 | 农村人口 | 城市化率 |
|---|---|---|---|---|
| 2017 | 737.54 | 338.46 | 399.08 | 45.89 |
| 2018 | 737.05 | 350.02 | 387.03 | 47.49 |
| 2019 | 730.24 | 356.2 | 374.04 | 48.78 |
| 2020 | 656.15 | 342.22 | 313.93 | 52.16 |
| 2021 | 646.83 | 343.05 | 303.78 | 53.04 |
| 2022 | 641.78 | 344.29 | 297.49 | 53.65 |
| 2023 | 635.88 | 345.46 | 290.42 | 54.33 |
注意:2021 年发布的统计年鉴对 2019 年及以前的人口数据做了一次修订,上表中保留了修订之前的数据。
邵阳市下辖一共 12 个市区县,其中,双清区,大祥区和北塔区是邵阳市直管的城区,武冈市和邵东市是邵阳市代管的县级市。
import pandas as pd
shaoyang = pd.DataFrame(
{
"市县名称": ["双清区", "大祥区", "北塔区", "新邵县", "邵阳县", "隆回县", "洞口县", "绥宁县", "新宁县", "城步县", "武冈市", "邵东市"],
"常住人口": [32.01, 36.32, 12.37, 58.59, 71.45, 98.02, 65.71, 28.32, 49.78, 22.11, 62.01, 99.19],
"城镇人口": [28.83, 32.02, 9.94, 27.36, 34.03, 44.04, 33.01, 12.12, 23.76, 9.85, 31.97, 58.53],
"农村人口": [3.18, 4.3, 2.43, 31.23, 37.42, 53.98, 32.7, 16.2, 26.02, 12.26, 30.04, 40.66],
"城市化率": [90.07, 88.16, 80.36, 46.7, 47.63, 44.93, 50.24, 42.8, 47.73, 44.55, 51.56, 59.01]
}
)
from great_tables import GT
(GT(shaoyang))
| 市县名称 | 常住人口 | 城镇人口 | 农村人口 | 城市化率 |
|---|---|---|---|---|
| 双清区 | 32.01 | 28.83 | 3.18 | 90.07 |
| 大祥区 | 36.32 | 32.02 | 4.3 | 88.16 |
| 北塔区 | 12.37 | 9.94 | 2.43 | 80.36 |
| 新邵县 | 58.59 | 27.36 | 31.23 | 46.7 |
| 邵阳县 | 71.45 | 34.03 | 37.42 | 47.63 |
| 隆回县 | 98.02 | 44.04 | 53.98 | 44.93 |
| 洞口县 | 65.71 | 33.01 | 32.7 | 50.24 |
| 绥宁县 | 28.32 | 12.12 | 16.2 | 42.8 |
| 新宁县 | 49.78 | 23.76 | 26.02 | 47.73 |
| 城步县 | 22.11 | 9.85 | 12.26 | 44.55 |
| 武冈市 | 62.01 | 31.97 | 30.04 | 51.56 |
| 邵东市 | 99.19 | 58.53 | 40.66 | 59.01 |
以上两份城市化率数据,应该使用哪份数据来做检验?应该使用第一份数据,检验方法见A/B 实验之统计指标的计算。
4 运行环境
reticulate::py_config()
## python: /opt/.virtualenvs/r-tensorflow/bin/python
## libpython: /opt/homebrew/opt/python@3.13/Frameworks/Python.framework/Versions/3.13/lib/python3.13/config-3.13-darwin/libpython3.13.dylib
## pythonhome: /opt/.virtualenvs/r-tensorflow:/opt/.virtualenvs/r-tensorflow
## virtualenv: /opt/.virtualenvs/r-tensorflow/bin/activate_this.py
## version: 3.13.11 (main, Dec 5 2025, 16:06:33) [Clang 17.0.0 (clang-1700.4.4.1)]
## numpy: /opt/.virtualenvs/r-tensorflow/lib/python3.13/site-packages/numpy
## numpy_version: 2.3.4
##
## NOTE: Python version was forced by RETICULATE_PYTHON_ENV