用 R 与 ggplot2 可视化数量:Data Science for Beginners 鸟类数据集折线图、散点图与柱状图实战
本文以微软开源课程 Data Science for Beginners 中第 9 课《Visualizing Quantities》的 R 语言实现为骨架,带你使用 ggplot2 基于明尼苏达州鸟类数据集(data/birds.csv)完成从折线图、散点图到柱状图的完整可视化流程。读完本文,你将掌握图形语法(Grammar of Graphics)的核心思想、ggplot() 的声明式绘图套路、异常值识别与数据过滤、以及用 dplyr 分组聚合后绘制可读性更高的统计图表。
本课对应的原始英文讲义位于 3-Data-Visualization/R/09-visualization-quantities/README.md,仓库中同时提供 Python 版本(3-Data-Visualization/09-visualization-quantities/README.md),便于两种语言对照学习。
ggplot2 与图形语法:一次声明式的绘图体验
ggplot2 是 R 生态中最受欢迎的绘图包之一,既能生成简单图表,也能胜任复杂的高级图形。它的底层设计理念是"图形语法"(The Grammar of Graphics)——一种将图形拆解为语义组件的通用可视化方案,例如尺度(scales)与图层(layers)。换句话说,你只需要用少量代码就能完成单变量或多变量数据的可视化:告诉 ggplot2 如何把变量映射到美学属性(aesthetics)、使用哪种图形原语(geometry),剩下的事情由它包办。
✅ 绘图公式:Plot = Data + Aesthetics + Geometry
- Data:指的是数据集本身
- Aesthetics:指明要研究的变量(即 x 与 y 变量)
- Geometry:指明图表的几何类型(折线图、柱状图等)
一般性的绘图流程包括四步:先定位数据框中想要呈现的列;再对数据执行必要的转换;随后把变量赋给 x 轴与 y 轴;最后选定图表类型并渲染输出。ggplot() 的管道式写法(+ 号逐层叠加)让每一步都清晰可见。
选择哪种几何类型(图表),取决于你的数据和你想讲述的故事:
| 分析目的 | 推荐图表类型 |
|---|---|
| 分析趋势(trends) | 折线图、柱状图 |
| 比较数值(compare values) | 柱状图、饼图、散点图 |
| 展示部分与整体的关系(parts to a whole) | 饼图 |
| 展示数据分布(distribution) | 散点图、柱状图 |
| 展示数值间的关系(relationships) | 折线图、散点图、气泡图 |
准备数据:读取明尼苏达鸟类数据集
打开 R 控制台,导入数据集。注意:该数据集存放在本仓库根目录下的 data/birds.csv(共 442 条鸟类记录、13 个字段),文件带 UTF-8 BOM 头,因此读取时必须显式指定 fileEncoding="UTF-8-BOM",否则列名可能出现乱码:
birds <- read.csv("data/birds.csv", fileEncoding = "UTF-8-BOM")
head(birds)
head() 返回数据的前 5 行,内容是文本与数字的混合体,各列含义如下:
| 列名 | 含义 |
|---|---|
| Name / ScientificName | 常用名 / 学名 |
| Category | 类别(如 Ducks/Geese/Waterfowl) |
| Order / Family / Genus | 林奈分类学中的目 / 科 / 属 |
| ConservationStatus | 保护状态(IUCN 等级缩写:LC 无危、NT 近危、VU 易危、EN 濒危、CR 极危、EX 灭绝) |
| MinLength / MaxLength | 最小 / 最大体长(厘米) |
| MinBodyMass / MaxBodyMass | 最小 / 最大体重(克) |
| MinWingspan / MaxWingspan | 最小 / 最大翼展(厘米) |
前五行示例:
| Name | ScientificName | Category | Order | Family | Genus | ConservationStatus | MinLength | MaxLength | MinBodyMass | MaxBodyMass | MinWingspan | MaxWingspan | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Black-bellied whistling-duck | Dendrocygna autumnalis | Ducks/Geese/Waterfowl | Anseriformes | Anatidae | Dendrocygna | LC | 47 | 56 | 652 | 1020 | 76 | 94 |
| 1 | Fulvous whistling-duck | Dendrocygna bicolor | Ducks/Geese/Waterfowl | Anseriformes | Anatidae | Dendrocygna | LC | 45 | 53 | 712 | 1050 | 85 | 93 |
| 2 | Snow goose | Anser caerulescens | Ducks/Geese/Waterfowl | Anseriformes | Anatidae | Anser | LC | 64 | 79 | 2050 | 4050 | 135 | 165 |
| 3 | Ross's goose | Anser rossii | Ducks/Geese/Waterfowl | Anseriformes | Anatidae | Anser | LC | 57.3 | 64 | 1066 | 1567 | 113 | 116 |
| 4 | Greater white-fronted goose | Anser albifrons | Ducks/Geese/Waterfowl | Anseriformes | Anatidae | Anser | LC | 64 | 81 | 1930 | 3310 | 130 | 165 |
折线图:观察最大翼展并发现异常值
先绘制基础折线图,看看这些鸟类的最大翼展(MaxWingspan)是什么分布:
install.packages("ggplot2")
library("ggplot2")
ggplot(data = birds, aes(x = Name, y = MaxWingspan, group = 1)) +
geom_line()
这里先安装 ggplot2 包,再用 library("ggplot2") 将其载入工作区。绘图时调用 ggplot() 函数,以 data 指定数据集、在 aes() 中指定 x、y 变量;由于目标是折线图,几何层选用 geom_line()。注意 group = 1 的作用:当 x 轴是离散的分类变量(鸟名)时,它告诉 ggplot 把所有点连成一条折线,而不是按分组断开。
第一眼你能发现什么?图中至少存在一个异常值——那条"翼展"竟然超过 2000 厘米,也就是 20 多米!难道明尼苏达上空有翼龙在盘旋吗?显然需要调查一下。虽然你可以在 Excel 里快速排序找出这些异常值(它们多半是录入时的笔误),但本课更推荐"在绘图过程中解决问题",也就是继续用可视化手段定位它们。
给 x 轴加上标签,指明涉及的鸟类种类:
ggplot(data = birds, aes(x = Name, y = MaxWingspan, group = 1)) +
geom_line() +
theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
xlab("Birds") +
ylab("Wingspan (CM)") +
ggtitle("Max Wingspan in Centimeters")
这里通过 theme() 中的 axis.text.x = element_text(angle = 45, hjust = 1) 将 x 轴刻度文本旋转 45 度、并向右对齐(hjust = 1),避免过长的鸟名互相重叠;xlab() 与 ylab() 分别设置 x、y 轴标签,ggtitle() 为图表命名。
即使把标签旋转到 45 度,鸟名依然太多、难以阅读。换一种策略:只标注异常值,并把标注放进图内。此时改用散点图,为标注腾出更多空间:
ggplot(data = birds, aes(x = Name, y = MaxWingspan, group = 1)) +
geom_point() +
geom_text(aes(label = ifelse(MaxWingspan > 500, as.character(Name), '')), hjust = 0, vjust = 0) +
theme(axis.title.x = element_blank(),
axis.text.x = element_blank(),
axis.ticks.x = element_blank()) +
ylab("Wingspan (CM)") +
ggtitle("Max Wingspan in Centimeters")
这段代码做了什么?
geom_point()绘制散点;geom_text()配合ifelse(MaxWingspan > 500, as.character(Name), ''),只对翼展超过 500 厘米的鸟显示名称标签,其余留空;theme()中隐藏了 x 轴的标题、刻度文本与刻度线(element_blank()),让画面更清爽。
⚠️ 排错提示:原讲义中
theme(...)一行末尾漏写了管道连接符+,直接以换行接ylab(...)会导致语法错误。实际运行时请务必像上文那样,让每一层都以+结束(最后一行除外)。
过滤数据:剔除异常值
从散点图可以看出,秃鹰(Bald Eagle)和草原隼(Prairie Falcon)虽然确实是大型猛禽,但它们的最大翼展多半被录错了——多打了一个 0。你不太可能遇见翼展 25 米的秃鹰。让我们新建一个不包含这两个异常值的数据框:
birds_filtered <- subset(birds, MaxWingspan < 500)
ggplot(data = birds_filtered, aes(x = Name, y = MaxWingspan, group = 1)) +
geom_point() +
ylab("Wingspan (CM)") +
xlab("Birds") +
ggtitle("Max Wingspan in Centimeters") +
geom_text(aes(label = ifelse(MaxWingspan > 500, as.character(Name), '')), hjust = 0, vjust = 0) +
theme(axis.text.x = element_blank(), axis.ticks.x = element_blank())
subset(birds, MaxWingspan < 500) 生成新数据框 birds_filtered,随后在其上绘制散点图。过滤掉异常值后,数据变得更紧凑、更易理解。
💡 细节说明:经过
MaxWingspan < 500过滤后,geom_text()里的ifelse(MaxWingspan > 500, ...)条件已不可能为真,因此标注层实际上不会输出任何文本——它只是沿用了上一步的代码。若想保留标注,应把阈值降到 500 以下;这也提醒我们:清理数据后,要同步复核下游的过滤逻辑。
现在至少在翼展维度上我们拥有了一份更干净的数据集。折线图与散点图可以展示数值及其分布,但接下来我们更关心数据集中"数量"层面的内在信息。可以围绕下面这些量化问题创建可视化:
- 数据集里有多少种鸟类类别?各类别数量是多少?
- 有多少鸟处于灭绝、濒危、稀有或常见状态?
- 按林奈分类学统计,不同属(Genus)和目(Order)各有多少种鸟?
柱状图:探索类别分布
柱状图非常适合展示数据的分组聚合结果。先探索数据集中的鸟类类别,看看哪一类数量最多。
install.packages("dplyr")
install.packages("tidyverse")
library(lubridate)
library(scales)
library(dplyr)
library(ggplot2)
library(tidyverse)
birds_filtered %>%
group_by(Category) %>%
summarise(n = n(),
MinLength = mean(MinLength),
MaxLength = mean(MaxLength),
MinBodyMass = mean(MinBodyMass),
MaxBodyMass = mean(MaxBodyMass),
MinWingspan = mean(MinWingspan),
MaxWingspan = mean(MaxWingspan)) %>%
gather("key", "value", -c(Category, n)) %>%
ggplot(aes(x = Category, y = value, group = key, fill = key)) +
geom_bar(stat = "identity") +
scale_fill_manual(values = c("#D62728", "#FF7F0E", "#8C564B", "#2CA02C", "#1F77B4", "#9467BD")) +
xlab("Category") + ggtitle("Birds of Minnesota")
这段代码用到了 dplyr 与 lubridate 两个辅助包(tidyverse 一并载入以便使用 %>% 管道和 gather()):首先按鸟类 Category 分组,再用 summarise() 对 MinLength、MaxLength、MinBodyMass、MaxBodyMass、MinWingspan、MaxWingspan 六列求均值,并统计每组数量 n = n();随后 gather() 把六列指标"长表化"为 key/value 两列,最后交给 ggplot() 绘制分组柱状图。scale_fill_manual() 为不同指标指定了六种颜色(#D62728、#FF7F0E、#8C564B、#2CA02C、#1F77B4、#9467BD),stat = "identity" 表示直接使用 y 值本身作为柱高,而非计数。
不过,这张柱状图几乎不可读——未经充分聚合的指标太多、柱子过密。应该只选择真正想展示的指标。既然类别数量众多,可以改用纵向(横向条形)展示,并调整高度以容纳全部数据:
birds_count <- dplyr::count(birds_filtered, Category, sort = TRUE)
birds_count$Category <- factor(birds_count$Category, levels = birds_count$Category)
ggplot(birds_count, aes(Category, n)) +
geom_bar(stat = "identity") +
coord_flip()
思路如下:
dplyr::count(birds_filtered, Category, sort = TRUE)统计Category列每个类别的出现次数,并按数量降序排序,存入新数据框birds_count;factor(birds_count$Category, levels = birds_count$Category)把类别列转成**因子(factor)**并锁定其顺序,确保柱状图按排序后的顺序绘制;geom_bar(stat = "identity")用计数n作柱高;coord_flip()把坐标系翻转 90 度,柱子横向排列,长类别名就能完整显示。
这张图清楚地展示了每个类别的鸟类数量:一眼就能看出,数量最多的是 Ducks/Geese/Waterfowl(鸭/鹅/水禽) 类别。明尼苏达是"万湖之州"(land of 10,000 lakes),这个结果并不意外。对 data/birds.csv 的实际统计也印证了这一点:Ducks/Geese/Waterfowl 共 45 条记录,位列第一;其次是 New World warblers(41 条)与 Sandpipers/Allies(34 条)。如果按保护状态统计,LC(无危)占绝大多数(402 条),NT(近危)27 条、VU(易危)10 条、EN(濒危)2 条、CR(极危)1 条、EX(灭绝)1 条。
✅ 不妨再对这份数据集尝试其他计数维度,看看是否有让你惊讶的发现。
比较数据:按类别聚合与叠加柱状图
通过创建新的分组轴,可以对聚合数据做各种比较。比如按鸟类类别比较其最大体长(MaxLength):
birds_grouped <- birds_filtered %>%
group_by(Category) %>%
summarise(
MaxLength = max(MaxLength, na.rm = T),
MinLength = max(MinLength, na.rm = T)
) %>%
arrange(Category)
ggplot(birds_grouped, aes(Category, MaxLength)) +
geom_bar(stat = "identity") +
coord_flip()
先用 group_by(Category) 对 birds_filtered 分组,summarise() 用 max() 求出每类的最大体长(na.rm = T 忽略缺失值,注意讲义此处 MinLength 也取了 max,属笔误,可依需求改为 min()),arrange(Category) 按类别排序,最后绘制横向柱状图。
结果毫无悬念:蜂鸟(hummingbirds)的最大体长比鹈鹕(Pelicans)或鹅(Geese)小得多。当数据符合常识时,说明分析是靠谱的!
还可以通过**叠加(superimpose)**数据,做出更有信息量的柱状图。下面把每个鸟类类别的最小体长与最大体长叠加在同一张图上:
ggplot(data = birds_grouped, aes(x = Category)) +
geom_bar(aes(y = MaxLength), stat = "identity", position = "identity", fill = 'blue') +
geom_bar(aes(y = MinLength), stat = "identity", position = "identity", fill = 'orange') +
coord_flip()
两个 geom_bar() 分别以 MaxLength 和 MinLength 为柱高,position = "identity" 让两组柱子从同一基线(0)出发、互不位移地重叠绘制,蓝色表示最大体长、橙色表示最小体长,最后 coord_flip() 转为横向布局。
叠加之后,每个类别的体长区间一目了然:蓝橙两条柱的起点相同、长度不同,直观呈现了每个类别内部的体长跨度。
实战挑战:把方法迁移到新数据集
这份鸟类数据集蕴含了特定生态系统内不同鸟类的丰富信息。不妨上网寻找其他面向鸟类的数据集(例如不同州、不同国家的鸟类观测数据),用同样的三件套——折线图、散点图、柱状图——围绕这些数据反复练习,去发现一些你以前不知道的事实。
课后巩固与作业
本课是使用 ggplot2 可视化"数量"的第一课。除了 ggplot2,R 生态中还有 Lattice 与 Plotly 等可视化包,可以继续研究如何用它们处理同一类数据集。
配套作业《Lines, Scatters, and Bars》位于 translations/ar/3-Data-Visualization/R/09-visualization-quantities/assignment.md(英文版见 3-Data-Visualization/R/09-visualization-quantities/assignment.md):在本次课学习过的折线图、散点图和柱状图基础上,深入挖掘数据集,围绕某一种鸟类(例如雪雁 Snow Goose)编写一个脚本,用三种图表讲一个完整的数据故事。评分标准包括:脚本是否带有良好的注释、叙事是否完整、图表是否美观。
仓库中的佐证资料一览
- 数据集:data/birds.csv(含 BOM 头,442 条记录、13 列,故读取需
fileEncoding="UTF-8-BOM"); - 本课英文原始讲义:3-Data-Visualization/R/09-visualization-quantities/README.md;
- 本课作业:translations/ar/3-Data-Visualization/R/09-visualization-quantities/assignment.md;
- Python 对照版本:3-Data-Visualization/09-visualization-quantities/README.md;
- 课程框架总览:1-Introduction/README.md。
以上所有 R 代码均在 R 控制台可直接运行;绘图前的核心数据转换(分组、聚合、过滤)由 dplyr 完成,绘图本身完全由 ggplot2 的图层叠加机制驱动,这正是图形语法"声明式绘图"的精髓——你只管描述数据是什么、映射到哪里、用什么几何呈现,剩下的交给 ggplot2。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00





