Home

回归诊断中的强影响点:从标准化残差到影响图

· xiebro

在解释性建模里,拟合出一条回归线只是开始。真正的问题往往是:这条线可信吗?会不会有那么一两条记录,在背后悄悄操纵着整个方程?《数据科学家的实用统计学》(Bruce、Bruce、Gedeck)第 4 章"回归诊断"给出了一套判断工具——它们不直接关乎预测准确度,却能告诉你模型对数据的拟合到底稳不稳。

本文用一份模拟数据把这套诊断量逐一演示出来:每个指标度量什么、经验阈值怎么定、图怎么读、最后怎么据此做决策。主线严格沿用书中这一节的术语与阈值(标准化残差、杠杆值、库克距离,以及本章的招牌可视化——影响图/气泡图);文末再单开一节,补上书中未涉及、但经典回归诊断教材(如 Montgomery)常用的三个"删除诊断"——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$ 很普通,却高高地偏离;C 的 $x$ 偏大、$y$ 又明显偏低。注意实线(全量)与虚线(剔除 C)之间的夹角——只删掉 C 一个点,斜率就从 1.05 跳到 1.33。这正是书中图 4-5 想说的:一个点能不能左右回归线,和它残差大不大,是两回事。

下面把五个诊断量一次性算出来备用。它们全部来自 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)

这次换成 AC 越线,而残差巨大的 B 却贴在底部——它的 $x$ 太普通了。杠杆值完全不看响应变量 $y$,只问"$x$ 有多极端"。可见它和标准化残差恰好互补:一个管 $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)。这里复刻书中图 4-6:

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 之后,最实在的检验就是把它删掉,看系数变化多大——这对应书中的表 4-2:

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$ 平均变化多少"这个核心结论。 在小样本里,这种事必须查清楚:是录入错误?是量纲不一致?还是一笔本就不该纳入的异常记录?

书中也提醒:对大数据里的预测型回归,单个点几乎不可能有这么大权重,识别强影响点意义有限;但在异常检测场景里,找出这种点恰恰是全部目的所在。

书外延伸:删除诊断三兄弟

到这里,书中这一节的内容就讲完了。不过实务中还有三个更细的诊断量常被用到——它们不在本书范围内,属于经典回归诊断教材(如 Montgomery《线性回归分析导论》)的标准配置。三者共享同一个思想:删掉第 $i$ 个点,重新拟合,看某个量变化了多少。 R 里一行 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)查它到底扰动了哪个系数、扰动多大 → 结合业务判断是修正、剔除还是保留。别忘了书里的提醒:小样本里单点足以改写结论,务必查清;而在大数据的预测任务里,与其纠结单点,不如把"识别强影响点"的力气留给真正需要它的异常检测场景。