首页
/ 用 R 与 ggplot2 可视化数量:Data Science for Beginners 鸟类数据集折线图、散点图与柱状图实战

用 R 与 ggplot2 可视化数量:Data Science for Beginners 鸟类数据集折线图、散点图与柱状图实战

2026-09-09 21:08:22作者:姚月梅Lane

本文以微软开源课程 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")

这段代码用到了 dplyrlubridate 两个辅助包(tidyverse 一并载入以便使用 %>% 管道和 gather()):首先按鸟类 Category 分组,再用 summarise()MinLengthMaxLengthMinBodyMassMaxBodyMassMinWingspanMaxWingspan 六列求均值,并统计每组数量 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()

思路如下:

  1. dplyr::count(birds_filtered, Category, sort = TRUE) 统计 Category 列每个类别的出现次数,并按数量降序排序,存入新数据框 birds_count
  2. factor(birds_count$Category, levels = birds_count$Category) 把类别列转成**因子(factor)**并锁定其顺序,确保柱状图按排序后的顺序绘制;
  3. geom_bar(stat = "identity") 用计数 n 作柱高;
  4. 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() 分别以 MaxLengthMinLength 为柱高,position = "identity" 让两组柱子从同一基线(0)出发、互不位移地重叠绘制,蓝色表示最大体长、橙色表示最小体长,最后 coord_flip() 转为横向布局。

叠加展示各类别最小与最大体长的柱状图

叠加之后,每个类别的体长区间一目了然:蓝橙两条柱的起点相同、长度不同,直观呈现了每个类别内部的体长跨度。

实战挑战:把方法迁移到新数据集

这份鸟类数据集蕴含了特定生态系统内不同鸟类的丰富信息。不妨上网寻找其他面向鸟类的数据集(例如不同州、不同国家的鸟类观测数据),用同样的三件套——折线图、散点图、柱状图——围绕这些数据反复练习,去发现一些你以前不知道的事实。

课后巩固与作业

本课是使用 ggplot2 可视化"数量"的第一课。除了 ggplot2,R 生态中还有 LatticePlotly 等可视化包,可以继续研究如何用它们处理同一类数据集。

配套作业《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)编写一个脚本,用三种图表讲一个完整的数据故事。评分标准包括:脚本是否带有良好的注释、叙事是否完整、图表是否美观。

仓库中的佐证资料一览

以上所有 R 代码均在 R 控制台可直接运行;绘图前的核心数据转换(分组、聚合、过滤)由 dplyr 完成,绘图本身完全由 ggplot2 的图层叠加机制驱动,这正是图形语法"声明式绘图"的精髓——你只管描述数据是什么、映射到哪里、用什么几何呈现,剩下的交给 ggplot2

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
docsdocs
暂无描述
Markdown
899
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
525
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395