每个 id 单行到每个 id 多行

Single row per id to multiple row per id

我想根据给定的时间间隔将观察结果从每个 ID 单行扩展到每个 ID 多行:

> dput(df)
structure(list(id = c(123, 456, 789), gender = c(0, 1, 1), yr.start = c(2005, 
2010, 2000), yr.last = c(2007, 2012, 2000)), .Names = c("id", 
"gender", "yr.start", "yr.last"), class = c("tbl_df", "tbl", 
"data.frame"), row.names = c(NA, -3L))
> df
# A tibble: 3 x 4
     id gender yr.start yr.last
  <dbl>  <dbl>    <dbl>   <dbl>
1   123      0     2005    2007
2   456      1     2010    2012
3   789      1     2000    2000

我希望每年将 id 扩展为一行:

> dput(df_out)
structure(list(id = c(123, 123, 123, 456, 456, 456, 789), gender = c(0, 
0, 0, 1, 1, 1, 1), yr = c(2005, 2006, 2007, 2010, 2011, 2012, 
2000)), .Names = c("id", "gender", "yr"), class = c("tbl_df", 
"tbl", "data.frame"), row.names = c(NA, -7L))
> df_out
# A tibble: 7 x 3
     id gender    yr
  <dbl>  <dbl> <dbl>
1   123      0  2005
2   123      0  2006
3   123      0  2007
4   456      1  2010
5   456      1  2011
6   456      1  2012
7   789      1  2000

我知道如何melt/reshape,但我不确定如何扩展这些年。 谢谢

这是一个基本的 R 方法。

# expand years to a list
yearList <- mapply(":", df$yr.start, df$yr.last)

现在,使用此列表计算每个 ID 的重复行数(rep 的第二个参数),然后将其附加为向量(从带有 unlist 的列表转换而来)使用 cbind.

# get data.frame
cbind(df[rep(seq_along(df$id), lengths(yearList)), c("id", "gender")], yr=unlist(yearList))
     id gender   yr
1   123      0 2005
1.1 123      0 2006
1.2 123      0 2007
2   456      1 2010
2.1 456      1 2011
2.2 456      1 2012
3   789      1 2000

这是一个 tidyverse 解决方案

library(tidyverse)
df %>%
  group_by(id, gender) %>%
  nest() %>%
  mutate(data = map(data, ~ seq(.x$yr.start, .x$yr.last))) %>%
  unnest() %>%
  rename(year = data)

# A tibble: 7 x 3
     id gender  year
  <dbl>  <dbl> <int>
1   123      0  2005
2   123      0  2006
3   123      0  2007
4   456      1  2010
5   456      1  2011
6   456      1  2012
7   789      1  2000

您可以 gather 为长格式,然后使用 tidyr.

通过 complete 填充缺失的行
library(dplyr)
library(tidyr)

df %>%
     gather(group, yr, starts_with("yr") ) %>%
     group_by(id, gender) %>%
     complete(yr = full_seq(yr, period = 1) )

您可以使用 select 删除多余的列。

df %>%
     gather(group, yr, starts_with("yr") ) %>%
     select(-group) %>%
     group_by(id, gender) %>%
     complete(yr = full_seq(yr, period = 1) )

# A tibble: 8 x 3
# Groups:   id, gender [3]
     id gender    yr
  <dbl>  <dbl> <dbl>
1   123      0  2005
2   123      0  2006
3   123      0  2007
4   456      1  2010
5   456      1  2011
6   456      1  2012
7   789      1  2000
8   789      1  2000

dplyr 中使用 do 的另一种方法,但它比基本 R 方法慢。

df %>%
  group_by(id, gender) %>%
  do(data.frame(yr=.$yr.start:.$yr.last))

# # A tibble: 7 x 3
# # Groups:   id, gender [3]
#      id gender    yr
#   <dbl>  <dbl> <int>
# 1   123      0  2005
# 2   123      0  2006
# 3   123      0  2007
# 4   456      1  2010
# 5   456      1  2011
# 6   456      1  2012
# 7   789      1  2000

由于 OP 提到他的生产数据集有超过 100 万行并且他正在对不同的解决方案进行基准测试,因此可能值得尝试 data.table 版本:

library(data.table)   # CRAN version 1.10.4 used
data.table(DF)[, .(yr = yr.start:yr.last), by = .(id, gender)]

哪个return

    id gender    yr
1: 123      0  2005
2: 123      0  2006
3: 123      0  2007
4: 456      1  2010
5: 456      1  2011
6: 456      1  2012
7: 789      1  2000

如果非变化列比 gender 多,则进行连接可能比在分组参数中包含所有这些列更有效 by =:

data.table(DF)[DF[, .(yr = yr.start:yr.last), by = id], on = "id"]
    id gender yr.start yr.last    yr
1: 123      0     2005    2007  2005
2: 123      0     2005    2007  2006
3: 123      0     2005    2007  2007
4: 456      1     2010    2012  2010
5: 456      1     2010    2012  2011
6: 456      1     2010    2012  2012
7: 789      1     2000    2000  2000

请注意,这两种方法都假定 id 在输入数据中是唯一的。


基准测试

OP 有 that he is surprised that above data.table solution is five times slower than ,显然 OP 的生产数据集超过 100 万行。

此外,该问题吸引了 5 个不同的答案以及额外的建议。因此,值得在处理速度方面比较解决方案。

数据

由于生产数据集不可用,并且问题大小以及数据结构等其他因素对于基准测试很重要,因此创建了样本数据集。

# parameters
n_rows <- 1E2
yr_range <- 10L
start_yr <- seq(2000L, length.out = 10L, by = 1L)
# create sample data set
set.seed(123L)
library(data.table)
DT <- data.table(id = seq_len(n_rows),
                 gender = sample(0:1, n_rows, replace = TRUE),
                 yr.start = sample(start_yr, n_rows, replace = TRUE))
DT[, yr.last := yr.start + sample(0:yr_range, n_rows, replace = TRUE)]
DF <- as.data.frame(DT)
str(DT)
Classes ‘data.table’ and 'data.frame':    100 obs. of  4 variables:
 $ id      : int  1 2 3 4 5 6 7 8 9 10 ...
 $ gender  : int  0 1 0 1 1 0 1 1 1 0 ...
 $ yr.start: int  2005 2003 2004 2009 2004 2008 2009 2006 2004 2001 ...
 $ yr.last : int  2007 2013 2010 2014 2008 2017 2013 2009 2005 2002 ...
 - attr(*, ".internal.selfref")=<externalptr>

对于第一个 运行,创建了 100 行,开始年份可以在 2000 年到 2009 年之间变化,并且个人 id 可以涵盖的年份跨度在 0 到 10 年之间。因此,结果集应该有大约 100 * (10 + 1) / 2 行。

另外,尽管 生产数据可能有 2 到 10 个不变的列,但只包含一个额外的列 gender

代码

library(magrittr)
bm <- microbenchmark::microbenchmark(
  lmo = {
    yearList <- mapply(":", DF$yr.start, DF$yr.last)
    res_lmo <- cbind(DF[rep(seq_along(DF$id), lengths(yearList)), c("id", "gender")], 
                     yr=unlist(yearList))
  },
  hao = {
    res_hao <- DF %>%
      dplyr::group_by(id, gender) %>%
      tidyr::nest() %>%
      dplyr::mutate(data = purrr::map(data, ~ seq(.x$yr.start, .x$yr.last))) %>%
      tidyr::unnest() %>%
      dplyr::rename(yr = data)
  },
  aosmith = {
    res_aosmith <- DF %>%
      tidyr::gather(group, yr, dplyr::starts_with("yr") ) %>%
      dplyr::select(-group) %>%
      dplyr::group_by(id, gender) %>%
      tidyr::complete(yr = tidyr::full_seq(yr, period = 1) )
  },
  jason = {
    res_jason <- DF %>%
      dplyr::group_by(id, gender) %>%
      dplyr::do(data.frame(yr=.$yr.start:.$yr.last))
  },
  uwe1 = {
    res_uwe1 <- DT[, .(yr = yr.start:yr.last), by = .(id, gender)]
  },
  uwe2 = {
    res_uwe2 <- DT[DT[, .(yr = yr.start:yr.last), by = id], on = "id"
                   ][, c("yr.start", "yr.last") := NULL]
  },
  frank1 = {
    res_frank1 <- DT[rep(1:.N, yr.last - yr.start + 1L), 
                     .(id, gender, yr = DT[, unlist(mapply(":", yr.start, yr.last))])]
    },
  frank2 = {
    res_frank2 <- DT[, {
      m = mapply(":", yr.start, yr.last); c(.SD[rep(.I, lengths(m))], .(yr = unlist(m)))}, 
      .SDcols=id:gender]
  },
  times = 3L
)

请注意,对 tidyverse 函数的引用是明确的,以避免由于名称混乱而导致的名称冲突 space。

第一个运行

Unit: microseconds
    expr        min          lq       mean     median         uq        max neval
     lmo    655.860    692.6740    968.749    729.488   1125.193   1520.899     3
     hao  40610.776  41484.1220  41950.184  42357.468  42619.887  42882.307     3
 aosmith 319715.984 336006.9255 371176.437 352297.867 396906.664 441515.461     3
   jason  77525.784  78197.8795  78697.798  78869.975  79283.804  79697.634     3
    uwe1    834.079    870.1375    894.869    906.196    925.264    944.332     3
    uwe2   1796.910   1810.8810   1880.482   1824.852   1922.268   2019.684     3
  frank1    981.712   1057.4170   1086.680   1133.122   1139.164   1145.205     3
  frank2    994.172   1003.6115   1081.016   1013.051   1124.438   1235.825     3

对于给定的 100 行大小的问题,时间清楚地表明 dplyr/tidyr 解决方案比基础 R 或 data.table 解决方案慢很多。

结果基本一致:

all.equal(as.data.table(res_lmo), res_uwe1)
all.equal(res_hao, res_uwe1)
all.equal(res_jason, res_uwe1)
all.equal(res_uwe2, res_uwe1)
all.equal(res_frank1, res_uwe1)
all.equal(res_frank2, res_uwe1)

return TRUE 除了 all.equal(res_aosmith, res_uwe1) 其中 returns

[1] "Incompatible type for column yr: x numeric, y integer"

第二个运行

由于执行时间长,tidyverse 解决方案在对较大问题进行基准测试时被跳过。

修改后的参数

n_rows <- 1E4
yr_range <- 100L

结果集预计包含大约 500'000 行。

Unit: milliseconds
   expr        min         lq      mean    median        uq       max neval
    lmo 425.026101 447.716671 455.85324 470.40724 471.26681 472.12637     3
   uwe1   9.555455   9.796163  10.05562  10.03687  10.30571  10.57455     3
   uwe2  18.711805  18.992726  19.40454  19.27365  19.75091  20.22817     3
 frank1  22.639031  23.129131  23.58424  23.61923  24.05685  24.49447     3
 frank2  13.989016  14.124945  14.47987  14.26088  14.72530  15.18973     3

对于给定的问题大小和结构,data.table 解决方案是最快的,而基础 R 方法要慢一个数量级。最简洁的解决方案uwe1也是最快的,在这里。

请注意,结果取决于数据的结构,尤其是参数 n_rowsyr_range 以及不变列的数量。如果这些列的数量超过 gender,时间可能会有所不同。

基准测试结果与 OP 对执行速度的观察相矛盾,需要进一步调查。