13  数值处理

13.1 引言

数值向量是数据科学的核心,本章系统学习它们在 R 中的用法。

本章需要用到以下R包:

library(tidyverse)
library(nycflights13) #航班数据

13.2 创建数值

大多数情况下,数值会以 R 的标准数值类型(如 integerdouble)形式存在。但有时会遇到字符串形式的数字,可能是从列名透视过来的数据,或者数据导入过程中出现了错误。

readr 包提供了两个函数可将字符串转换为数值:parse_double()parse_number()

  • 如果字符串是纯数字(可含有小数点或科学计数法),使用 parse_double()
x <- c("1.2", "5.6", "1e3")
parse_double(x)
#> [1]    1.2    5.6 1000.0
  • 如果字符串中包含需忽略的非数字字符(如货币符号或百分号),使用 parse_number()
x <- c("$1,234", "USD 3,513", "59%")
parse_number(x)
#> [1] 1234 3513   59

13.3 计数

数据分析仅靠简单计数和基础运算就能完成很多工作。因此 dplyr 提供了极为便捷的 count() 函数,能快速在数据分析中统计频次,结果将显示每个目的地(dest)对应的航班数量(n):

flights |> count(dest)
#> # A tibble: 105 × 2
#>   dest      n
#>   <chr> <int>
#> 1 ABQ     254
#> 2 ACK     265
#> ...

尽管第 4 章建议分行书写长命令,但 count() 一般用于交互式快速查看,因此常将其写在一行中。

想查看出现次数最多的前几项,可以加上 sort = TRUE

flights |> count(dest, sort = TRUE)
#>   dest      n
#> 1 ORD   17283
#> 2 ATL   17215
#> 3 LAX   16174
#> ...

如果想一次性查看全部值,可以使用 flights |> View() 打开交互窗口,或 flights |> print(n = Inf) 输出全部结果。

除了 count(),也可以手动组合 group_by()summarize()n() 来完成相同操作。这种方式更灵活,可以同时进行其他统计:

flights |> 
  group_by(dest) |> 
  summarize(
    n = n(),
    delay = mean(arr_delay, na.rm = TRUE)
  )

这里 n() 是一个特殊的汇总函数,不需要任何参数,它会统计当前分组的行数。它只能在 dplyr 的函数(如 mutate()filter()summarize())中使用,否则会报错:

n()
#> Error: Must only be used inside data-masking verbs like `mutate()`, `filter()`, and `group_by()`.

另外还有n_distinct(x)函数统计某变量中特定值的数量。例如,想知道哪些目的地有最多航空公司服务,如下可列出各个目的地以及服务它的不同航空公司数量。:

flights |> 
  group_by(dest) |> 
  summarize(carriers = n_distinct(carrier)) |> 
  arrange(desc(carriers))

有时还需要对某个变量进行“加权计数”,本质上就是求和。例如,可以统计每架飞机飞行的总里程:

flights |> 
  group_by(tailnum) |> 
  summarize(miles = sum(distance))

count() 函数也支持这种加权方式,通过 wt 参数实现:

flights |> count(tailnum, wt = distance)

若要统计缺失值的数量,可以结合 sum()is.na()。比如通过判断出发时间是否缺失,统计每个目的地取消的航班数量:

flights |> 
  group_by(dest) |> 
  summarize(n_cancelled = sum(is.na(dep_time)))

13.4 数值变换

对于基本运算,R中存在“扩展规则”,当两个向量进行基本运算时,若向量所含元素数目不相同, R 会将少元素向量自动扩展(recycle)与另一向量一样长,从而逐个元素进行计算。举例如下:

x <- c(1, 2, 10, 20)
x / 5
# 等价于
x / c(5, 5, 5, 5)
#> [1] 0.2 0.4 2.0 4.0

只有当较长向量的长度是较短向量的整数倍时才不会报错或发出警告:

x * c(1, 2)
#> [1]  1  4 10 40

x * c(1, 2, 3)
#> Warning: longer object length is not a multiple of shorter
#> [1]  1  4 30 20

此规则同样适用于逻辑比较(==、<、!= 等)。如果不小心用 == 代替 %in%,并且数据框的行数刚好是“错误的倍数”,就可能出现悄无声息的逻辑错误:

flights |> filter(month == c(1, 2))  # 本想选出1月和2月的航班

这段代码实际上会选出奇数行中 month == 1 的航班、偶数行中 month == 2 的航班,但不会报错!因为 flights 的行数正好是 2 的倍数,自发执行扩展规则。这种“沉默的失败”是数据分析中最难察觉的陷阱之一。

区分==%in%

  • == 是逐个元素一一比较,用于判断两个向量的对应位置元素是否相等:
c(1, 2, 3) == c(2, 3, 4)
#> [1] FALSE FALSE FALSE
  • %in% 是用来判断是否属于的:
c(1, 2, 3) %in% c(2, 3, 4)
#> [1] FALSE  TRUE  TRUE

除了基本运算,还可以用 pmin()pmax() 来逐行求最小值或最大值:

df <- tribble(
  ~x, ~y,
  1,  3,
  5,  2,
  7, NA
)

df |> mutate(
  min = pmin(x, y, na.rm = TRUE),
  max = pmax(x, y, na.rm = TRUE)
)

区别在于:

  • pmin()/pmax():按行比较返回每行的最小/最大值
  • min()/max():整体取最小/最大,会返回一个标量

另外,模运算(modular arithmetic)是处理“整除和余数”的工具。在 R 中:

  • %/% 表示整除,取商
  • %% 表示取余
1:10 %/% 3
#> [1] 0 0 1 1 1 2 2 2 3 3

1:10 %% 3
#> [1] 1 2 0 1 2 0 1 2 0 1

以及对数变换,广泛用于:缩放数量级差异较大的数据,或将指数增长转化为线性增长。

R 提供了三种对数函数:

  • log():自然对数(以 e 为底)
  • log2():以 2 为底
  • log10():以 10 为底

对应的反函数:

  • exp()log() 的反函数
  • 2^x10^x:分别为 log2()log10() 的反函数

要对运算结果进行四舍五入,可使用 round() 函数,默认取整:

round(123.456)  # [1] 123

可以用第二个参数 digits 控制小数位数:

round(123.456, 2)  # 保留两位小数 -> 123.46
round(123.456, -2) # 取整到百位 -> 100

注意,R 默认采用“四舍六入,五取偶”的修约策略,如下为例:

> round(1.35,1)
[1] 1.4
> round(1.45,1)
[1] 1.4

此外还有:

  • floor(x):向下取整
  • ceiling(x):向上取整

这些函数没有 digits 参数,因此要通过缩放再还原的方式控制小数精度:

# 向下保留两位小数
floor(x / 0.01) * 0.01
# 向上保留两位小数
ceiling(x / 0.01) * 0.01

可使用 cut() 可以将连续变量划分为多个区间(分箱):

x <- c(1, 2, 5, 10, 15, 20)
cut(x, breaks = c(0, 5, 10, 15, 20))
#> [1] (0,5]   (0,5]   (0,5]   (5,10]  (10,15] (15,20]

也可以给每一段设置分段标签:

cut(x, breaks = c(0, 5, 10, 15, 20), 
    labels = c("sm", "md", "lg", "xl"))

超出分段范围的值会返回 NA

x <- c(1, 2, 5, 10, 15, 30)
cut(x, breaks = c(0, 5, 10, 15, 20))
#> [1] (0,5]   (0,5]   (0,5]   (5,10]  (10,15] <NA>   

还可以通过参数控制区间是否包含端点,如:

  • right = FALSE 表示区间是 [a, b)
  • include.lowest = TRUE 表示包括最小值

此外,base R 提供了常见的累积函数:

  • cumsum():累加
  • cumprod():累乘
  • cummin():累积最小
  • cummax():累积最大

dplyr 还提供了 cummean() 用于计算累积平均值。

x <- 1:10
cumsum(x)
#> [1] 1 3 6 10 15 21 28 36 45 55
cummean(x)
#> [1] 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 5.5

13.5 通用变换

dplyr 提供了一系列参考 SQL 的排名函数,比如 min_rank()从小到大排名,且处理并列值的方法符合常理。

x <- c(1, 2, 2, 3, 4, NA)
min_rank(x)
#> [1]  1  2  2  4  5 NA

若想从大到小,可追加使用 desc(x)

min_rank(desc(x))
#> [1]  5  3  3  2  1 NA

还有以下变体函数:

  • row_number():不保留并列,按顺序排名相同值
  • dense_rank():将并列的若干值视为一体,下一个名次不跳号
  • percent_rank():按百分比标准化排名
  • cume_dist():累计分布,表示当前值小于等于多少比例

这些函数的行为类似于 base R 的 rank(),可通过设置 ties.methodna.last = "keep" 来实现同样的功能。

此外,在 dplyr 管道中使用 row_number() 不带参数时,表示“当前行号”。结合 %/%%% 可以实现按行号分组,例如:

df <- tibble(id = 1:10)
df |> mutate(
  row0 = row_number() - 1,
  three_groups = row0 %% 3,         # 三组标记(循环编号)
  three_in_each_group = row0 %/% 3  # 每组3个(等分分组)
)

lead()函数可以将当前向量向前移动一定单位,默认1位,加上参数 n 可实现多位偏移; lag()则向后移动。返回向量长度与输入一致,并在开头或结尾填充 NA

x <- c(2, 5, 11, 11, 19, 35)
lag(x)
#> [1] NA  2  5 11 11 19
lead(x)
#> [1]  5 11 11 19 35 NA

有时需要在满足某个条件时开始新的分组。例如,分析网站访问行为时,若两次访问间隔超过某一阈值(如5分钟),就认为是新的访问会话:

events <- tibble(time = c(0, 1, 2, 3, 5, 10, 12, 15, 17, 19, 20, 27, 28, 30))

events <- events |> mutate(
  diff = time - lag(time, default = first(time)),
  has_gap = diff >= 5
)

生成的 has_gap 是逻辑值,表示是否存在5分钟间隔。接下来使用 cumsum() 为每段会话生成连续编号:

events |> mutate(group = cumsum(has_gap))

另一个办法是使用 consecutive_id(),此函数给连续的相同元素编相同的号:

df <- tibble(
  x = c("a", "a", "a", "b", "c", "c", "d", "e", "a", "a", "b", "b"),
  y = c(1, 2, 3, 2, 4, 1, 3, 9, 4, 8, 10, 199)
)

df |> 
  group_by(id = consecutive_id(x)) |> 
  slice_head(n = 1)  # 保留每一组重复元素的第一个

13.5.0.1 13.6 数值函数

下面总结不同数值函数的用途与适用场景。

  • mean(): 平均数,对极端值敏感,适合对称分布。

  • median(): 中位数,不受极端值影响,适合偏态分布。

  • min() / max(): 最小/最大值;

  • quantile(x, p): 分位数,常用于排除极端值影响。p取值为[0,1]。

  • sd(x): 标准差,衡量总体波动。

  • IQR(x): 四分位距 = Q3 - Q1,衡量中间 50% 数据的跨度。

  • first(x) / last(x) / nth(x, n): 提取每组数据中的第一个、最后一个、第 n 个元素。支持以下参数:

    • na_rm = TRUE:跳过缺失值;
    • default = ...:位置不存在时提供默认值;
    • order_by = ...:更改排序方式;

以上函数不仅用于 summarize(),也常用于 mutate() 实现标准化变换:

变换形式 效果
x / sum(x) 转换为比例
(x - mean(x)) / sd(x) 转换为标准分数(Z-score)
(x - min(x)) / (max(x) - min(x)) 归一化到 [0, 1]
x / first(x) 转换为指数增长(以首值为基准)