¿Cuál es el algoritmo detrás de la función `split` del núcleo R?

split es una función especialmente importante en R core. Muchas soluciones de Stack Overflow que ofrecen soluciones de base R basadas en la manipulación de datos se basan en él. Es la rutina de caballo de batalla de cualquier grupo por operaciones.

También hay muchas preguntas cuya solución es solo una línea con split . Mucha gente no sabe eso

  • split.data.frame puede dividir una matriz por fila;
  • split.default puede dividir un dataframe por columna.

Quizás la documentación de R sobre split no está funcionando muy bien. Menciona el primer uso, pero no menciona el segundo.

Hay cuatro métodos para split en R core:

 methods(split) #[1] split.data.frame split.Date split.default split.POSIXct 

Proporcionaré una respuesta explicando en profundidad cómo funcionan split.data.frame , split.default y C-level .Internal(split(x, f)) . Se aceptan otras respuestas en el objeto “Fecha” y “POSIXct”.

¿Cómo funciona split.data.frame ?

 function (x, f, drop = FALSE, ...) lapply(split(x = seq_len(nrow(x)), f = f, drop = drop, ...), function(ind) x[ind, , drop = FALSE]) 

Llama a split.default para dividir el vector de índice de fila seq_len(nrow(x)) , luego usa un ciclo de aplicación para extraer las filas asociadas en una entrada de lista.

Esto no es estrictamente un método de “data.frame”. Divide cualquier objeto bidimensional por la 1ª dimensión, incluida la división de una matriz por filas .


¿Cómo funciona split.default ?

 function (x, f, drop = FALSE, sep = ".", lex.order = FALSE, ...) { if (!missing(...)) .NotYetUsed(deparse(...), error = FALSE) if (is.list(f)) f < - interaction(f, drop = drop, sep = sep, lex.order = lex.order) else if (!is.factor(f)) f <- as.factor(f) else if (drop) f <- factor(f) storage.mode(f) <- "integer" if (is.null(attr(x, "class"))) return(.Internal(split(x, f))) lf <- levels(f) y <- vector("list", length(lf)) names(y) <- lf ind <- .Internal(split(seq_along(x), f)) for (k in lf) y[[k]] <- x[ind[[k]]] y } 
  1. si x no tiene clases (es decir, mayormente un vector atómico), se .Internal(split(x, f)) ;
  2. de lo contrario, utiliza .Internal(split()) para dividir el índice a lo largo de x , luego usa un ciclo for para extraer los elementos asociados en una entrada de lista.

Un vector atómico (ver ?vector ) es un vector con el siguiente modo:

  • "lógico", "entero", "numérico", "complejo", "personaje" y "crudo"
  • "lista"
  • "expresión"

Un objeto con clase ... Er ... ¡hay tantos! Permítanme dar tres ejemplos:

  • "factor"
  • "dataframe"
  • "matriz"

En mi opinión, split.default no está bien escrito. Hay tantos objetos con clases, pero split.default trataría de la misma manera a través de "[" . Esto funciona bien con "factor" y "data.frame" (¡así que dividiremos el dataframe a lo largo de las columnas!), Pero definitivamente no funciona con una matriz de la manera que esperamos.

 A < - matrix(1:9, 3) # [,1] [,2] [,3] #[1,] 1 4 7 #[2,] 2 5 8 #[3,] 3 6 9 split.default(A, c(1, 1, 2)) ## it does not split the matrix by columns! #$`1` #[1] 1 2 4 5 7 8 # #$`2` #[1] 3 6 9 

En realidad, la regla de reciclaje se ha aplicado a c(1, 1, 2) , y estamos haciendo lo mismo:

 split(c(A), rep_len(c(1,1,2), length(A))) 

¿Por qué el núcleo R no escribe otra línea para una "matriz", como

 for (k in lf) y[[k]] < - x[, ind[[k]], drop = FALSE] 

Hasta ahora, la única forma de dividir de forma segura una matriz por columnas es transponerla, luego split.data.frame , luego otra transponer.

 lapply(split.data.frame(t(A), c(1, 1, 2)), t) 

Otra solución mediante lapply(split.default(data.frame(A), c(1, 1, 2)), as.matrix) es un lapply(split.default(data.frame(A), c(1, 1, 2)), as.matrix) si A es una matriz de caracteres.


¿Cómo funciona .Internal(split(x, f)) trabajo?

¡Este es realmente el núcleo del núcleo! Tomaré un pequeño ejemplo a continuación para una explicación:

 set.seed(0) f < - sample(factor(letters[1:3]), 10, TRUE) # [1] cabbcaccbb #Levels: abc x <- 0:9 

Básicamente hay 3 pasos. Para mejorar la legibilidad, se proporciona el código R equivalente para cada paso.

paso 1: tabulación (contar la ocurrencia de cada nivel de factor)

 ## a factor has integer mode so `tabulate` works tab < - tabulate(f, nbins = nlevels(f)) [1] 2 4 4 

paso 2: asignación de almacenamiento de la lista resultante

 result < - vector("list", nlevels(f)) for (i in 1:length(tab)) result[[i]] <- vector(mode(x), tab[i]) names(result) <- levels(f) 

Me gustaría anotar esta lista de la siguiente manera, donde cada línea es un elemento de lista que es un vector en este ejemplo, y cada [ ] es un marcador de posición para una entrada de ese vector.

 $a: [ ] [ ] $b: [ ] [ ] [ ] [ ] $c: [ ] [ ] [ ] [ ] 

paso 3: asignación de elementos

Ahora es útil descubrir el modo entero interno para un factor:

 .f < - as.integer(f) #[1] 3 1 2 2 3 1 3 3 2 2 

Necesitamos escanear x y .f , llenando x[i] en la entrada derecha del result[[.f[i]]] , informado por un vector acumulador de búfer.

 ab < - integer(nlevels(f)) ## accumulator buffer for (i in 1:length(.f)) { fi <- .f[i] counter <- ab[fi] + 1L result[[fi]][counter] <- x[i] ab[fi] <- counter } 

En la siguiente ilustración, ^ es un puntero a los elementos a los que se accede o actualiza.

 ## i = 1 x: [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] .f: [3] [1] [2] [2] [3] [1] [3] [3] [2] [2] ^ ab: [0] [0] [0] ## on entry ^ $a: [ ] [ ] $b: [ ] [ ] [ ] [ ] $c: [0] [ ] [ ] [ ] ^ ab: [0] [0] [1] ## on exit ^ 

 ## i = 2 x: [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] .f: [3] [1] [2] [2] [3] [1] [3] [3] [2] [2] ^ ab: [0] [0] [1] ## on entry ^ $a: [1] [ ] ^ $b: [ ] [ ] [ ] [ ] $c: [0] [ ] [ ] [ ] ab: [1] [0] [1] ## on exit ^ 

 ## i = 3 x: [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] .f: [3] [1] [2] [2] [3] [1] [3] [3] [2] [2] ^ ab: [1] [0] [1] ## on entry ^ $a: [1] [ ] $b: [2] [ ] [ ] [ ] ^ $c: [0] [ ] [ ] [ ] ab: [1] [1] [1] ## on exit ^ 

 ## i = 4 x: [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] .f: [3] [1] [2] [2] [3] [1] [3] [3] [2] [2] ^ ab: [1] [1] [1] ## on entry ^ $a: [1] [ ] $b: [2] [3] [ ] [ ] ^ $c: [0] [ ] [ ] [ ] ab: [1] [2] [1] ## on exit ^ 

 ## i = 5 x: [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] .f: [3] [1] [2] [2] [3] [1] [3] [3] [2] [2] ^ ab: [1] [2] [1] ## on entry ^ $a: [1] [ ] $b: [2] [3] [ ] [ ] $c: [0] [4] [ ] [ ] ^ ab: [1] [2] [2] ## on exit ^ 

 ## i = 6 x: [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] .f: [3] [1] [2] [2] [3] [1] [3] [3] [2] [2] ^ ab: [1] [2] [2] ## on entry ^ $a: [1] [5] ^ $b: [2] [3] [ ] [ ] $c: [0] [4] [ ] [ ] ab: [2] [2] [2] ## on exit ^ 

 ## i = 7 x: [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] .f: [3] [1] [2] [2] [3] [1] [3] [3] [2] [2] ^ ab: [2] [2] [2] ## on entry ^ $a: [1] [5] $b: [2] [3] [ ] [ ] $c: [0] [4] [6] [ ] ^ ab: [2] [2] [3] ## on exit ^ 

 ## i = 8 x: [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] .f: [3] [1] [2] [2] [3] [1] [3] [3] [2] [2] ^ ab: [2] [2] [3] ## on entry ^ $a: [1] [5] $b: [2] [3] [ ] [ ] $c: [0] [4] [6] [7] ^ ab: [2] [2] [4] ## on exit ^ 

 ## i = 9 x: [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] .f: [3] [1] [2] [2] [3] [1] [3] [3] [2] [2] ^ ab: [2] [2] [4] ## on entry ^ $a: [1] [5] $b: [2] [3] [8] [ ] ^ $c: [0] [4] [6] [7] ab: [2] [3] [4] ## on exit ^ 

 ## i = 10 x: [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] .f: [3] [1] [2] [2] [3] [1] [3] [3] [2] [2] ^ ab: [2] [3] [4] ## on entry ^ $a: [1] [5] $b: [2] [3] [8] [9] ^ $c: [0] [4] [6] [7] ab: [2] [4] [4] ## on exit ^