← 返回 Tessera
散点2 张图mtcars(R 内置,32 行)2026-08-04

scatter_fit

两个连续变量之间是什么关系——趋势有多强,分组之后还成不成立?

需要的输入
2 个数值变量,同一批观测(可选:再加 1 个分组变量)
示例数据
mtcars(R 内置,32 行)
依赖
ggplot2 · ggpubr
配色
未命名#74A9CF #2B8CBE #045A8D
成图预览另有 1 张在配方里
scatter_fit

什么时候用它

这是最基本的那张图:两个数值变量,一个点是一次观测。相关系数、回归斜率、以及一切"两个量有关系"的说法,都必须先在这张图上站得住。

correlation_heatmap 那一页反复说"回到散点图确认",说的就是这里。相关系数把一段关系压成一个数,而压缩必然丢东西——丢掉的正是形状:是直是弯、有没有离群点在主导、是不是几个分组各自成团拼出来的假趋势。这些在热图上全都看不见,在这张图上一眼就有。

怎么读

  1. 先看形状,别急着看线。 点云是沿一条直线散开,还是拐了个弯、或者分成几团?拟合线画在任何点云上都会出来一条,它不负责告诉你该不该画。
  2. 看离散程度。 点贴着线还是甩得很开。r 高只说明贴得紧,不说明斜率大;斜率大也不说明贴得紧。这是两件事。
  3. 找孤立点。 一个远离点云的观测能显著改变斜率和 r,尤其在 x 方向上远的那种(杠杆点)。看到就单独查一下它是什么。
  4. 看置信带宽度。 两端变宽是正常的——那里数据少。带子宽到能塞进一条水平线,就说明"有没有趋势"这件事本身都还没定。
  5. 再问一次有没有分组。 这是最容易跳过的一步,也是最容易翻车的一步:整体一条趋势,拆开之后可能完全是另一回事。

常见陷阱

  • 整体斜率不等于组内斜率。 下面方法 B 就是例子:mpg ~ wt 整体斜率 −5.34,但 6 缸组是 −2.78、8 缸组是 −2.19,只有整体的一半上下。多出来的那一半来自组间偏移——8 缸车又重又费油,两个方向同时偏,把整条拟合线拽陡了。极端情况下组内斜率会和整体反号,那就是辛普森悖论。
  • 别在小组里认真读斜率。 上面那三组里 6 缸只有 7 辆车。分组拟合最容易出的问题不是算错,是把 n=7 的斜率当成结论
  • geom_smooth() 默认不是直线。 不写 method 时,n < 1000 走的是 loess。想要直线必须显式写 method = "lm",否则你以为在看线性关系,其实在看一条会跟着局部数据扭的曲线。
  • 别外推。 拟合线只在数据覆盖的 x 范围内有依据。xlim() 会把点连同拟合一起裁掉再重算,要放大看局部得用 coord_cartesian()
  • 点重叠了就不是散点图了。 几百个点以上会糊成一片,那时该上 alphageom_hex() 或者二维密度——散点图的前提是数得清。

配色

分组变量 cyl4 / 6 / 8 缸,这是有序的分类,而且图上没有任何别的通道在表达这个顺序(x 轴是车重,不是缸数)。所以这里用单色渐深的三档,缸数越多颜色越深——顺序读得出来,而且黑白打印后深浅仍然能排。

这也是这一条没有引用本站配色的原因:站内那六套是定性或发散型,定性色的整个设计目标就是让人分不出先后,正好和这里要的相反。配色是按变量类型选的,不是按好不好看选的。

拟合线用比点更深的同色系,让线压在点云上面还能认出属于哪一组。

配方

方法绘图系统什么时候选它
Ageom_point() + geom_smooth()ggplot2只有两个变量,先看整体是什么形状
B+ 按分组拆开拟合ggplot2手上还有一个分组变量——**只要有,就该拆一次看看**

数据准备

mtcars 直接能用,只要把 cyl 转成因子——不转的话它是数值 4/6/8,会被当成连续量映射成渐变色标,而它其实是三个组。

library(ggplot2)
library(ggpubr)

d <- mtcars
d$cyl <- factor(d$cyl)   # 不转的话图例会变成一根连续色条

cyl_cols <- c("4" = "#74A9CF", "6" = "#2B8CBE", "8" = "#045A8D")

方法 A · 一条拟合线

#| fig: basic
#| fig-width: 7
#| fig-height: 5
ggplot(d, aes(x = wt, y = mpg)) +
  geom_point(size = 2.6, alpha = 0.85, color = "#2B8CBE") +
  geom_smooth(
    method  = "lm",       # 必须显式写 —— 默认在 n < 1000 时是 loess,会画出曲线
    formula = y ~ x,      # 显式给公式,否则每次画图都提示一行 message
    se      = TRUE,       # 置信带:它宽不宽比线本身斜不斜更值得看
    color = "#045A8D", fill = "#045A8D", alpha = 0.15, linewidth = 0.9
  ) +
  # r 和 p 直接标在图上,省得读者去别处找
  stat_cor(method = "pearson", label.x = 3.8, label.y = 33, size = 4.2) +
  labs(
    x = "Weight (1000 lbs)", y = "Miles per gallon",
    title = "mpg vs wt — mtcars"
  ) +
  theme_pubr(base_size = 12)
scatter_fit — basic
scatter_fit-basic

点云沿一条直线散得挺紧,r 也很高。到这里为止,"车越重越费油"这个结论看着毫无问题。

方法 B · 按分组拆开

同一份数据,只多映射一个 color = cyl,结论就要改口了。

#| fig: grouped
#| fig-width: 7.5
#| fig-height: 5
ggplot(d, aes(x = wt, y = mpg, color = cyl)) +
  geom_point(size = 2.6, alpha = 0.9) +
  geom_smooth(
    method = "lm", formula = y ~ x,
    se = FALSE,        # 组内样本少,置信带会宽到互相盖住,反而看不清三条线
    linewidth = 0.9
  ) +
  # 整体那条虚黑线:group = 1 覆盖掉分组映射,让它跨所有点拟合一次
  geom_smooth(
    aes(group = 1), method = "lm", formula = y ~ x,
    se = FALSE, color = "grey30", linetype = "dashed", linewidth = 0.8
  ) +
  scale_color_manual(values = cyl_cols, name = "Cylinders") +
  labs(
    x = "Weight (1000 lbs)", y = "Miles per gallon",
    title = "Same data, split by cylinder count",
    subtitle = "Dashed line: pooled fit across all 32 cars"
  ) +
  theme_pubr(base_size = 12, legend = "right")
scatter_fit — grouped
scatter_fit-grouped

三条实线明显比那条虚线。整体斜率 −5.34,组内是 −5.65 / −2.78 / −2.19——6 缸和 8 缸只有整体的一半上下。差出来的部分不是"重量更伤油耗",而是三个组本身就分别占着图的不同角落:8 缸车集中在右下(重、费油),4 缸车集中在左上(轻、省油)。拟合线穿过这三团的连线,斜率自然比任何一团内部都陡。

两种方法怎么选

  1. 只有两个变量就是 A。 没有分组变量可拆的时候,A 就是完整答案。
  2. 手上有分组变量,就一定要画一次 B。 哪怕最后放进正文的是 A——先看一眼分组会不会改变结论,这一步的成本是一行 color =,不看的代价是整段结论可能是组间偏移伪装出来的。
  3. B 不一定要进正文。 组数多、每组样本少的时候,B 会是一团互相穿插的线,读起来比 A 差得多。那种情况下正确的做法是:自己画 B 确认过,正文放 A,并在文字里交代分组之后趋势是否一致。
运行环境2 个包 · 2026-08-04

R version 4.5.1 (2025-06-13 ucrt) · x86_64-w64-mingw32

  • ggplot2 4.0.3
  • ggpubr 0.6.2