在R中使用data.table

The Complete Beginners Guide

data.table
R
Author

Lee

Published

June 20, 2024

图 1: data.tabel包的最简洁语法

图 1 所示,在R中使用data.table的语法非常简洁。

1 data.table的通用语法

1.1 创建data.table

library(tidyverse)
library(data.table)
# 建立data.table
dt <- data.table(
  x = 1:3,
  y = c("v1", "v2", "v3")
)
dt
       x      y
   <int> <char>
1:     1     v1
2:     2     v2
3:     3     v3
# 加载其他数据集
data(mtcars)
mtcars$carname <- rownames(mtcars)
mtcars_dt <- as.data.table(mtcars)[1:10][, c("carb", "disp") := NULL]
mtcars_dt

使用as.data.table()函数可以将普通数据转换为data.table。如果只想按引用转化,则使用setDT()函数。

1.2 特殊符号

  1. .():代替.list()
  2. := 赋值符号,按引用的方式增加和修改列。
  3. .N: 行数。
  4. .I: 整数向量seq_len(nrow(x))
  5. .SD: 每个分组数据(除by或keyby的列)。
  6. .SDcols: 与.SD配合使用,用来选择.SD中的列。
  7. .BY:包含所有by分组变量的list。
  8. .GRP:分组索引,包含当前分组的整数向量,1代表第1分组,2代表第2分组,以此类推。
  9. .NGRP:分组数。
  10. .EACHI:用于by/keyby = .EACHI,表示根据i表达式的每一行进行分组。

以上特殊符号在接下来的会有针对性的应用。

1.3 链式操作

data.table支持链式操作,即多个操作可以串联起来,形成一个表达式。具体有点类似于tidyverse中的管道。基本形式如下:

DT[…][…][…] 或者可以写为 DT[ …][ …][ …]`

1.4 引用语法

data.table采用了高效的引用语法,即浅复制1

浅复制只是复制列指针对象(即对应数据框的列),而实际的数据在内存中不做物理复制。

data.table使用:=运算符,做整列或部分列替换时不做任何复制,因为:=运算符是通过引用就地更新data.table的列,而不会创建新的对象。

如果希望复制数据而不按引用处理,则可以使用copy()函数。

1.5 键与索引

data.table支持设置索引,使得选择行和做数据连接时更加方便快捷。

  • 键:一级有序索引。
  • 索引:二级自动索引。

1.5.1

data.table 上设置一个或多个键可以使其执行二进制搜索(binary search),这比线性搜索要快很多数量级,尤其是对于大型数据。setkey() 函数可以设置键。

setkey(mtcars_dt, carname) # 设置键
key(mtcars_dt) # 查看键
[1] "carname"
mtcars_dt
Key: <carname>
      mpg   cyl    hp  drat    wt  qsec    vs    am  gear           carname
    <num> <num> <num> <num> <num> <num> <num> <num> <num>            <char>
 1:  22.8     4    93  3.85 2.320 18.61     1     1     4        Datsun 710
 2:  14.3     8   245  3.21 3.570 15.84     0     0     3        Duster 360
 3:  21.4     6   110  3.08 3.215 19.44     1     0     3    Hornet 4 Drive
 4:  18.7     8   175  3.15 3.440 17.02     0     0     3 Hornet Sportabout
 5:  21.0     6   110  3.90 2.620 16.46     0     1     4         Mazda RX4
 6:  21.0     6   110  3.90 2.875 17.02     0     1     4     Mazda RX4 Wag
 7:  22.8     4    95  3.92 3.150 22.90     1     0     4          Merc 230
 8:  24.4     4    62  3.69 3.190 20.00     1     0     4         Merc 240D
 9:  19.2     6   123  3.92 3.440 18.30     1     0     4          Merc 280
10:  18.1     6   105  2.76 3.460 20.22     1     0     3           Valiant
  1. 设置键后,data.table会自动对数据集进行排序,使得键值有序。
  2. 设置的键必须是数据集中的一列。
  3. 键非常有用,在后续的数据合并等操作中会有奇效((sec?)–data-man)。
  4. 索引可以提高查询速度,但并不是所有情况下都适用。我们多数情况是还是用键。

keyby用法,这个函数可以进行分组统计。

output <- mtcars_dt[, .(
  mean_mpg = mean(mpg),
  mean_wt = mean(wt),
  mean_qsec = mean(qsec)
), keyby = cyl] # 按cyl分组的同时,设定cyl为键
key(output)
[1] "cyl"

2 数据读写

data.table使用fread()函数读取数据,fwrite()函数写入数据。注意,fread()不能读取excel文件

2.1 读写数据

# 读入数据
flights <- fread("d:/Myblog/posts/datatable-beginners-guide-2024-06-20/tech/flights14.csv")

# 写入数据
fwrite(flights, "d:/Myblog/posts/datatable-beginners-guide-2024-06-20/tech/flights14_new.csv")

flights
         year month   day dep_delay arr_delay carrier origin   dest air_time
        <int> <int> <int>     <int>     <int>  <char> <char> <char>    <int>
     1:  2014     1     1        14        13      AA    JFK    LAX      359
     2:  2014     1     1        -3        13      AA    JFK    LAX      363
     3:  2014     1     1         2         9      AA    JFK    LAX      351
     4:  2014     1     1        -8       -26      AA    LGA    PBI      157
     5:  2014     1     1         2         1      AA    JFK    LAX      350
    ---                                                                     
253312:  2014    10    31         1       -30      UA    LGA    IAH      201
253313:  2014    10    31        -5       -14      UA    EWR    IAH      189
253314:  2014    10    31        -8        16      MQ    LGA    RDU       83
253315:  2014    10    31        -4        15      MQ    LGA    DTW       75
253316:  2014    10    31        -5         1      MQ    LGA    SDF      110
        distance  hour
           <int> <int>
     1:     2475     9
     2:     2475    11
     3:     2475    19
     4:     1035     7
     5:     2475    13
    ---               
253312:     1416    14
253313:     1400     8
253314:      431    11
253315:      502    11
253316:      659     8

3 数据操作

3.1 操作行

data.table对象知道自己的列名(与data.frame的不同),所以在操作行时,只传递列名即可。

3.1.1 根据索引选择行

mtcars_dt[3:5] # 选择第3到第5行
Key: <carname>
     mpg   cyl    hp  drat    wt  qsec    vs    am  gear           carname
   <num> <num> <num> <num> <num> <num> <num> <num> <num>            <char>
1:  21.4     6   110  3.08 3.215 19.44     1     0     3    Hornet 4 Drive
2:  18.7     8   175  3.15 3.440 17.02     0     0     3 Hornet Sportabout
3:  21.0     6   110  3.90 2.620 16.46     0     1     4         Mazda RX4
mtcars_dt[!3:7] # 选择除3:7行之外的行
Key: <carname>
     mpg   cyl    hp  drat    wt  qsec    vs    am  gear    carname
   <num> <num> <num> <num> <num> <num> <num> <num> <num>     <char>
1:  22.8     4    93  3.85  2.32 18.61     1     1     4 Datsun 710
2:  14.3     8   245  3.21  3.57 15.84     0     0     3 Duster 360
3:  24.4     4    62  3.69  3.19 20.00     1     0     4  Merc 240D
4:  19.2     6   123  3.92  3.44 18.30     1     0     4   Merc 280
5:  18.1     6   105  2.76  3.46 20.22     1     0     3    Valiant

3.1.2 根据逻辑表达式

mtcars_dt[mpg > 20] # 选择mpg大于20的行
Key: <carname>
     mpg   cyl    hp  drat    wt  qsec    vs    am  gear        carname
   <num> <num> <num> <num> <num> <num> <num> <num> <num>         <char>
1:  22.8     4    93  3.85 2.320 18.61     1     1     4     Datsun 710
2:  21.4     6   110  3.08 3.215 19.44     1     0     3 Hornet 4 Drive
3:  21.0     6   110  3.90 2.620 16.46     0     1     4      Mazda RX4
4:  21.0     6   110  3.90 2.875 17.02     0     1     4  Mazda RX4 Wag
5:  22.8     4    95  3.92 3.150 22.90     1     0     4       Merc 230
6:  24.4     4    62  3.69 3.190 20.00     1     0     4      Merc 240D
mtcars_dt[carname %chin% c("AMC Javelin")]
Key: <carname>
Empty data.table (0 rows and 10 cols): mpg,cyl,hp,drat,wt,qsec...

3.1.3 删除行

# 删除重复行
unique(mtcars_dt)
Key: <carname>
      mpg   cyl    hp  drat    wt  qsec    vs    am  gear           carname
    <num> <num> <num> <num> <num> <num> <num> <num> <num>            <char>
 1:  22.8     4    93  3.85 2.320 18.61     1     1     4        Datsun 710
 2:  14.3     8   245  3.21 3.570 15.84     0     0     3        Duster 360
 3:  21.4     6   110  3.08 3.215 19.44     1     0     3    Hornet 4 Drive
 4:  18.7     8   175  3.15 3.440 17.02     0     0     3 Hornet Sportabout
 5:  21.0     6   110  3.90 2.620 16.46     0     1     4         Mazda RX4
 6:  21.0     6   110  3.90 2.875 17.02     0     1     4     Mazda RX4 Wag
 7:  22.8     4    95  3.92 3.150 22.90     1     0     4          Merc 230
 8:  24.4     4    62  3.69 3.190 20.00     1     0     4         Merc 240D
 9:  19.2     6   123  3.92 3.440 18.30     1     0     4          Merc 280
10:  18.1     6   105  2.76 3.460 20.22     1     0     3           Valiant
# 删除包含NA的行
na.omit(mtcars_dt)
Key: <carname>
      mpg   cyl    hp  drat    wt  qsec    vs    am  gear           carname
    <num> <num> <num> <num> <num> <num> <num> <num> <num>            <char>
 1:  22.8     4    93  3.85 2.320 18.61     1     1     4        Datsun 710
 2:  14.3     8   245  3.21 3.570 15.84     0     0     3        Duster 360
 3:  21.4     6   110  3.08 3.215 19.44     1     0     3    Hornet 4 Drive
 4:  18.7     8   175  3.15 3.440 17.02     0     0     3 Hornet Sportabout
 5:  21.0     6   110  3.90 2.620 16.46     0     1     4         Mazda RX4
 6:  21.0     6   110  3.90 2.875 17.02     0     1     4     Mazda RX4 Wag
 7:  22.8     4    95  3.92 3.150 22.90     1     0     4          Merc 230
 8:  24.4     4    62  3.69 3.190 20.00     1     0     4         Merc 240D
 9:  19.2     6   123  3.92 3.440 18.30     1     0     4          Merc 280
10:  18.1     6   105  2.76 3.460 20.22     1     0     3           Valiant

3.1.4 行切片

mtcars_dt[sample(.N, 3)] # 随机选择3行
     mpg   cyl    hp  drat    wt  qsec    vs    am  gear        carname
   <num> <num> <num> <num> <num> <num> <num> <num> <num>         <char>
1:  24.4     4    62  3.69 3.190 20.00     1     0     4      Merc 240D
2:  19.2     6   123  3.92 3.440 18.30     1     0     4       Merc 280
3:  21.4     6   110  3.08 3.215 19.44     1     0     3 Hornet 4 Drive
mtcars_dt[sample(.N, .N * 0.5)] # 随机选择50%行
     mpg   cyl    hp  drat    wt  qsec    vs    am  gear        carname
   <num> <num> <num> <num> <num> <num> <num> <num> <num>         <char>
1:  24.4     4    62  3.69 3.190 20.00     1     0     4      Merc 240D
2:  22.8     4    95  3.92 3.150 22.90     1     0     4       Merc 230
3:  14.3     8   245  3.21 3.570 15.84     0     0     3     Duster 360
4:  19.2     6   123  3.92 3.440 18.30     1     0     4       Merc 280
5:  21.4     6   110  3.08 3.215 19.44     1     0     3 Hornet 4 Drive
mtcars_dt[frankv(-mpg, ties.method = "dense") < 2] # 选择mpg最大的行
Key: <carname>
     mpg   cyl    hp  drat    wt  qsec    vs    am  gear   carname
   <num> <num> <num> <num> <num> <num> <num> <num> <num>    <char>
1:  24.4     4    62  3.69  3.19    20     1     0     4 Merc 240D

3.1.5 行排序

mtcars_dt[order(mpg)] # 按mpg排序
      mpg   cyl    hp  drat    wt  qsec    vs    am  gear           carname
    <num> <num> <num> <num> <num> <num> <num> <num> <num>            <char>
 1:  14.3     8   245  3.21 3.570 15.84     0     0     3        Duster 360
 2:  18.1     6   105  2.76 3.460 20.22     1     0     3           Valiant
 3:  18.7     8   175  3.15 3.440 17.02     0     0     3 Hornet Sportabout
 4:  19.2     6   123  3.92 3.440 18.30     1     0     4          Merc 280
 5:  21.0     6   110  3.90 2.620 16.46     0     1     4         Mazda RX4
 6:  21.0     6   110  3.90 2.875 17.02     0     1     4     Mazda RX4 Wag
 7:  21.4     6   110  3.08 3.215 19.44     1     0     3    Hornet 4 Drive
 8:  22.8     4    93  3.85 2.320 18.61     1     1     4        Datsun 710
 9:  22.8     4    95  3.92 3.150 22.90     1     0     4          Merc 230
10:  24.4     4    62  3.69 3.190 20.00     1     0     4         Merc 240D
mtcars_dt[order(-mpg)] # 按mpg逆序排序
      mpg   cyl    hp  drat    wt  qsec    vs    am  gear           carname
    <num> <num> <num> <num> <num> <num> <num> <num> <num>            <char>
 1:  24.4     4    62  3.69 3.190 20.00     1     0     4         Merc 240D
 2:  22.8     4    93  3.85 2.320 18.61     1     1     4        Datsun 710
 3:  22.8     4    95  3.92 3.150 22.90     1     0     4          Merc 230
 4:  21.4     6   110  3.08 3.215 19.44     1     0     3    Hornet 4 Drive
 5:  21.0     6   110  3.90 2.620 16.46     0     1     4         Mazda RX4
 6:  21.0     6   110  3.90 2.875 17.02     0     1     4     Mazda RX4 Wag
 7:  19.2     6   123  3.92 3.440 18.30     1     0     4          Merc 280
 8:  18.7     8   175  3.15 3.440 17.02     0     0     3 Hornet Sportabout
 9:  18.1     6   105  2.76 3.460 20.22     1     0     3           Valiant
10:  14.3     8   245  3.21 3.570 15.84     0     0     3        Duster 360
setorder(mtcars_dt, -mpg, cyl) # 按mpg逆序排序,再按cyl排序(改变原数据)

3.1.6 其他操作

mtcars_dt[carname %like% "^M"] # carname以M开头的行
     mpg   cyl    hp  drat    wt  qsec    vs    am  gear       carname
   <num> <num> <num> <num> <num> <num> <num> <num> <num>        <char>
1:  24.4     4    62  3.69 3.190 20.00     1     0     4     Merc 240D
2:  22.8     4    95  3.92 3.150 22.90     1     0     4      Merc 230
3:  21.0     6   110  3.90 2.620 16.46     0     1     4     Mazda RX4
4:  21.0     6   110  3.90 2.875 17.02     0     1     4 Mazda RX4 Wag
5:  19.2     6   123  3.92 3.440 18.30     1     0     4      Merc 280
mtcars_dt[mpg %between% c(10, 15)] # mpg在10到15之间的行-闭区间
     mpg   cyl    hp  drat    wt  qsec    vs    am  gear    carname
   <num> <num> <num> <num> <num> <num> <num> <num> <num>     <char>
1:  14.3     8   245  3.21  3.57 15.84     0     0     3 Duster 360
mtcars_dt[between(mpg, 10, 15, incbounds = FALSE)] # 开区间
     mpg   cyl    hp  drat    wt  qsec    vs    am  gear    carname
   <num> <num> <num> <num> <num> <num> <num> <num> <num>     <char>
1:  14.3     8   245  3.21  3.57 15.84     0     0     3 Duster 360

3.2 操作列

有几种方法可以选择

3.2.1 根据列名选择列

3.2.1.1 直接输出列名进行选择

# 返回向量
ans <- flights[, arr_delay]
head(ans)
[1]  13  13   9 -26   1   0
# 返回data.table
ans <- flights[, list(arr_delay)] # 选择单列
head(ans)
   arr_delay
       <int>
1:        13
2:        13
3:         9
4:       -26
5:         1
6:         0
ans <- flights[, .(arr_delay, dep_delay)] # 选择多列
head(ans)
   arr_delay dep_delay
       <int>     <int>
1:        13        14
2:        13        -3
3:         9         2
4:       -26        -8
5:         1         2
6:         0         4
ans <- flights[, !c("arr_delay", "dep_delay")] # 反选列
head(ans)
    year month   day carrier origin   dest air_time distance  hour
   <int> <int> <int>  <char> <char> <char>    <int>    <int> <int>
1:  2014     1     1      AA    JFK    LAX      359     2475     9
2:  2014     1     1      AA    JFK    LAX      363     2475    11
3:  2014     1     1      AA    JFK    LAX      351     2475    19
4:  2014     1     1      AA    LGA    PBI      157     1035     7
5:  2014     1     1      AA    JFK    LAX      350     2475    13
6:  2014     1     1      AA    EWR    LAX      339     2454    18
  1. data.table中将列名变量封装在list()中,从而确保返回的结果为data.table.如果需要返回向量,则直接传入列名即可。
  2. list()可以用.()简写代替。
  3. 只要j表达式是一个list,这个列表中的每个元素均会转换为结果data.table的一列。这使得j表达式变得非常重要,也是进行更复杂操作的基础。

3.2.1.2 将所需列储存在字符向量中-..前缀或with=FALSE参数

# 前缀..
select_cols <- c("arr_delay", "dep_delay")
flights[, ..select_cols]
        arr_delay dep_delay
            <int>     <int>
     1:        13        14
     2:        13        -3
     3:         9         2
     4:       -26        -8
     5:         1         2
    ---                    
253312:       -30         1
253313:       -14        -5
253314:        16        -8
253315:        15        -4
253316:         1        -5
# with=FALSE参数
flights[, select_cols, with = FALSE]
        arr_delay dep_delay
            <int>     <int>
     1:        13        14
     2:        13        -3
     3:         9         2
     4:       -26        -8
     5:         1         2
    ---                    
253312:       -30         1
253313:       -14        -5
253314:        16        -8
253315:        15        -4
253316:         1        -5
  • with参数与函数with()函数类似。
  • data.table中的列可以想变量一样被引用,所以默认的with参数设定为TURE。

3.2.2 修改列排序

setcolorder(mtcars_dt, "cyl") # 将cyl放到第一列
setcolorder(mtcars_dt, c("carname", "gear")) # 可以选择多列

3.2.3 修改列名

# 方法1
setnames(
  mtcars_dt,
  "cyl", # 旧名
  "CYL" # 新名
)
head(mtcars_dt)
          carname  gear   CYL   mpg    hp  drat    wt  qsec    vs    am
           <char> <num> <num> <num> <num> <num> <num> <num> <num> <num>
1:      Merc 240D     4     4  24.4    62  3.69 3.190 20.00     1     0
2:     Datsun 710     4     4  22.8    93  3.85 2.320 18.61     1     1
3:       Merc 230     4     4  22.8    95  3.92 3.150 22.90     1     0
4: Hornet 4 Drive     3     6  21.4   110  3.08 3.215 19.44     1     0
5:      Mazda RX4     4     6  21.0   110  3.90 2.620 16.46     0     1
6:  Mazda RX4 Wag     4     6  21.0   110  3.90 2.875 17.02     0     1
# 方法2
ans <- flights[, .(delay_arr = arr_delay, delay_dep = dep_delay)] # 新列名 = 旧列名
head(ans)
   delay_arr delay_dep
       <int>     <int>
1:        13        14
2:        13        -3
3:         9         2
4:       -26        -8
5:         1         2
6:         0         4

3.2.4 在j中进行计算

ans <- flights[, sum((arr_delay + dep_delay) < 0)]
head(ans)
[1] 141814

j不仅可以处理变量,还可以处理表达式。这样我们就可以通过调用函数来对变量进行计算。

3.2.5 在i子集中操作j

  1. 计算6月以”JFK”为始发机场的所有航班的平均到达和起飞的延误时间。

  2. 2014年6月,从”JFK”机场出发的飞机共有多少?

# q1
ans <- flights[
  origin == "JFK" & month == 6L,
  .(
    m_arr = mean(arr_delay),
    m_dep = mean(dep_delay)
  )
]
head(ans)
      m_arr    m_dep
      <num>    <num>
1: 5.839349 9.807884
# q2
ans <- flights[
  origin == "JFK" & month == 6L,
  length(dest)
]
head(ans)
[1] 8422

类似以上的,从i中筛选子集并对其中的j进行操作非常常见,所以在data.table中专门提供了一个特殊的符号.N,具体可见 Section 4

3.2.6 修改因子水平

mtcars_dt[, setattr(gear, "levels", c("3", "4"))]
 [1] 4 4 4 3 4 4 4 3 3 3
attr(,"levels")
[1] "3" "4"

3.2.7 增减列-加”[]“可直接输出结果

# 增加一列
mtcars_dt[, gear_new := gear * 2][] # 增加gear_new列,值为原gear的两倍
              carname  gear   CYL   mpg    hp  drat    wt  qsec    vs    am
               <char> <num> <num> <num> <num> <num> <num> <num> <num> <num>
 1:         Merc 240D     4     4  24.4    62  3.69 3.190 20.00     1     0
 2:        Datsun 710     4     4  22.8    93  3.85 2.320 18.61     1     1
 3:          Merc 230     4     4  22.8    95  3.92 3.150 22.90     1     0
 4:    Hornet 4 Drive     3     6  21.4   110  3.08 3.215 19.44     1     0
 5:         Mazda RX4     4     6  21.0   110  3.90 2.620 16.46     0     1
 6:     Mazda RX4 Wag     4     6  21.0   110  3.90 2.875 17.02     0     1
 7:          Merc 280     4     6  19.2   123  3.92 3.440 18.30     1     0
 8: Hornet Sportabout     3     8  18.7   175  3.15 3.440 17.02     0     0
 9:           Valiant     3     6  18.1   105  2.76 3.460 20.22     1     0
10:        Duster 360     3     8  14.3   245  3.21 3.570 15.84     0     0
    gear_new
       <num>
 1:        8
 2:        8
 3:        8
 4:        6
 5:        8
 6:        8
 7:        8
 8:        6
 9:        6
10:        6
# 增加多列
mtcars_dt[, c("hp_new", "wt_new", "NEW") := .(sqrt(hp), mean(wt), "x")]

3.2.8 删除列

cols_to_remove <- c("mpg", "cyl", "disp")
as.data.table(mtcars)[, (cols_to_remove) := NULL]
  • 同时我们也可以使用-!符号来反向选择某些列。

3.2.9 重新编码

# 一分支
mtcars_dt[mpg < 20, mpg := 0]

# 二分支
mtcars_dt[, gear := fifelse(gear > 3, -gear, gear)]

# 多分支
mtcars_dt[, hp := fcase(hp < 100, "low",
  hp < 120, "mid",
  default = "high"
)]

3.2.10 前移/后移计算

# 1,2,3转换为NA,1,2
shift(mtcars_dt[[3]], n = 1, fill = NA, type = "lag")
 [1] NA  4  4  4  6  6  6  6  8  6
# 1,2,3转换为2,3,NA
shift(mtcars_dt[[4]], n = 1, fill = NA, type = "lead")
 [1] 22.8 22.8 21.4 21.0 21.0  0.0  0.0  0.0  0.0   NA

4 .N和.I的作用

4.1 .N:返回当前分组的观测数。

.N的作用是保存当前group中观测值的个数,是用于j中的一个特殊内置变量,当他与by结合使用时特别有用(Section 5)。在不进行分组操作的情况下,它只会返回i子集中的行数

# 计算6月以"JFK"为始发机场的所有航班数。
# 在之前的操作用使用了length(dest)的操作。
ans <- flights[
  origin == "JFK" & month == 6L,
  .N
]
ans
[1] 8422
Note
  1. 在以上代码中,.N没有被list().()包裹,因此返回的结果是一个向量。
  2. j中只使用了.N而没有引用其他列,即整个操作并未对原data.table对象进行取子集的操作,这是data.table高效的其中一个原因。

4.2 .I:返回当前分组的索引,如果没有分组,则返回行号。

mtcars_dt[, .I]
 [1]  1  2  3  4  5  6  7  8  9 10

以上代码返回了所有的行号。那么如何返回特定行的行号呢?

mtcars_dt[, .I[CYL == 6]]
[1] 4 5 6 7 9

为什么以下两行代码返回的结果不同?

mtcars_dt[, .I[CYL == 6]]
[1] 4 5 6 7 9
mtcars_dt[CYL == 6, .I]
[1] 1 2 3 4 5

第二行代码是先筛选CYL == 6的行,再返回这些行的行号,但这显然不是我们想要的结果。

注意,.I.N本质上是一个集合,我们可以在其基础上进行筛选操作

4.3 计算每种档位(gear)的汽车数量和平均里程。

test <- as.data.table(mtcars)
test[, .(
  gear_count = .N,
  gear_avg_mile = mean(mpg, na.rm = TRUE) %>% round(2)
),
by = gear
]
    gear gear_count gear_avg_mile
   <num>      <int>         <num>
1:     4         12         24.53
2:     3         15         16.11
3:     5          5         21.38

5 分组操作

  • by:指定分组变量。
  • keyby:指定分组变量的同时,创建键。
flights <- fread("d:/Myblog/posts/datatable-beginners-guide-2024-06-20/tech/flights14.csv")

DT <- readxl::read_xlsx(
  "D:/Myblog/posts/datatable-beginners-guide-2024-06-20/tech/ExamDatas_NAs.xlsx"
) %>%
  as.data.table()

5.1 分组修改

5.1.1 使用by进行分组

  1. 计算每个始发机场对应的trip次数。
ans <- flights[, .(.N), by = .(origin)]
ans
   origin     N
   <char> <int>
1:    JFK 81483
2:    LGA 84433
3:    EWR 87400
  • .N 是一个特殊变量,用于保存当前组中的行数。通过 origin 分组,可以得到每个组的行数 .N
  • 在原始数据中,始发机场的顺序为 “JFK”、“LGA”和 “EWR”。结果中保留了分组变量的原始顺序。这一点非常重要,必须牢记!
  • 由于我们没有为j中返回的列提供名称,该列通过识别特殊符号 .N 自动命名为N。当然可以对它进行重命名。
  • by可以接受列名的字符向量。
  • jby中只有一列或一个表达式需要引用时,可以去掉.()符号。当然by也可以接受多列,这时就需要.()了。
  1. 计算运营商代码 “AA” 中每对 origin, dest 的总班次数
ans <- flights[carrier == "AA", .N, 
               by = .(origin, dest)]
ans
    origin   dest     N
    <char> <char> <int>
 1:    JFK    LAX  3387
 2:    LGA    PBI   245
 3:    EWR    LAX    62
 4:    JFK    MIA  1876
 5:    JFK    SEA   298
 6:    EWR    MIA   848
 7:    JFK    SFO  1312
 8:    JFK    BOS  1173
 9:    JFK    ORD   432
10:    JFK    IAH     7
11:    JFK    AUS   297
12:    EWR    DFW  1618
13:    LGA    ORD  4366
14:    JFK    STT   229
15:    JFK    SJU   690
16:    LGA    MIA  3334
17:    LGA    DFW  3785
18:    JFK    LAS   595
19:    JFK    MCO   597
20:    JFK    EGE    85
21:    JFK    DFW   474
22:    JFK    SAN   299
23:    JFK    DCA   172
24:    EWR    PHX   121
    origin   dest     N

5.1.2 使用keyby进行分组变量排序

ans <- flights[carrier == "AA",
               .(mean(arr_delay), mean(dep_delay)),
               keyby = .(origin, dest, month)]
ans
Key: <origin, dest, month>
     origin   dest month         V1         V2
     <char> <char> <int>      <num>      <num>
  1:    EWR    DFW     1   6.427673 10.0125786
  2:    EWR    DFW     2  10.536765 11.3455882
  3:    EWR    DFW     3  12.865031  8.0797546
  4:    EWR    DFW     4  17.792683 12.9207317
  5:    EWR    DFW     5  18.487805 18.6829268
 ---                                          
196:    LGA    PBI     1  -7.758621  0.3103448
197:    LGA    PBI     2  -7.865385  2.4038462
198:    LGA    PBI     3  -5.754098  3.0327869
199:    LGA    PBI     4 -13.966667 -4.7333333
200:    LGA    PBI     5 -10.357143 -6.8571429
  • 使用keyby替换by,这将自动按照分组变量的递增顺序对结果进行排序。还记得吗,by保留了原始数据中的排序。
  • 实际上, keyby 的作用不仅仅是排序。它还通过设置名为 sortedattribute 在排序后设置一个键。 我们将在 “Keys and fast binary search based subset[TODO]”小节中进一步了解 keys ;现在,您只需知道可以使用 keybyby 中指定的列自动排列结果。
连锁操作

使用升序列 origin 和降序列 dest 对进行排序。

ans <- flights[carrier == "AA", .N, by = .(origin, dest)]
ans <- ans[order(origin, -dest)]
head(ans)
   origin   dest     N
   <char> <char> <int>
1:    EWR    PHX   121
2:    EWR    MIA   848
3:    EWR    LAX    62
4:    EWR    DFW  1618
5:    JFK    STT   229
6:    JFK    SJU   690

以上操作实现了升序列 origin 和降序列 dest 进行排序,但如果我们本来就需要进行这样的排序操作,那么命名一个ans的中间的临时变量并覆盖该结果就不太好。此时我们可以使用链式操作,即多个[]连起来使用。

ans <- flights[carrier == "AA", .N,
               by = .(origin, dest)][
                 order(origin, -dest)
               ]
head(ans, 10)
    origin   dest     N
    <char> <char> <int>
 1:    EWR    PHX   121
 2:    EWR    MIA   848
 3:    EWR    LAX    62
 4:    EWR    DFW  1618
 5:    JFK    STT   229
 6:    JFK    SJU   690
 7:    JFK    SFO  1312
 8:    JFK    SEA   298
 9:    JFK    SAN   299
10:    JFK    ORD   432

5.1.3 分组汇总

分别对每个分组(by)进行操作(计算新列),相当于`group_by + mutate)。

计算运营商代码 “AA” 中每个月每对 origin,dest 的平均到达和出发延迟?

ans <- flights[carrier == "AA",
               .(mean(arr_delay), mean(dep_delay)),
               by = .(origin, dest, month)]
ans
     origin   dest month         V1         V2
     <char> <char> <int>      <num>      <num>
  1:    JFK    LAX     1   6.590361 14.2289157
  2:    LGA    PBI     1  -7.758621  0.3103448
  3:    EWR    LAX     1   1.366667  7.5000000
  4:    JFK    MIA     1  15.720670 18.7430168
  5:    JFK    SEA     1  14.357143 30.7500000
 ---                                          
196:    LGA    MIA    10  -6.251799 -1.4208633
197:    JFK    MIA    10  -1.880184  6.6774194
198:    EWR    PHX    10  -3.032258 -4.2903226
199:    JFK    MCO    10 -10.048387 -1.6129032
200:    JFK    DCA    10  16.483871 15.5161290

特别注意,分组列的输入顺序会在结果中保留。

5.1.4 未分组汇总

DT[, .(math_avg = mean(math, na.rm = TRUE))]
   math_avg
      <num>
1: 68.04255

5.1.5 简单的分组汇总

DT[, .(
  n = .N,
  math_avg = mean(math, na.rm = TRUE),
  math_med = median(math, na.rm = TRUE)
),
by = sex
]
      sex     n math_avg math_med
   <char> <int>    <num>    <num>
1:     女    25 70.78261       73
2:     男    24 64.56522       69
3:   <NA>     1 85.00000       85

5.1.6 by中适用表达式

  • 可以直接在by中使用判断条件或表达式,特别是根据整合单位的日期或时间2进行分组。
  • 可以在by中使用表达式的同时传入列。
date <- as.IDate("2021-01-01") + 0:49 # 生成50个日期,间隔为1天
dt <- data.table(date, a = 1:50)
dt[, .(
  n = .N,
  mean_by_month = mean(a)
),
by = .(mon = month(date))
]
     mon     n mean_by_month
   <int> <int>         <num>
1:     1    31            16
2:     2    19            41

6 什么是 .SD.SDcols.EACHI

如果我们需要计算数据中所有列的平均值怎么办?难道要逐列输入mean(myCol),显示不现实。

要便捷的实现这一目标,我们要重新复习一下data.tablej表达式的特性:只要 j 表达式返回 listlist 中的每个元素都将转换为结果 data.table 中的一列

  • .SD是一个data.table,它用于保存by定义的当前组的数据子集。.SD只能在位置j中使用。
  • .SDcols常于.SD用在一起,它可以指定.SD中所包含的列,也就是对.SD取子集。
  • .EACHI是一个特殊的变量,它代表了当前分组中的第i个元素。.EACHI只能在位置j中使用。by=.EACHI允许按每一个已知i的子集分组,在使用by=.EACHI时需要设置键值 , 必须设置i。
DT = data.table(
  ID = c("b","b","b","a","a","c"),
  a = 1:6,
  b = 7:12,
  c = 13:18
)
DT
       ID     a     b     c
   <char> <int> <int> <int>
1:      b     1     7    13
2:      b     2     8    14
3:      b     3     9    15
4:      a     4    10    16
5:      a     5    11    17
6:      c     6    12    18
DT[, print(.SD), by = ID]
       a     b     c
   <int> <int> <int>
1:     1     7    13
2:     2     8    14
3:     3     9    15
       a     b     c
   <int> <int> <int>
1:     4    10    16
2:     5    11    17
       a     b     c
   <int> <int> <int>
1:     6    12    18
Empty data.table (0 rows and 1 cols): ID
Note
  • .SD 默认情况下包含分组列以外的所有列。

6.0.1 同时操作多列

data.table中,同时修改多列大都使用的是lapply()map_**()以及特殊符号进行操作:

DT[, map(.SD, mean), by = ID]
       ID     a     b     c
   <char> <num> <num> <num>
1:      b   2.0   8.0  14.0
2:      a   4.5  10.5  16.5
3:      c   6.0  12.0  18.0
DT[, lapply(.SD, mean), by = ID]
       ID     a     b     c
   <char> <num> <num> <num>
1:      b   2.0   8.0  14.0
2:      a   4.5  10.5  16.5
3:      c   6.0  12.0  18.0

6.0.2 .SD和.SDcols

.SD默认包含了分组变量以外的所有列,那如果只需要计算数据集中其中几列的平均值该怎么办?

  • .SDcols:与.SD连用,用来选择包含在.SD中的列,支持索引、列名、连选、反选、正则表达式、条件判断函数等。
  • 同时修改多列的代码,均只保留新列,如果需要保留原有列,则需使用:=符号,同时准备新列名,并在j表达式中使用对新列名进行定义。

现在我们使用.SD.SDcols来计算origindestmonth 分组的 arr_delaydep_delay 列的 mean()

ans <- flights[carrier == "AA",
               map(.SD, mean),
               by = .(origin, dest, month),
               .SDcols = c("arr_delay", "dep_delay")]
head(ans)
   origin   dest month arr_delay  dep_delay
   <char> <char> <int>     <num>      <num>
1:    JFK    LAX     1  6.590361 14.2289157
2:    LGA    PBI     1 -7.758621  0.3103448
3:    EWR    LAX     1  1.366667  7.5000000
4:    JFK    MIA     1 15.720670 18.7430168
5:    JFK    SEA     1 14.357143 30.7500000
6:    EWR    MIA     1 11.011236 12.1235955

6.0.3 获取每个组的子集

获取每个月数据的前两行。

ans <- flights[, slice_head(.SD, n = 2), by = month]
head(ans)
   month  year   day dep_delay arr_delay carrier origin   dest air_time
   <int> <int> <int>     <int>     <int>  <char> <char> <char>    <int>
1:     1  2014     1        14        13      AA    JFK    LAX      359
2:     1  2014     1        -3        13      AA    JFK    LAX      363
3:     2  2014     1        -1         1      AA    JFK    LAX      358
4:     2  2014     1        -5         3      AA    JFK    LAX      358
5:     3  2014     1       -11        36      AA    JFK    LAX      375
6:     3  2014     1        -3        14      AA    JFK    LAX      368
   distance  hour
      <int> <int>
1:     2475     9
2:     2475    11
3:     2475     8
4:     2475    11
5:     2475     8
6:     2475    11
DT
       ID     a     b     c
   <char> <int> <int> <int>
1:      b     1     7    13
2:      b     2     8    14
3:      b     3     9    15
4:      a     4    10    16
5:      a     5    11    17
6:      c     6    12    18
DT[, .(val = c(a, b)), by = ID]
        ID   val
    <char> <int>
 1:      b     1
 2:      b     2
 3:      b     3
 4:      b     7
 5:      b     8
 6:      b     9
 7:      a     4
 8:      a     5
 9:      a    10
10:      a    11
11:      c     6
12:      c    12

6.1 总结

DT[i, j, by]data.table的基本语法。

6.2 使用i

  • 我们可以像 data.frame 一样对行进行子集。
  • 我们还可以使用 order()data.table 进行排序,内部使用 data.table 的快速排序,以获得更好的性能。
  • 我们还可以在i中做更多事,We will see this in the “Keys and fast binary search based subsets” and “Joins and rolling joins” vignette.[TODO]

6.3 使用j

  1. 选择列:DT[, .(colA, colB)]DT[, c("colA", "colB")] .

  2. 对列进行计算:DT[, .(sum(colA), mean(colB))].

    • 可以设定新列名:DT[, .(sA =sum(colA), mB = mean(colB))]
  3. i连用:DT[colA > value, sum(colB)].

6.4 使用by

  • 我们可以通过指定列列表或列名字符向量,甚至表达式来按列分组。 j 的灵活性与 byi 结合在一起,构成了一种非常强大的语法。

  • by 可以处理多列和表达式.

  • 我们可以通过 keyby 分组列来自动对分组结果进行排序

  • 我们可以在 j 中使用 .SD.SDcols ,使用已经熟悉的基本函数对多列进行操作。下面是一些示例:

    1. DT[, lapply(.SD, fun), by = ..., .SDcols = ...]: 将 fun 应用于 .SDcols 中指定的所有列,同时按 by 中指定的列分组。
    2. DT[, head(.SD, n), by = ...]: 返回每个组的前n行。
    3. DT[col > val, head(.SD, 1), by = ...]: 将 ijby 合并

只要 j 返回 list ,列表中的每个元素都将成为结果 data.table 中的一列

Footnotes

  1. 引用语法,相当于只有一个对象在内存中,不做多余的复制,用两个指针都指向该同一对象,无论操作哪个指针,本质上都是在修改该同一对象。↩︎

  2. data.table提供了快速处理时间和日期的IDataTime类。↩︎