每个id单行到每个id多行

时间:2017-06-20 19:58:54

标签: r dataframe dplyr reshape melt

我想基于给定的时间间隔将观察从单行每个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

我知道如何融化/重塑,但我不确定如何扩展这些年。 感谢。

5 个答案:

答案 0 :(得分:4)

这是一个基础R方法。

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

现在,使用此列表计算要为每个ID重复的行数(rep的第二个参数),然后将其作为向量附加(使用unlist从列表转换)使用{ {1}}。

cbind

答案 1 :(得分:4)

您可以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

答案 2 :(得分:3)

这是一个整齐的解决方案

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

答案 3 :(得分:3)

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

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

返回

    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有noted他感到惊讶的是,data.table以上的解决方案比lmo's base R solution慢五倍,显然OP的生产数据集超过1M行。

此外,这个问题吸引了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行。

此外,只包含一个额外的列gender,尽管the OP has told生产数据可能包含2到10个不变的列。

代码

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 函数的引用是显式的,以避免由于名称空间混乱而导致名称冲突。

首次运行

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)

返回TRUE,但返回

all.equal(res_aosmith, res_uwe1)除外
  

[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对执行速度的观察结果相矛盾,需要进一步研究。

答案 4 :(得分:1)

do中使用dplyr的另一种方法,但它比基本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