【问题标题】:Counting rows based upon conditional grouping with dplyr使用 dplyr 基于条件分组计算行数
【发布时间】:2016-07-12 11:09:09
【问题描述】:

我有一个如下的数据框:

        position_time telematic_trip_no  lat_dec lon_dec
1 2016-06-05 00:00:01         526132109 -26.6641 27.8733
2 2016-06-05 00:00:01         526028387 -26.6402 27.8059
3 2016-06-05 00:00:01         526081476 -26.5545 28.3263
4 2016-06-05 00:00:04         526140512 -26.5310 27.8704
5 2016-06-05 00:00:05         526140518 -26.5310 27.8704
6 2016-06-05 00:00:19         526006880 -26.5010 27.8490 
  is_stolen hour_of_day time_of_day day_of_week  lat_min
1         0           0           0      Sunday -26.6651
2         0           0           0      Sunday -26.6412
3         0           0           0      Sunday -26.5555
4         0           0           0      Sunday -26.5320
5         0           0           0      Sunday -26.5320
6         0           0           0      Sunday -26.5020
   lat_max lon_max lon_min 
1 -26.6631 27.8743 27.8723     
2 -26.6392 27.8069 27.8049    
3 -26.5535 28.3273 28.3253    
4 -26.5300 27.8714 27.8694      
5 -26.5300 27.8714 27.8694      
6 -26.5000 27.8500 27.8480     

现在我要做的是计算 is_stolen = 1 的每一行,即满足以下条件的数据帧中的行数:

  • lat_declon_declat_maxlat_minlon_max 之间和 lon_min(即适合该 GPS 点周围的“框”)
  • time_of_dayday_of_week 与感兴趣行的相同
  • 行的telematic_trip_no需要与感兴趣行的不同
  • 最后匹配行的is_stolen标签需要等于0

我已经编写了一个脚本来使用 for 循环来执行此操作,但它运行非常很慢,这让我开始思考是否有一种有效的方法来进行复杂的行计数在许多条件下使用 dplyr 或 data.table 之类的东西?

ps 如果你很好奇,我确实在尝试计算一辆被盗汽车在一次典型的旅行中经过了多少辆汽车:)

【问题讨论】:

  • dput() 的足够数据,包括必要的条件,可以帮助他人帮助您。
  • 你能把你的“for循环”代码也包括进来吗?
  • “感兴趣的行”指的是什么?您是否选择了一行,然后将其与所有其他行进行比较?
  • 尝试使用which() 语句创建满足所有条件的行的子集。这样,它只会在提取适当的数据时遍历数据集。您可能需要两到四个语句的序列来保持人类可读性和紧凑性,但它通常比复杂循环更快且更不容易出错。这是克兰的描述。 inside-r.org/r-doc/base/which

标签: r dplyr


【解决方案1】:

根据您对问题的描述,以下应该可行

library(dplyr)
library(stats)
# df is the data.frame (see below)
df <- cbind(ID=seq_len(nrow(df)),df)
r.stolen <- which(df$is_stolen == 1)
r.not <- which(df$is_stolen != 1)
print(df[rep(r.not, times=length(r.stolen)),] %>%
  setNames(.,paste0(names(.),"_not")) %>%
    bind_cols(df[rep(r.stolen, each=length(r.not)),], .) %>% 
      mutate(in_range = as.numeric(telematic_trip_no != telematic_trip_no_not & time_of_day == time_of_day_not & day_of_week == day_of_week_not & lat_dec >= lat_min_not & lat_dec <= lat_max_not & lon_dec >= lon_min_not & lon_dec <= lon_max_not)) %>%
        group_by(ID) %>%
          summarise(count = sum(in_range)) %>% 
            arrange(desc(count)))

第一行只是将一个名为ID 的列添加到df,它通过行号来标识行,我们稍后可以dplyr::group_by 进行计数。

接下来的两行将行划分为被盗和未被盗的汽车。关键是:

  1. 复制每一行被盗汽车N 次,其中N 是未被盗汽车的行数,
  2. 复制未被盗汽车的行(作为一个块)M 次,其中M 是被盗汽车的行数,并且
  3. 将 (2) 的结果作为新列附加到 (1) 并更改这些新列的名称,以便我们可以在条件中引用它们

(3) 的结果中的行枚举了原始数据帧中所有被盗行和未被盗行的行,因此您的条件可以以数组方式应用。 dplyr 管道 R 工作流是代码的第四行(包装在 print() 中)这样做:

  • 第一个命令使用times复制未被盗的汽车行
  • 第二个命令在我们绑定列时将_not附加到列名中,以将它们与被盗的汽车列区分开来。感谢 this SO answer 的宝石。
  • 第三条命令使用each复制被盗汽车行,并使用dplyr::bind_cols将先前的结果作为新列追加
  • 第四条命令使用dplyr::mutate 创建一个名为in_range 的新列,这是应用条件的结果。布尔结果转换为{0,1} 以便于累积
  • 管道中的其余命令对由ID 分组的in_range 进行计数,并将结果按计数的递减顺序排列。请注意,现在ID 是标识原始数据帧中is_stolen = 1 的行的列,而ID_notis_stolen = 0 的行的列

这假设您想要原始数据框中is_stolen = 1 的每一行的计数,这就是您在问题中所说的。相反,如果您真的想要每个被盗的telematic_trip_no 的计数,那么您可以使用

group_by(telematic_trip_no) %>%

改为在管道中。

我已经使用以下数据 sn-p 对此进行了测试

df <- structure(list(position_time = structure(c(1L, 1L, 1L, 2L, 3L, 
                4L, 4L, 5L, 6L, 7L, 8L, 9L, 10L), .Label = c("2016-06-05 00:00:01", 
                "2016-06-05 00:00:04", "2016-06-05 00:00:05", "2016-06-05 00:00:19", 
                "2016-06-05 00:00:20", "2016-06-05 00:00:22", "2016-06-05 00:00:23", 
                "2016-06-05 00:00:35", "2016-06-05 00:09:34", "2016-06-06 01:00:06"
                ), class = "factor"), telematic_trip_no = c(526132109L, 526028387L, 
                526081476L, 526140512L, 526140518L, 526006880L, 526017880L, 526027880L, 
                526006880L, 526006890L, 526106880L, 526005880L, 526007880L), 
                lat_dec = c(-26.6641, -26.6402, -26.5545, -26.531, -26.531, 
                -26.501, -26.5315, -26.5325, -26.501, -26.5315, -26.5007, 
                -26.5315, -26.5315), lon_dec = c(27.8733, 27.8059, 28.3263, 
                27.8704, 27.8704, 27.849, 27.88, 27.87, 27.849, 27.87, 27.8493, 
                27.87, 27.87), is_stolen = c(0L, 0L, 0L, 0L, 0L, 0L, 1L, 
                1L, 1L, 1L, 1L, 1L, 1L), hour_of_day = c(0L, 0L, 0L, 0L, 
                0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L), time_of_day = c(0L, 
                0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 9L, 0L), day_of_week = structure(c(2L, 
                2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 1L), .Label = c("Monday", 
                "Sunday"), class = "factor"), lat_min = c(-26.6651, -26.6412, 
                -26.5555, -26.532, -26.532, -26.502, -26.532, -26.532, -26.502, 
                -26.532, -26.502, -26.532, -26.532), lat_max = c(-26.6631, 
                -26.6392, -26.5535, -26.53, -26.53, -26.5, -26.53, -26.53, 
                -26.5, -26.53, -26.5, -26.53, -26.53), lon_max = c(27.8743, 
                27.8069, 28.3273, 27.8714, 27.8714, 27.85, 27.8714, 27.8714, 
                27.85, 27.8714, 27.85, 27.8714, 27.8714), lon_min = c(27.8723, 
                27.8049, 28.3253, 27.8694, 27.8694, 27.848, 27.8694, 27.8694, 
                27.848, 27.8694, 27.848, 27.8694, 27.8694)), .Names = c("position_time", 
                "telematic_trip_no", "lat_dec", "lon_dec", "is_stolen", "hour_of_day", 
                "time_of_day", "day_of_week", "lat_min", "lat_max", "lon_max", 
                "lon_min"), class = "data.frame", row.names = c(NA, -13L))

在这里,我将带有 is_stolen = 17 新行附加到您原来的 6 行,这些行都是 is_stolen = 0

  1. 第一个添加的带有telematic_trip_no = 526005880 的行违反了所有未被盗行的经度条件,因此其计数应为0
  2. 添加的第二行带有telematic_trip_no = 526006880 违反了所有未被盗行的纬度条件,因此其计数应为0
  3. 添加的第三行带有telematic_trip_no = 526007880 违反了所有未被盗行的telematic_trip_no 条件,因此其计数应为0
  4. 添加的第四行telematic_trip_no = 526006890满足45未被盗行的条件,因此其计数应为2
  5. 添加telematic_trip_no = 526106880 的第五行满足6 行未被窃取的条件,因此其计数应为1
  6. 添加的第六行带有telematic_trip_no = 526017880 违反了所有未被盗行的time_of_day 条件,因此其计数应为0
  7. 添加的第七行带有telematic_trip_no = 526027880 违反了所有未被盗行的day_of_week 条件,因此其计数应为0

在这个数据上运行代码给出:

# A tibble: 7 x 2
     ID count
  <int> <dbl>
1    10     2
2    11     1
3     7     0
4     8     0
5     9     0
6    12     0
7    13     0

正如预期的那样,回想带有is_stolen = 1 的附加行从带有ID = 7 的行7 开始。

如果要按telematic_trip_no 分组,我们会得到结果:

# A tibble: 7 x 2
  telematic_trip_no count
              <int> <dbl>
1         526006890     2
2         526106880     1
3         526005880     0
4         526006880     0
5         526007880     0
6         526017880     0
7         526027880     0

需要注意的是,上述方法确实会消耗内存。在最坏的情况下,行数增长到N^2/4,其中N 是原始数据框中的行数,而用于评估条件的数据框的列数加倍。与大多数阵列处理技术一样,速度和内存之间存在折衷。

希望这会有所帮助。

【讨论】:

    【解决方案2】:

    The current development version of data.table, v1.9.7 有一个新功能 non-equi 连接,这使得条件连接非常简单。使用@aichao的数据:

    require(data.table) # v1.9.7+
    setDT(df)[, ID := .I] # add row numbers
    not_stolen = df[is_stolen == 0L]
    is_stolen  = df[is_stolen == 1L]
    
    not_stolen[is_stolen, 
        .(ID = i.ID, N = .N - sum(telematic_trip_no == i.telematic_trip_no)), 
        on = .(time_of_day, day_of_week, lat_min <= lat_dec, 
              lat_max >= lat_dec, lon_min <= lon_dec, lon_max >= lon_dec), 
        by=.EACHI][, .(ID, N)]
    #    ID  N
    # 1:  7 NA
    # 2:  8 NA
    # 3:  9  0
    # 4: 10  2
    # 5: 11  1
    # 6: 12 NA
    # 7: 13 NA
    

    not_stolen[is_stolen, 部分执行 subset-like 连接操作。即,对于 is_stolen 中的每一行,匹配 行索引(基于提供给on= 参数)被提取。

    by = .EACHI 确保对于i(第一个)参数(此处为is_stolen)中的每一行,在相应的匹配行索引上,评估j 中提供的表达式,第二个参数.(ID = i.ID, N = .N-sum(telematic_trip_no==i.telematic_trip_no)), .这将返回上面显示的结果。

    HTH。

    【讨论】:

    • 我正在寻找这样的连接功能。很高兴知道。谢谢。
    猜你喜欢
    • 1970-01-01
    • 2019-11-21
    • 2014-05-11
    • 2023-03-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多