按组附加第一行到原始数据。

5

我有一个带有分组变量“ID”和一些值的数据:

ID, Value
1, 1
1, 2
1, 3
1, 4
2, 5
2, 6 
2, 7
2, 8

在每个组内,我想将第一行附加到最后一行之后。

ID, Value
1, 1
1, 2
1, 3
1, 4
1, 1 # First row of ID 1 inserted as last row in the group
2, 5
2, 6 
2, 7
2, 8
2, 5 # First row of ID 2 inserted as last row in the group

我有5000行这个。

5个回答

5

tidyverse 中,你可以使用 add_row 结合 do(现已不再推荐使用) 或者 group_modify实验性功能):

dat |>
  group_by(ID) |>
  do(add_row(., ID = unique(.$ID), Value = first(.$Value))) |>
  ungroup()

dat |>
  group_by(ID) |>
  group_modify(~ add_row(., Value = first(.$Value))) |>
  ungroup()

或者使用 bind_rowssummarize(我的变体,感谢 @Gregor Thomas):

dat |> 
  group_by(ID) |>
  summarize(bind_rows(cur_data(), head(cur_data(), 1))) |>
  ungroup()

或者采用与@Henrik相同的逻辑,使用bind_rowsfilterarrange函数:

dat |>
  bind_rows(dat |> filter(!duplicated(ID))) |>
  arrange(ID)

输出:

# A tibble: 10 × 2
      ID Value
   <int> <dbl>
 1     1     1
 2     1     2
 3     1     3
 4     1     4
 5     1     1
 6     2     5
 7     2     6
 8     2     7
 9     2     8
10     2     5

感谢@SamR提供的数据。


1
另一种方法(因为“do”已被弃用):dat %>% group_by(ID) %>% summarize(bind_rows(cur_data(), cur_data()[1,])) - Gregor Thomas

4

使用data.table

library(data.table)

dt <- data.table(ID = rep(1:2, each = 4), Value = 1:8)
dt[,.(Value = c(Value, first(Value))), ID]
#>     ID Value
#>  1:  1     1
#>  2:  1     2
#>  3:  1     3
#>  4:  1     4
#>  5:  1     1
#>  6:  2     5
#>  7:  2     6
#>  8:  2     7
#>  9:  2     8
#> 10:  2     5

使用5000行表格进行基准测试:

library(dplyr)

dt <- data.table(ID = rep(1:1250, each = 4), Value = 1:5e3)

f1 <- function(dt) dt[,.(Value = c(Value, first(Value))), ID]
# base R
f2 <- function(dt) do.call(rbind, lapply(split(dt, by = "ID"), function(x) rbind(x, x[1,])))
# tidyverse
f3 <- function(dt) {
  dt %>%
    group_by(ID) %>%
    do(add_row(., ID = unique(.$ID), Value = first(.$Value))) %>%
    ungroup()
}
f4 <- function(dt) {
  dt %>%
    group_by(ID) %>%
    group_modify(~ add_row(., Value = first(.$Value))) %>%
    ungroup()
}

microbenchmark::microbenchmark(data.table = f1(dt),
                               "base R" = f2(dt),
                               tidyverse1 = f3(dt),
                               tidyverse2 = f4(dt),
                               times = 10)
#> Unit: milliseconds
#>        expr      min       lq      mean   median       uq      max neval
#>  data.table   3.4989   3.6844   4.16619   4.3085   4.4623   5.3020    10
#>      base R 245.1397 263.1636 283.61131 284.8053 307.7105 310.3901    10
#>  tidyverse1 761.7097 773.3705 791.05115 787.9463 808.5416 821.5321    10
#>  tidyverse2 711.9593 716.4959 752.20273 728.2170 782.6474 837.1926    10

如果速度非常重要,这个简单的Rcpp函数提供了一个非常快速的解决方案。
Rcpp::cppFunction(
  "IntegerVector firstLast(const IntegerVector& x) {
    const int n = x.size();
    IntegerVector idxOut(2*n);
    int i0 = 1;
    int idx = 0;
    idxOut(0) = 1;
    
    for (int i = 1; i < n; i++) {
      if (x(i) != x(i - 1)) {
        idxOut(++idx) = i0;
        i0 = i + 1;
      }
      idxOut(++idx) = i + 1;
    }
    idxOut(++idx) = i0;
    return idxOut[Rcpp::Range(0, idx)];
  }"
)

对比答案中最快的解决方案(在更大的数据集上)进行基准测试:

dt = data.table(ID = rep(1:125e4, each = 4), Value = 1:5e6)

microbenchmark::microbenchmark(
  f_uniq_dt = setorder(rbindlist(list(dt, unique(dt, by = "ID"))), ID),
  f_Rcpp = dt[firstLast(dt$ID)],
  check = "equal"
)
#> Unit: milliseconds
#>      expr     min       lq     mean   median       uq      max neval
#> f_uniq_dt 78.6056 83.71345 95.42876 85.80720 90.03685 175.8867   100
#>    f_Rcpp 49.1485 53.38275 60.96322 55.44925 58.01485 121.3637   100

4

使用 !duplicated 按 "ID" 获取第一行 (比 "by group" 操作更高效)。将结果与原数据使用 rbind 合并,再使用 order 排序:

df = data.frame(ID = rep(1:2, each = 4), Value = 1:8)

d2 = rbind(df, df[!duplicated(df$ID), ])
d2[order(d2$ID), ]
#   ID Value
# 1   1     1
# 2   1     2
# 3   1     3
# 4   1     4
# 11  1     1
# 5   2     5
# 6   2     6
# 7   2     7
# 8   2     8
# 51  2     5

data.table :: duplicated相同的思路:
d = as.data.table(df)
d2 = rbindlist(list(d, d[!duplicated(d, by = "ID")]))
setorder(d2, ID)

使用 data.table::unique 更加简单明了:

d2 = rbindlist(list(d, unique(d, by = "ID")))
setorder(d2, ID)

还有 data.table::rowid

d2 = rbindlist(list(d, d[rowid(ID) == 1]))
setorder(d2, ID)

避免使用“按组”操作,在5000行数据集上,!duplicateduniquerowid替代方法都比之前基准测试中的明显胜者data.table解决方案(使用by)更快。(参见OP):

df = data.frame(ID = rep(1:1250, each = 4), Value = 1:5e3)
d = as.data.table(d)

microbenchmark(
  f_by = {
    d1 = d[ , .(Value = c(Value, first(Value))), by = ID]
  },
  f_dupl_df = {
    d2 = rbind(df, df[!duplicated(df$ID), ])
    d2 = d2[order(d2$ID), ]
  },
  f_dupl_dt = {
    d3 = rbindlist(list(d, d[!duplicated(d, by = "ID")]))
    setorder(d3, ID)
  },
  f_uniq_dt = {
    d4 = rbindlist(list(d, unique(d, by = "ID")))
    setorder(d4, ID)
  },
  f_rowid = {
    d5 = rbindlist(list(d, d[rowid(ID) == 1]))
    setorder(d5, ID)
  },
  times = 10L)
# Unit: milliseconds
#       expr    min     lq     mean  median      uq     max
#      f_by 8.5167 9.1397 11.01410 9.90925 12.3327 15.9134
# f_dupl_df 6.8337 7.0901  8.31057 7.56810  8.4899 13.9278
# f_dupl_dt 2.4742 2.6687  3.24932 3.18670  3.7993  4.3318
# f_uniq_dt 2.2059 2.4225  3.50756 3.36250  4.4590  5.6632
#   f_rowid 2.2963 2.4295  3.43876 2.74345  4.8035  5.9278

all.equal(d1, as.data.table(d2))
all.equal(d1, d3)
all.equal(d1, d4)
all.equal(d1, d5)
# [1] TRUE

然而,亚秒级基准测试并不十分具有信息性,因此请尝试在具有许多组的大型数据上进行测试。 base 解决方案会失去优势。 data.table::duplicateduniquerowid 的扩展效果更好,现在快了约 20 倍,其中 data.table::unique 最快。
df = data.frame(ID = rep(1:1250000, each = 4), Value = 1:5e6)
d = as.data.table(df)

# Unit: milliseconds
#      expr        min         lq       mean     median         uq        max neval
#      f_by  6834.5959  7157.1686 12273.2399  7775.3919  8850.5324 35339.0262    10
# f_dupl_df 10732.1536 11035.4886 19440.4964 11691.5347 37956.6961 38387.4927    10
# f_dupl_dt   174.5640   183.8399   391.8605   381.8920   401.4929   962.4948    10
# f_uniq_dt   156.1267   161.9555   212.3472   180.7912   209.3905   406.7780    10
#   f_rowid   192.1106   197.1564   380.0023   234.5851   474.5024  1172.6529    10

为了完整起见,使用mult = "first"的二分查找来选择第一个匹配项:
d[.(unique(ID)), on = .(ID), mult = "first"]

然而,在上述两种情况下,它的结束时间是与unique替代方案相比两倍长。


2

以下是基于R语言的方法:

do.call(rbind, 
    lapply(split(dat, dat$ID), \(id_df) rbind(id_df, id_df[1,]))
) 
#      ID Value
# 1.1   1     1
# 1.2   1     2
# 1.3   1     3
# 1.4   1     4
# 1.5   1     1
# 2.5   2     5
# 2.6   2     6
# 2.7   2     7
# 2.8   2     8
# 2.51  2     5

它会给你略微奇怪的行名 - 如果你在意这个,你可以用(或将其管道到)tibble :: as_tibble()中来包装它,这样就完全删除了行名。

或者你可以使用 data.table :: rbindlist(lapply(split(dat, dat $ ID), \(id_df) rbind(id_df, id_df [1,]))),因为data.table也不使用行名。

数据

dat  <- read.csv(text = "ID, Value
1, 1
1, 2
1, 3
1, 4
2, 5
2, 6 
2, 7
2, 8", h=T)

1
您可以尝试以下的 data.table 选项。
> setDT(df)[, .SD[(seq(1 + .N) - 1) %% .N + 1], ID]
    ID Value
 1:  1     1
 2:  1     2
 3:  1     3
 4:  1     4
 5:  1     1
 6:  2     5
 7:  2     6
 8:  2     7
 9:  2     8
10:  2     5

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接