第18讲:图表美化、多图合并与交互式图表初探

数据挖掘与R语言

2026年11月03日

本讲导览

Polish, Compose & Interact — 项目四收官课

  • 一、前课测验与导入
  • 二、主题与标签
  • 三、分面与拼接
  • 四、坐标轴与交互
  • 五、综合实战
  • 六、项目四收官

重要

前三讲我们学会了用 ggplot2 画柱状图、折线图、散点图、箱形图和直方图——今天是项目四"数据可视化"的最后一课:让图表更专业、把多张图拼在一起排版、初步认识可交互的图表,并对整个项目四做一次总结。

本讲要用两个新包:patchwork(拼多图)和 plotly(做交互图)。第一次用到的那一页会当场写出 library(),先把包装好即可。

本讲学习目标

学完这一讲,你能够:

  • 用 theme_minimal() 等内置主题和 theme() 微调,把"毛坯图"改成"精装图"
  • 用 labs() 补齐标题、副标题、带单位的轴名和数据来源脚注
  • 分清两种多图思路:facet_wrap() 拆着看,patchwork 拼着看(library(patchwork))
  • 用 scale_y_continuous(limits = ) 安排坐标轴的起止点,用 coord_cartesian() 放大局部而不删数据,并判断什么时候该抬高起点、什么时候必须从 0 起
  • 用 ggplotly() 把静态图一键变成交互式图表(library(plotly))

一、前课测验与情境导入

第 17 讲:直方图、散点图与箱形图 → 今天:让图更专业

1.1 选择题

1. 想看清纽约每日最高气温这一个数值变量的分布形态,最合适画: A. 条形图 B. 直方图 C. 箱形图 D. 折线图

2. 想在一张图上考察 company_data 里营收与成本这两个数值变量之间有没有关系,应该加的图层是: A. geom_col() B. geom_point() C. geom_boxplot() D. geom_histogram()

1.2 填空题

3. 数据表中已经计算好了各公司的营业收入,现需要以公司名称为横轴、营业收入为纵轴绘制柱状图,应使用 geom_______();数据表中有一列公司的信用评级(如 AAA、AA、A、BBB),现需要绘制各评级公司的数量对比柱状图,应使用 geom_______()。

4. 箱形图中,箱子两端的边是上、下四分位数,箱子盖住的是中间 ______% 的数据;须以外的点可能是 ______ 值。

提示

图选对了、结论读准了,还差最后一步——让图看起来专业。今天补这一步。

1.3 贯穿数据(一):年度财务表

company_data 是项目四前三讲一直在用的教学虚构数据,今天当场生成、一键复现:

▶️ 查看代码
company_data <- data.frame(         # 12 家公司的年度财务表
  company = c("锦城科技", "蜀汉制造", "天府物流", "青羊金融",
              "锦江生物", "浣花实业", "武侯地产", "龙泉食品",
              "新都电子", "双流航运", "温江农科", "郫都建材"),
  revenue = c(1300, 980, 1560, 760, 2100, 890,   # 营收(万元)
              1180, 540, 1720, 1420, 360, 680),
  cost = c(1000, 650, 900, 500, 900, 620,        # 成本(万元)
           850, 380, 1180, 1010, 260, 540),
  rating = c("A", "B", "B", "C", "A", "C",       # 内部信用评级
             "B", "C", "C", "B", "C", "C")
)

#company_data <- company_data %>%    # 管道:在原表上追加一列
    # 利润 = 营收 − 成本

1.4 贯穿数据(二):季度营收表

▶️ 查看代码
quarter_wide <- data.frame(         # 3 家公司 × 4 个季度
  company = rep(c("锦城科技", "蜀汉制造", "天府物流"),  # 公司名
                each = 4),          # 每家公司连续 4 行
  quarter = rep(c("Q1", "Q2", "Q3", "Q4"),   # 季度
                times = 3),         # 整组重复 3 遍
  revenue = c(300, 330, 305, 365,   # 锦城科技四个季度
              250, 260, 250, 220,   # 蜀汉制造四个季度
              290, 330, 400, 540)   # 天府物流四个季度
)

提示

两份数据的数字第 16 讲已逐个核对:12 家公司评级 A 类 2 家、B 类 4 家、C 类 6 家,利润合计 4700 万元,锦江生物 1200 万元第一;quarter_wide 四个季度三家合计 840/920/955/1125 万元,逐季走高——今天要把它"精装"一遍。

1.5 毛坯房:只画图形

上一讲大多数图的样子——不加主题、不加说明文字:

▶️ 查看代码
ggplot(airquality, aes(x = Temp)) + # 数据 + 映射:横轴每日最高气温
  geom_histogram(bins = 15)         # 图层:直方图,默认灰底

1.6 精装修:多写一行主题

数据、图形一个没动,只在末尾多写一行——放进年报里,哪张更让人愿意读下去?

▶️ 查看代码
ggplot(airquality, aes(x = Temp)) + # 数据 + 映射:完全没动
  geom_histogram(bins = 15) +       # 图层:还是同一张直方图
  theme_minimal()                   # 主题:一行代码换风格

二、主题与标签——让图表专业起来

一行代码,从"毛坯"到"精装"

2.1 内置主题:theme_xxx()

▶️ 查看代码
ggplot(faithful, aes(x = waiting, y = eruptions)) +  # 数据 + 映射
  geom_point(size = 2) +          # 图层:散点
  theme_classic()                 # 主题:换一种风格试试

提示

ggplot2 内置了 theme_minimal()、theme_classic()、theme_bw() 等多种主题,一行代码就能改变整体风格。把 theme_classic() 换成 theme_bw() 再跑一遍,看看区别。

2.2 【练习】给上节课的散点图换主题

照抄 2.1 的代码(模仿),用 faithful 画等待间隔与喷发时长的散点图,再依次把主题换成 theme_minimal()、theme_bw()、theme_classic()(练习)。

画完说一句:三种主题里,你更愿意把哪一种放进报告?为什么?

2.3 labs()——完善说明性文字

▶️ 查看代码
ggplot(airquality, aes(x = Temp)) + # 数据 + 映射
  geom_histogram(bins = 15,         # 图层:直方图
                 fill = "steelblue",  #       柱子填色
                 color = "white") +  #       柱子描边
  labs(title = "纽约每日最高气温分布",  # 标签:主标题
       subtitle = "1973 年 5—9 月",  #       副标题
       x = "气温(℉)",             #       横轴名(带单位)
       y = "天数",                  #       纵轴名(带单位)
       caption = "数据来源:airquality(R 内置数据集)")  # 脚注:来源

提示

title 点明主题,subtitle 交代范围,轴名必须带单位,caption 说明来源——专业图表这四处缺一不可。airquality$Temp 共 153 条、无缺失,范围 56—97℉,其中 73 天在 80℉ 以上。

2.4 【练习】给贯穿数据补全标签

用 quarter_wide 画四个季度的营收合计折线图(提示:先 summarise() 按 quarter 汇总,再 geom_line()),补上标题、带单位的轴名和 caption(练习)。

geom_line() 和 summarise() 都是第 16 讲学过的,这里只是换成刚生成的数据。

2.5 theme()——在主题上精细调整

▶️ 查看代码
ggplot(mtcars, aes(x = factor(cyl), y = mpg, fill = factor(cyl))) +  # 数据 + 映射
  geom_boxplot() +                # 图层:箱形图
  labs(title = "不同气缸数汽车的油耗分布",  # 标签:标题
       x = "气缸数", y = "每加仑英里数(mpg)") +  # 轴名
  theme_minimal() +               # 主题:先选一个基调
  theme(plot.title = element_text(size = 16, face = "bold"),  # 精调:标题
        legend.position = "bottom")  #       图例挪到底部

提示

theme() 的写法是 theme(元件 = element_xxx(...)):element_text() 管文字,legend.position 管图例。把 size = 16 改成 size = 10 跑一遍,看标题的变化。

2.6 【练习】把图例挪到底部

用 mtcars 画不同气缸数(cyl)油耗(mpg)的箱形图,按 2.5 的写法把标题加粗到 16 号、图例放到底部(模仿 + 练习)。

画完回答:4 缸、6 缸、8 缸的 mpg 中位数各是多少?哪一组更省油?

三、facet_wrap() 与 patchwork——两种"多图"思路

一份数据拆着看,还是几张图拼着看?

3.1 facet_wrap()——同一份数据拆成多图

▶️ 查看代码
ggplot(mtcars, aes(x = wt, y = mpg)) + # 数据 + 映射:车重 vs 油耗
  geom_point(size = 3) +        # 图层:散点
  facet_wrap(~ cyl)             # 分面:按气缸数拆成三张子图

提示

facet_wrap(~ 分类变量) 把一张图按该变量拆成多张结构相同的小图,每张只放自己那类数据——适合"同一份数据、按某个维度拆开看"。试试 facet_wrap(~ cyl, ncol = 1),控制排成几列。

3.2 【练习】按季度拆开看

用 quarter_wide 画各季度营收柱状图(geom_col()),并按 quarter 分面、排成两列(练习)——每个子图里比较三家公司。

画完回答:四个季度里,哪一家公司的柱子最高?是多少万元?

3.3 patchwork——把不同的图拼在一起

▶️ 查看代码
library(patchwork)              # 加载分包:多图拼接(本讲第一次用到)

p1 <- company_data %>%          # 先算数:各公司平均营收
  summarise(avg_rev = mean(revenue), .by = company) %>%  # 按公司求均值
  ggplot(aes(x = avg_rev,       # 映射:横向图,数值放 x
             y = reorder(company, avg_rev))) +  # 类别放 y、按均值排序
  geom_col(fill = "#2E5984") +  # 图层:柱状图(横向)
  theme_minimal()               # 主题:与右图统一

p2 <- ggplot(mtcars, aes(x = wt, y = mpg)) +  # 另一张:车重与油耗
  geom_point(size = 2) +        # 图层:散点
  geom_smooth(method = "lm", se = TRUE) +  # 图层:拟合线 + 置信带
  theme_minimal()               # 主题:与左图统一

p1 + p2                         # 拼接:+ 号左右并排

提示

patchwork 不是 ggplot2 自带的:先 install.packages("patchwork") 装一次,再像上面那样 library(patchwork) 加载一次——和 ggplot2、dplyr 一样,一个会话里加载一次就够,3.4 和第五节再拼图时不必重复写。

3.4 patchwork:/ 号上下叠放

▶️ 查看代码
p1 / p2                         # 斜杠:上下叠放

重要

patchwork 把多张独立的 ggplot 图拼成一张组合图:+(或 |)左右排列,/ 上下排列,括号还能嵌套——比如 (p1 | p2) / p1 就是"上面两张并排、下面一整张"。年报里"左边营收排名、右边成本结构"就是这么排出来的。

3.5 【练习】拼一张"两图对照"页

自己写两张图再拼起来:p1 用 faithful 画等待间隔与喷发时长的散点图;p2 用 airquality 画气温直方图(bins = 15)。先试 p1 + p2,再试 p1 / p2(练习)。

画完说一句:两种排版,哪种更适合放进一页横向的 PPT?

3.6 facet_wrap() vs patchwork——怎么选

facet_wrap():同一份数据、按某个分类变量拆成多张结构相同的小图(比如"每种气缸数一张散点图")

patchwork:把几张完全不同的图(不同数据、不同图型)拼在一起,各自独立设计

提示

一句话:facet 是"一份数据拆着看",patchwork 是"几张图拼着看"。R 还有 lattice 等更早期的绘图系统也能做多图,本课程不展开,有兴趣的同学课后了解。

四、坐标轴起止点与交互式图表

起点落在哪里,结论就是什么样

4.1 折线图:起点抬一抬,起伏才看得见

quarter_wide 里蜀汉制造四个季度是 250/260/250/220 万元,Q4 环比掉了 12%。同一张图,纵轴起点选 0 还是选 200,读出来的东西完全不同:

▶️ 查看代码
shu <- quarter_wide %>%         # 贯穿数据:季度表
  filter(company == "蜀汉制造") # 只留一家(filter 第 14 讲学过)

line <- ggplot(shu, aes(x = quarter, y = revenue)) +  # 数据 + 映射
  geom_line(group = 1, color = "#2E5984") +  # 图层:折线,分类轴要 group
  geom_point(color = "#2E5984") +  # 图层:标出四个季度的点
  labs(x = "季度", y = "营收(万元)")  # 轴名(带单位)

pl0 <- line + scale_y_continuous(limits = c(0, 280)) +  # 起点压到 0
  labs(title = "起点 = 0:几乎是平的")  # 标签:小标题写明起点
pl2 <- line + scale_y_continuous(limits = c(200, 280)) +  # 起点抬到 200
  labs(title = "起点 = 200:Q4 下滑一眼看见")  # 标签:小标题
pl0 | pl2                       # 拼接对比:| 号左右(第三节学过)

提示

scale_y_continuous(limits = c(下限, 上限)) 就是坐标轴的起止点(横轴换成 scale_x_continuous())。折线图用位置编码数值,起点抬到哪里都不改变"掉了 20 万元"这个事实,只是把观察窗口拉近拉远——起伏很小时,就得把起点抬到接近数据下沿才看得出来。

⚠️ 但 limits 会把轴外的数据整行删掉:散点图画 12 家公司的营收,写成 limits = c(1000, 2800),营收低于 1000 万元的 6 家公司当场从图上消失,R 只给一句警告。折线图、散点图用它没问题;柱状图用之前先想清楚——柱子的底边永远是 0,下限一旦写成大于 0 的数,柱子会整根消失,原因见 4.3。

4.2 【练习】给月均温折线选一个起点

用 airquality 先 summarise(avg = mean(Temp), .by = Month) 求各月平均气温,再画折线图,分别加上 scale_y_continuous(limits = c(0, 90)) 和 limits = c(60, 90)(练习),比较两张图。

画完回答:8 月 84.0℉、9 月 76.9℉,回落了 7.1℉——哪张图更能看出这次回落?抬高起点有没有改变数据本身?

4.3 柱状图:起点不从0开始会夸大差距

重要

4 缸 26.7、8 缸 15.1,真实差距是 1.77 倍;起点抬到 14 后,左柱的可见长度成了右柱的 11.5 倍——视觉差距被夸大 6 倍多。所以柱状图(含横向条形图)的数值轴应该从 0 起。

调轴有两个旋钮,分工不同:上面 4.1 的 scale_y_continuous(limits = ) 会把轴外的数据整行删掉,而柱子的底边永远是 0,下限只要比 0 大,ymin 就成了 NA,三根柱子一起消失、画面上什么都不剩;本页用的 coord_cartesian(ylim = ) 是放大镜,只裁画面、不删数据(横向图写 xlim =),所以能既保留柱子又演示"截断夸大差距"。想在放大局部的同时不失真,正解是改用折线图或点图。

4.4 ggplotly()——静态图一键变交互

▶️ 查看代码
library(plotly)                 # 加载分包:交互式图表(本讲第一次用到)

p <- ggplot(company_data, aes(x = revenue, y = cost, color = rating)) +  # 数据 + 映射
  geom_point(size = 3)          # 图层:散点(最后一行不加 +)

ggplotly(p)                     # 一键把 ggplot2 图转成交互式图表

提示

plotly 不是 ggplot2 自带的:需先 install.packages("plotly"),再 library(plotly)——一个会话加载一次即可。ggplotly() 把画好的 ggplot2 图一键变成可缩放、可悬停查数的交互式图表,代码几乎不用改:鼠标移到点上,营收、成本、评级当场显示。

4.5 【练习】把直方图变成交互式

把 airquality 的气温直方图(bins = 15)用 ggplotly() 转成交互式图表(练习)。

在图上悬停任意一根柱子,读出:该温度区间有多少天?再试试拖动框选放大某一段。

五、综合实战——三张图收官项目四

分面 · 拼接 · 交互式——每张都要"能直接放进年报"

5.1 动手实战(约 8 分钟)

用 1.3/1.4 当场生成的 company_data 与 quarter_wide,依次完成下面三张图——它们正好对应本讲三个知识点(图① 必做,课上有时间的同学继续②③);下一页给了一段挖好六个空的骨架代码,直接照着填。

图① 分面图(练 facet_wrap()):quarter_wide 各公司季度营收柱状图(geom_col()),按 quarter 分面、排成两列

图② 拼接图(练 patchwork):再画一张 company_data 年度营收排名图(reorder() + 横向 geom_col()),和图①拼成一页年报图(| 左右、/ 上下自选)

图③ 交互图(练 ggplotly()):library(plotly) 后,把图①或图②变成可悬停查数的交互式图表

三张图共同的硬要求:theme_minimal() 主题统一;labs() 补齐标题、带单位的轴名和 caption 来源;柱状图纵轴从 0 起——把 scale_y_continuous(limits = c(0, …)) 明写出来,下限只能是 0(写大了柱子会整根消失,见 4.3)

重要

验收四条:图型对(数个数还是画数值)、文字全(标题 + 带单位轴名 + 来源)、轴起点诚实(柱状图从 0 起)、读得出来(一图一句话)。

5.2 动笔骨架:六个空,按期末考试的样子填

下面这段就是考试卷上的形式——代码已经写好,关键位置挖掉六个空,行尾注释告诉你这里要干什么。把六个空填在自己的 R 里跑通,再往下看参考做法。

▶️ 查看代码
# 图① 分面:各公司季度营收,按季度拆成 2×2
p_facet <- ggplot(quarter_wide, aes(x = company, y = revenue)) +
  ______(fill = "steelblue") +          # 空① 图层:画现成的数值
  ______(~ quarter, ncol = 2) +       # 空② 分面:拆开后排两列
  scale_y_continuous(limits = c(0, 600)) +  #  轴:下限只能是 0
  labs(title = "三家公司季度营收",     #  标签:标题
       x = "公司", y = "营收(万元)",  #       带单位的轴名
       caption = "来源:quarter_wide") +  #    数据来源
  ______()                             # 空③ 主题:最简内置主题

# 图② 排名:横向条形图,与图①拼成一页年报
p_rank <- company_data %>%                # 贯穿数据:年度表
  summarise(avg = mean(revenue), .by = company) %>%
  ggplot(aes(x = avg, y = ______(company, avg))) +  # 空④ 按数值排序
  geom_col(fill = "steelblue") +        #  图层:横向柱
  scale_x_continuous(limits = c(0, 2200)) +  #  同上,0 起
  labs(title = "各公司年度营收排名",    #  标签
       x = "营收(万元)", y = "公司") +
  theme_bw()                     #  主题:与图①一致

p_facet ______ p_rank                  # 空⑤ 拼接:左右并排

# 图③ 交互:鼠标移到柱子上能查出公司名与营收
library(plotly)                        # 新包:加载一次即可
______(p_rank)                         # 空⑥ 静态图一键变交互

提示

六个空分别属于本讲的三块新知(②③⑤⑥)+第 16 讲的图层与排序(①④);填的时候注意 scale_y_continuous(limits = ) 已经替你写好了,别再改动它。

5.5 课堂展示

邀请 1—2 名同学展示自己的三张图,其余同学按"验收四条"点评美观度、专业度和准确性——特别留意柱状图的纵轴有没有从 0 起。

六、项目四总结收官、Exit Ticket 与预告项目五

从"了解绘图工具"到"产出专业图表"的完整能力链

6.1 项目四全景回顾

第 15 讲:可视化基础——图形三要素(位置 > 长度 > 角度面积 > 颜色)与 Base R 绘图

第 16 讲:ggplot2 语法入门与柱状图/折线图——data、aes、geom 核心概念,reorder() 排序与横向图,单条与多条折线

第 17 讲:常见图表类型(二)——散点图 + geom_smooth()、箱形图看分布对比、直方图看单变量分布

第 18 讲(今天):图表美化(主题与标签)、多图合并(facet 与 patchwork)、坐标轴起止点与交互式图表(plotly)初探

提示

四次课合起来,就是从"了解绘图工具"到"产出一张专业、准确、值得信赖的财务图表"的完整能力链。

6.2 学习成果自查

对照一下,今天这几件事你都做到了吗?

  • ✓ 能用 theme_minimal() 等内置主题改变图表风格,用 labs() 补齐标题、轴名与脚注
  • ✓ 能用 theme() 精细调整标题字号、图例位置
  • ✓ 能用 facet_wrap() 把一份数据按类别拆成多张小图
  • ✓ 能用 patchwork 把多张独立图表拼接成一张复合图(+ 左右、/ 上下),用前先 library(patchwork)
  • ✓ 能用 scale_x_continuous(limits = )/scale_y_continuous(limits = ) 调整坐标轴的起止点,并说清折线图可以抬高起点、柱状图必须从 0 起
  • ✓ 能用 ggplotly() 把静态图表转换为交互式图表,用前先 library(plotly)

6.3 Exit Ticket —— 写给自己的三个问题

请花 1 分钟,在纸条或在线问卷上简要回答:

  1. facet_wrap() 和 patchwork 分别适合什么场景?
  2. patchwork 里 + 和 / 分别是什么排版效果?
  3. 回顾项目四四次课,你觉得哪种图表类型在未来实习/工作中最可能用到?

6.4 预告项目五

重要

下周开始项目五"简单线性回归",将系统学习用 lm() 拟合两个变量之间的线性关系,并对回归结果进行解读——这正是第 17 讲"散点图 + geom_smooth(method = "lm")"那条趋势线背后的"数学答案"。

下讲预告

项目五:简单线性回归——开篇

  • 用 lm() 拟合两个变量之间的线性关系
  • 回归系数、截距的含义与解读
  • 回归结果的可视化与初步诊断

提示

课前准备:回顾第 17 讲"散点图 + geom_smooth(method = "lm")"的代码和结论——项目五会给出这条趋势线背后的数学答案。

谢谢!

第18讲:图表美化、多图合并与交互式图表初探

"Make it clear, make it honest, before you make it beautiful." 一张专业的财务图表,既要美观,更要准确、不误导——坐标轴的起点设在 0 还是设在别处、要不要放大局部,都体现着设计者的专业素养和责任心;图表美化不是为了炫技,而是为了让财务信息更好地服务于报表使用者,这是数据可视化工作者应有的职业操守。

朱 奇 | 锦城大学 · 财会学院