J'ai un data.frame que je voudrais convertir en une liste par lignes, ce qui signifie que chaque ligne correspond à ses propres éléments de liste. En d'autres termes, je voudrais une liste aussi longue que le data.frame a des lignes.
Jusqu'ici, j'ai abordé ce problème de la manière suivante, mais je me demandais s'il y avait une meilleure façon de l'aborder.
xy.df <- data.frame(x = runif(10), y = runif(10))
# pre-allocate a list and fill it with a loop
xy.list <- vector("list", nrow(xy.df))
for (i in 1:nrow(xy.df)) {
xy.list[[i]] <- xy.df[i,]
}
Comme ça:
xy.list <- split(xy.df, seq(nrow(xy.df)))
Et si vous voulez que les noms de domaine de xy.df
soient les noms de la liste de sortie, vous pouvez faire:
xy.list <- setNames(split(xy.df, seq(nrow(xy.df))), rownames(xy.df))
Eureka!
xy.list <- as.list(as.data.frame(t(xy.df)))
Si vous voulez abuser complètement du nom data.frame (comme je le fais) et que vous souhaitez conserver la fonctionnalité $, vous pouvez par exemple diviser data.frame en une seule ligne data.frames regroupés dans une liste:
> df = data.frame(x=c('a','b','c'), y=3:1)
> df
x y
1 a 3
2 b 2
3 c 1
# 'convert' into a list of data.frames
ldf = lapply(as.list(1:dim(df)[1]), function(x) df[x[1],])
> ldf
[[1]]
x y
1 a 3
[[2]]
x y
2 b 2
[[3]]
x y
3 c 1
# and the 'coolest'
> ldf[[2]]$y
[1] 2
Ce n'est pas seulement de la masturbation intellectuelle, il permet également de "transformer" le data.frame en une liste de ses lignes, en conservant l'indexation $ qui peut être utile pour une utilisation ultérieure avec lapply (en supposant que la fonction que vous passez à lapply utilise cette $ indexation)
Je travaillais sur cela aujourd'hui pour un data.frame (vraiment un data.table) avec des millions d'observations et 35 colonnes. Mon objectif était de renvoyer une liste de data.frames (data.tables), chacune avec une seule ligne. C'est-à-dire que je souhaitais scinder chaque ligne dans un data.frame distinct et les stocker dans une liste.
Voici deux méthodes que j'ai trouvées et qui étaient environ 3 fois plus rapides que split(dat, seq_len(nrow(dat)))
pour cet ensemble de données. Ci-dessous, je compare les trois méthodes sur un ensemble de données de 7500 lignes, 5 colonnes (iris répété 50 fois).
library(data.table)
library(microbenchmark)
microbenchmark(
split={dat1 <- split(dat, seq_len(nrow(dat)))},
setDF={dat2 <- lapply(seq_len(nrow(dat)),
function(i) setDF(lapply(dat, "[", i)))},
attrDT={dat3 <- lapply(seq_len(nrow(dat)),
function(i) {
tmp <- lapply(dat, "[", i)
attr(tmp, "class") <- c("data.table", "data.frame")
setDF(tmp)
})},
datList = {datL <- lapply(seq_len(nrow(dat)),
function(i) lapply(dat, "[", i))},
times=20
)
Cela retourne
Unit: milliseconds
expr min lq mean median uq max neval
split 861.8126 889.1849 973.5294 943.2288 1041.7206 1250.6150 20
setDF 459.0577 466.3432 511.2656 482.1943 500.6958 750.6635 20
attrDT 399.1999 409.6316 461.6454 422.5436 490.5620 717.6355 20
datList 192.1175 201.9896 241.4726 208.4535 246.4299 411.2097 20
Bien que les différences ne soient pas aussi grandes que dans mon test précédent, la méthode straight setDF
est nettement plus rapide à tous les niveaux de la distribution des exécutions avec max (setDF) <min (split) et la méthode attr
est généralement plus de deux fois plus rapide.
Une quatrième méthode est le champion extrême, qui consiste en une simple lapply
imbriquée, renvoyant une liste imbriquée. Cette méthode illustre le coût de construction d'un data.frame à partir d'une liste. De plus, toutes les méthodes que j'ai essayées avec la fonction data.frame
étaient à peu près d'un ordre de grandeur plus lent que les techniques data.table
.
Les données
dat <- vector("list", 50)
for(i in 1:50) dat[[i]] <- iris
dat <- setDF(rbindlist(dat))
Semble qu'une version actuelle du paquet purrr
(0.2.2) est la solution la plus rapide:
by_row(x, function(v) list(v)[[1L]], .collate = "list")$.out
Comparons les solutions les plus intéressantes:
data("Batting", package = "Lahman")
x <- Batting[1:10000, 1:10]
library(benchr)
library(purrr)
benchmark(
split = split(x, seq_len(.row_names_info(x, 2L))),
mapply = .mapply(function(...) structure(list(...), class = "data.frame", row.names = 1L), x, NULL),
purrr = by_row(x, function(v) list(v)[[1L]], .collate = "list")$.out
)
Résultats:
Benchmark summary:
Time units : milliseconds
expr n.eval min lw.qu median mean up.qu max total relative
split 100 983.0 1060.0 1130.0 1130.0 1180.0 1450 113000 34.3
mapply 100 826.0 894.0 963.0 972.0 1030.0 1320 97200 29.3
purrr 100 24.1 28.6 32.9 44.9 40.5 183 4490 1.0
Nous pouvons aussi obtenir le même résultat avec Rcpp
:
#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]
List df2list(const DataFrame& x) {
std::size_t nrows = x.rows();
std::size_t ncols = x.cols();
CharacterVector nms = x.names();
List res(no_init(nrows));
for (std::size_t i = 0; i < nrows; ++i) {
List tmp(no_init(ncols));
for (std::size_t j = 0; j < ncols; ++j) {
switch(TYPEOF(x[j])) {
case INTSXP: {
if (Rf_isFactor(x[j])) {
IntegerVector t = as<IntegerVector>(x[j]);
RObject t2 = wrap(t[i]);
t2.attr("class") = "factor";
t2.attr("levels") = t.attr("levels");
tmp[j] = t2;
} else {
tmp[j] = as<IntegerVector>(x[j])[i];
}
break;
}
case LGLSXP: {
tmp[j] = as<LogicalVector>(x[j])[i];
break;
}
case CPLXSXP: {
tmp[j] = as<ComplexVector>(x[j])[i];
break;
}
case REALSXP: {
tmp[j] = as<NumericVector>(x[j])[i];
break;
}
case STRSXP: {
tmp[j] = as<std::string>(as<CharacterVector>(x[j])[i]);
break;
}
default: stop("Unsupported type '%s'.", type2name(x));
}
}
tmp.attr("class") = "data.frame";
tmp.attr("row.names") = 1;
tmp.attr("names") = nms;
res[i] = tmp;
}
res.attr("names") = x.attr("row.names");
return res;
}
Maintenant, comparez avec purrr
:
benchmark(
purrr = by_row(x, function(v) list(v)[[1L]], .collate = "list")$.out,
rcpp = df2list(x)
)
Résultats:
Benchmark summary:
Time units : milliseconds
expr n.eval min lw.qu median mean up.qu max total relative
purrr 100 25.2 29.8 37.5 43.4 44.2 159.0 4340 1.1
rcpp 100 19.0 27.9 34.3 35.8 37.2 93.8 3580 1.0
Une solution plus moderne utilise uniquement purrr::transpose
:
library(purrr)
iris[1:2,] %>% purrr::transpose()
#> [[1]]
#> [[1]]$Sepal.Length
#> [1] 5.1
#>
#> [[1]]$Sepal.Width
#> [1] 3.5
#>
#> [[1]]$Petal.Length
#> [1] 1.4
#>
#> [[1]]$Petal.Width
#> [1] 0.2
#>
#> [[1]]$Species
#> [1] 1
#>
#>
#> [[2]]
#> [[2]]$Sepal.Length
#> [1] 4.9
#>
#> [[2]]$Sepal.Width
#> [1] 3
#>
#> [[2]]$Petal.Length
#> [1] 1.4
#>
#> [[2]]$Petal.Width
#> [1] 0.2
#>
#> [[2]]$Species
#> [1] 1
Une autre alternative en utilisant library(purrr)
(qui semble être un peu plus rapide sur les grandes données.)
flatten(by_row(xy.df, ..f = function(x) flatten_chr(x), .labels = FALSE))
Le meilleur moyen pour moi était:
Exemple de données:
Var1<-c("X1",X2","X3")
Var2<-c("X1",X2","X3")
Var3<-c("X1",X2","X3")
Data<-cbind(Var1,Var2,Var3)
ID Var1 Var2 Var3
1 X1 X2 X3
2 X4 X5 X6
3 X7 X8 X9
Nous appelons la bibliothèque BBmisc
library(BBmisc)
data$lists<-convertRowsToList(data[,2:4])
Et le résultat sera:
ID Var1 Var2 Var3 lists
1 X1 X2 X3 list("X1", "X2", X3")
2 X4 X5 X6 list("X4","X5", "X6")
3 X7 X8 X9 list("X7,"X8,"X9)
Une autre méthode consiste à convertir le fichier df en une matrice, puis à appliquer la liste apply lappy
function: ldf <- lapply(as.matrix(myDF), function(x)x)
.
La fonction by_row
du package purrrlyr
le fera pour vous.
Cet exemple montre
myfn <- function(row) {
#row is a tibble with one row, and the same number of columns as the original df
l <- as.list(row)
return(l)
}
list_of_lists <- purrrlyr::by_row(df, myfn, .labels=FALSE)$.out
Par défaut, la valeur renvoyée de myfn
est placée dans une nouvelle colonne list dans le fichier df appelé .out
. Le $.out
à la fin de l'instruction ci-dessus sélectionne immédiatement cette colonne et renvoie une liste de listes.
Comme @flodel a écrit: Ceci convertit votre dataframe en une liste qui a le même nombre d'éléments que le nombre de lignes dans dataframe:
NewList <- split(df, f = seq(nrow(df)))
Vous pouvez également ajouter une fonction à sélectionner uniquement les colonnes qui ne sont pas NA dans chaque élément de la liste:
NewList2 <- lapply(NewList, function(x) x[,!is.na(x)])