回归诊断中的强影响点:从标准化残差到影响图
回归系数由全部样本共同决定。若个别点位置特殊,它对结果的话语权可能远大于其余点,甚至单独决定斜率正负。回归诊断就是查这件事:有没有这样的点、影响多大、要不要处理。
下面用一份模拟数据过一遍常用诊断量——各衡量什么、阈值取多少、图怎么看。先讲最基础的三个(标准化残差、杠杆值、库克距离),再看整合它们的影响图,最后补三个删除诊断(DFFITS、DFBETAS、COVRATIO)。
三种"不正常"的点
三个易混的概念先分清:
- 离群值(outlier):$y$ 远离预测值、残差大,$y$ 方向异常。
- 高杠杆点(high leverage):自变量远离其余样本,$x$ 方向异常;杠杆衡量它能对结果施加多大影响。
- 强影响点(influential point):删掉它回归方程会明显改变,不一定伴随大残差。
杠杆高未必影响大,残差大也未必撼动方程。最麻烦的是既在 $x$ 方向极端、又偏离整体趋势的点。下面造出这三类点,看每个指标各能抓到谁。
模拟数据
用一元线性回归演示,好处是回归线能直接画在散点图上、看得见被带偏;这些指标对多元回归同样成立,只是无法用二维散点图呈现。
真实关系设为 $y = 3 + 1.5x + \varepsilon$,生成 40 个正常点,再刻意加进三个特殊点 A、B、C:
library(tidyverse)
set.seed(2026)
n_base <- 40
x_base <- runif(n_base, 4, 12)
y_base <- 3 + 1.5 * x_base + rnorm(n_base, 0, 1.2)
base <- tibble(x = x_base, y = y_base, label = "normal")
# B:x 居中、y 大幅偏离 —— 离群点
# C:x 偏大、y 明显偏离趋势 —— 强影响点
xB <- 7; yB <- 3 + 1.5 * xB + 9
xC <- 13; yC <- 7
# A:x 最极端(高杠杆),但 y 落在"其余数据所隐含的那条线"上 —— 顺趋势
# 这样它虽然杠杆最高,加入后几乎不改变方程,是"好杠杆点"的典型
rest <- bind_rows(base, tibble(x = c(xB, xC), y = c(yB, yC), label = c("B", "C")))
xA <- 17
yA <- as.numeric(predict(lm(y ~ x, data = rest),
newdata = tibble(x = xA))) + 0.6
special <- tibble(label = c("A", "B", "C"),
x = c(xA, xB, xC),
y = c(yA, yB, yC))
dat <- bind_rows(base, special) |> mutate(obs = row_number())
n <- nrow(dat) # 样本量
p <- 1 # 预测变量个数(系数个数为 p + 1)
先拟合全量回归,并和"剔除 C 之后"的回归放在一张图上对比:
fit <- lm(y ~ x, data = dat)
fit_noC <- lm(y ~ x, data = filter(dat, label != "C"))
pts <- dat |>
mutate(kind = recode(label,
normal = "normal",
A = "A: high leverage",
B = "B: outlier",
C = "C: influential"))
lab_pts <- filter(pts, label != "normal")
ggplot(pts, aes(x, y)) +
geom_point(data = filter(pts, label == "normal"),
color = palette_blog["ash"], size = 2, alpha = 0.8) +
geom_abline(intercept = coef(fit)[1], slope = coef(fit)[2],
color = palette_blog["blue"], linewidth = 0.9) +
geom_abline(intercept = coef(fit_noC)[1], slope = coef(fit_noC)[2],
color = palette_blog["rose"], linewidth = 0.9, linetype = 2) +
geom_point(data = lab_pts, aes(color = kind), size = 3.4) +
geom_text(data = lab_pts, aes(label = label, color = kind),
vjust = -1, fontface = "bold", show.legend = FALSE) +
annotate("text", x = 15.5, y = 27, label = "fit: all data",
color = palette_blog["blue"], hjust = 0, size = 3.4) +
annotate("text", x = 15.5, y = 23.2, label = "fit: without C",
color = palette_blog["rose"], hjust = 0, size = 3.4) +
scale_color_manual(values = unname(palette_blog[c("sage", "gold", "rose")])) +
labs(title = "Three kinds of unusual points",
subtitle = "Dropping the single point C visibly rotates the regression line",
x = "x", y = "y", color = NULL) +
theme_blog()
三个点位置各异:A 在最右端($x$ 极端)但落在其余点连成的线上;B 的 $x$ 普通、$y$ 却高高抬起;C 的 $x$ 偏大、$y$ 又明显偏低。看实线(全量)与虚线(剔除 C)的夹角:只去掉 C,斜率就从 1.05 变到 1.33。能否左右回归线,和残差大小是两回事。
把五个诊断量一次性算出来备用,都来自 base R,不用额外装包:
d <- dat |>
mutate(
hat = hatvalues(fit),
std_resid = rstandard(fit),
cooks = cooks.distance(fit),
dffits = dffits(fit),
covratio = covratio(fit)
)
thr <- list(
hat = 2 * (p + 1) / n, # 杠杆值阈值
cooks = 4 / (n - p - 1), # 库克距离阈值
dffits = 2 * sqrt((p + 1) / n), # DFFITS 阈值
dfbetas = 2 / sqrt(n), # DFBETAS 阈值
covr = 3 * (p + 1) / n # COVRATIO 阈值(偏离 1 的幅度)
)
先写一个通用的索引图函数:横轴是观测序号,纵轴是某个诊断量,把 A/B/C 三点标出来。
hl <- c(normal = unname(palette_blog["ash"]),
A = unname(palette_blog["sage"]),
B = unname(palette_blog["gold"]),
C = unname(palette_blog["rose"]))
index_plot <- function(df, yvar, title, ylab, hlines) {
ggplot(df, aes(obs, .data[[yvar]])) +
geom_hline(yintercept = hlines, linetype = 2,
color = palette_blog["ink"], linewidth = 0.4) +
geom_segment(aes(xend = obs, yend = 0, color = label), linewidth = 0.4) +
geom_point(aes(color = label, size = label != "normal")) +
geom_text(data = filter(df, label != "normal"),
aes(label = label, color = label),
vjust = -0.9, fontface = "bold", size = 3.4, show.legend = FALSE) +
scale_color_manual(values = hl, guide = "none") +
scale_size_manual(values = c(1.6, 3), guide = "none") +
labs(title = title, x = "observation index", y = ylab) +
theme_blog()
}
指标一 · 标准化残差:抓离群值
标准化残差是残差除以自身标准误,读作"偏离回归线多少个标准误":
$$ r_i = \frac{e_i}{\hat\sigma \sqrt{1 - h_{ii}}} $$
常用它判定离群值,经验阈值取 $\pm 2.5$。R 里用 rstandard():
index_plot(d, "std_resid",
"Standardized residuals catch the outlier",
"standardized residual", hlines = c(-2.5, 2.5))
B 冲出 $\pm 2.5$ 阈值线(3.26),C 更低至 -4.88;A 只有 0.18,完全正常。标准化残差只看 $y$ 方向偏离,抓不到藏在 $x$ 极端处的高杠杆点 A。
指标二 · 杠杆值:抓 x 方向的极端
杠杆值来自帽子矩阵。多元回归可写成 $\hat{Y} = HY$,其中
$$ H = X (X^{\top} X)^{-1} X^{\top} $$
叫帽子矩阵是因为它给 $Y$ 戴上帽子变成 $\hat{Y}$。对角线元素 $h_{ii}$ 即第 $i$ 个观测的杠杆值,衡量其自变量有多极端;超过 $2(p+1)/n$ 算高杠杆。R 里用 hatvalues():
index_plot(d, "hat",
"Leverage catches the extreme-x points",
"leverage (hat value)", hlines = thr$hat)
越线的是 A 和 C,残差很大的 B 因 $x$ 普通反而贴在底部。杠杆值只看 $x$ 的极端、不看 $y$,正好和标准化残差互补——一个管 $x$、一个管 $y$;但单看任一个都不足以判断有无影响。
指标三 · 库克距离:抓真正的强影响点
库克距离(Cook’s distance)把前两者合一,同时考虑残差与杠杆:
$$ D_i = \frac{r_i^{2}}{p + 1} \cdot \frac{h_{ii}}{1 - h_{ii}} $$
只有残差大、杠杆又高的点,$D_i$ 才被两个因子同时放大。阈值取 $4/(n - p - 1)$,R 里用 cooks.distance():
index_plot(d, "cooks",
"Cook's distance isolates the truly influential point",
"Cook's distance", hlines = thr$cooks)
C 遥遥领先($D_C =$ 1.74),把阈值线远远甩下;B 残差大但杠杆低,仅勉强越线;杠杆最高的 A 因顺着趋势,库克距离几乎为 0(0.008)。高杠杆不等于强影响。
影响图:一张图看全
把三个指标放进同一张图是常用画法:横轴杠杆值、纵轴标准化残差、点大小对应库克距离,超阈值的点填实。这种图叫影响图(influence plot)或气泡图(bubble plot):
d <- d |> mutate(flagged = cooks > thr$cooks)
ggplot(d, aes(hat, std_resid)) +
geom_hline(yintercept = c(-2.5, 2.5), linetype = 2,
color = palette_blog["ash"], linewidth = 0.4) +
geom_vline(xintercept = thr$hat, linetype = 2,
color = palette_blog["ash"], linewidth = 0.4) +
geom_point(aes(size = cooks, fill = flagged, color = flagged),
shape = 21, alpha = 0.75) +
geom_text(data = filter(d, label != "normal"),
aes(label = label), fontface = "bold",
vjust = -1.1, size = 3.6, color = palette_blog["ink"]) +
scale_size_area(max_size = 13, guide = "none") +
scale_fill_manual(values = c(`TRUE` = unname(palette_blog["rose"]),
`FALSE` = "white"), guide = "none") +
scale_color_manual(values = c(`TRUE` = unname(palette_blog["rose"]),
`FALSE` = unname(palette_blog["ash"])),
guide = "none") +
annotate("text", x = thr$hat, y = 4.6,
label = "leverage threshold", hjust = -0.05,
color = palette_blog["ash"], size = 3.2) +
coord_cartesian(ylim = c(-6.4, 5), clip = "off") +
labs(title = "Influence (bubble) plot",
subtitle = "x = leverage, y = standardized residual, bubble area = Cook's distance",
x = "leverage (hat value)", y = "standardized residual") +
theme_blog()
各位置的含义:
- 越靠右杠杆越高,越远离 0 残差越大,气泡越大库克距离越大。
- C 在右下角,杠杆高、负残差大、气泡最大且填实,最该关注。
- B 在左上,残差越过 $y=2.5$ 但杠杆低、气泡不大,只是离群点。
- A 在最右侧贴着 $y=0$,杠杆最高但残差近零、气泡极小,高杠杆却无影响。
要盯的是既靠右、又远离中线、气泡还大的点。
剔除强影响点,方程变了多少
C 被诊断出来后,最直接的检验是删掉它、看系数变化多少:
comp <- tibble(
term = c("Intercept (b0)", "Slope (b1)"),
full = round(coef(fit), 2),
without_C = round(coef(fit_noC), 2)
) |>
mutate(change_pct = round((without_C - full) / abs(full) * 100, 1))
knitr::kable(comp, col.names = c("Coefficient", "All data", "Without C", "Change (%)"),
align = "lrrr")
| Coefficient | All data | Without C | Change (%) |
|---|---|---|---|
| Intercept (b0) | 6.08 | 4.42 | -27.3 |
| Slope (b1) | 1.05 | 1.33 | 26.7 |
斜率从 1.05 抬到 1.33(约 +26%),截距也明显回落。一个点就能改写"$x$ 每增一单位、$y$ 平均变化多少"的结论。样本量小时这种点必须查清:录入错误、量纲不一致,还是本不该纳入的异常记录。
样本量很大时单点几乎不可能有这么大权重,专门找强影响点意义有限;但在异常检测里,找出这种点恰恰是目的本身。
再细一点:三个删除诊断
实务里还常用三个更细的诊断量,思路一致:删掉第 $i$ 点重新拟合,看某个量变化多少。influence.measures(fit) 一行即可全算,这里为对照阈值分开算。
DFFITS 衡量删掉第 $i$ 点后,其自身拟合值变化了多少个标准误:
$$ \text{DFFITS}i = \frac{\hat{y}i - \hat{y}{(i)}}{\hat\sigma{(i)} \sqrt{h_{ii}}} $$
阈值 $2\sqrt{(p+1)/n}$。它和库克距离高度相关,是从单点自身拟合值的角度看同一件事。
DFBETAS 更细,衡量删掉第 $i$ 点后每个回归系数各变化了多少个标准误:
$$ \text{DFBETAS}{j,i} = \frac{\hat\beta_j - \hat\beta{j(i)}}{\hat\sigma_{(i)} \sqrt{(X^{\top} X)^{-1}_{jj}}} $$
阈值 $2/\sqrt{n}$。好处是能定位到具体系数,告诉你这个点动的是哪一个 $\beta$:
dfb <- dfbetas(fit) |>
as_tibble() |>
rename(`Intercept (b0)` = `(Intercept)`, `Slope (b1)` = x) |>
mutate(obs = dat$obs, label = dat$label) |>
pivot_longer(c(`Intercept (b0)`, `Slope (b1)`),
names_to = "coef", values_to = "dfbetas")
ggplot(dfb, aes(obs, dfbetas)) +
geom_hline(yintercept = c(-thr$dfbetas, thr$dfbetas), linetype = 2,
color = palette_blog["ink"], linewidth = 0.4) +
geom_segment(aes(xend = obs, yend = 0, color = label), linewidth = 0.4) +
geom_point(aes(color = label, size = label != "normal")) +
geom_text(data = filter(dfb, label != "normal"),
aes(label = label, color = label),
vjust = -0.7, fontface = "bold", size = 3, show.legend = FALSE) +
scale_color_manual(values = hl, guide = "none") +
scale_size_manual(values = c(1.4, 2.6), guide = "none") +
facet_wrap(~ coef) +
labs(title = "DFBETAS pinpoints which coefficient a point distorts",
x = "observation index", y = "DFBETAS") +
theme_blog()
两个面板里都是 C 冲得最远,截距和斜率都被它拽出阈值带,A、B 基本没动,与前面"删掉 C 系数大变"一致。
COVRATIO 换个角度,衡量删掉第 $i$ 点后系数估计的精度(协方差矩阵行列式)变化多少:
$$ \text{COVRATIO}i = \frac{\det\big(\hat\sigma{(i)}^{2} (X_{(i)}^{\top} X_{(i)})^{-1}\big)}{\det\big(\hat\sigma^{2} (X^{\top} X)^{-1}\big)} $$
阈值是偏离 1 的幅度超过 $3(p+1)/n$,即 $\lvert \text{COVRATIO}_i - 1 \rvert \ge 3(p+1)/n$。读法和前面不同:
- $\text{COVRATIO}_i > 1$:删掉后估计变差,说明它在提升精度;
- $\text{COVRATIO}_i < 1$:删掉后估计变好,说明它在拖累精度。
band <- c(1 - thr$covr, 1 + thr$covr)
ggplot(d, aes(obs, covratio)) +
annotate("rect", xmin = -Inf, xmax = Inf, ymin = band[1], ymax = band[2],
fill = palette_blog["ash"], alpha = 0.15) +
geom_hline(yintercept = 1, linetype = 2,
color = palette_blog["ink"], linewidth = 0.4) +
geom_point(aes(color = label, size = label != "normal")) +
geom_text(data = filter(d, label != "normal"),
aes(label = label, color = label),
vjust = -0.9, fontface = "bold", size = 3.4, show.legend = FALSE) +
scale_color_manual(values = hl, guide = "none") +
scale_size_manual(values = c(1.6, 3), guide = "none") +
labs(title = "COVRATIO separates good leverage from bad",
subtitle = "Above the band = improves precision; below = hurts precision",
x = "observation index", y = "COVRATIO") +
theme_blog()
这张图的看点在 A:前面几个指标都放过了它,只有 COVRATIO 标出它。A 的 COVRATIO 为 1.56(带子上方),说明这个高杠杆点在稳住估计、删了反而更不准,是"好"杠杆点。C 则掉到 0.21(带子下方很远),删掉它精度明显变好,是要处理的坏点。
把三个点在所有指标上的表现汇成一张表:
mark <- function(cond) ifelse(cond, "✔", "–")
summ <- d |>
filter(label != "normal") |>
transmute(
Point = label,
`Std. residual` = mark(abs(std_resid) > 2.5),
`Leverage` = mark(hat > thr$hat),
`Cook's D` = mark(cooks > thr$cooks),
DFFITS = mark(abs(dffits) > thr$dffits),
COVRATIO = mark(abs(covratio - 1) > thr$covr)
)
knitr::kable(summ, align = "lccccc")
| Point | Std. residual | Leverage | Cook’s D | DFFITS | COVRATIO |
|---|---|---|---|---|---|
| A | – | ✔ | – | – | ✔ |
| B | ✔ | – | ✔ | ✔ | ✔ |
| C | ✔ | ✔ | ✔ | ✔ | ✔ |
- A(高杠杆点):只有 COVRATIO 标记,且是"好"的那种,其余指标都判无害。
- B(离群点):残差类指标抓得到,杠杆类指标漏掉。
- C(强影响点):几乎所有指标同时点名,才是真正要处理的点。
小结
这套指标各有分工:
- 标准化残差看 $y$ 方向偏离(离群值);
- 杠杆值看 $x$ 方向极端;
- 库克距离和 DFFITS 合两者,衡量综合影响、锁定强影响点;
- DFBETAS 定位到具体哪个系数被动;
- COVRATIO 从精度角度看,还能分出好坏杠杆。
实际顺序:先看影响图扫出可疑大气泡,再用 DFBETAS 看它扰动了哪个系数、多大,最后结合业务判断修正、剔除还是保留。记住:样本量小时单点足以改写结论、务必查清;样本量大时不必纠结单点,把力气留给异常检测更划算。