¿Cómo hacer vlookup y completar (como en Excel) en R?

82

Tengo un conjunto de datos de aproximadamente 105000 filas y 30 columnas. Tengo una variable categórica a la que me gustaría asignarle un número. En Excel, probablemente haría algo con VLOOKUPy llenaría.

¿Cómo haría yo para hacer lo mismo en R?

Básicamente, lo que tengo es una HouseTypevariable y necesito calcular el HouseTypeNo. A continuación, se muestran algunos datos de muestra:

HouseType HouseTypeNo
Semi            1
Single          2
Row             3
Single          2
Apartment       4
Apartment       4
Row             3
usuario2142810
fuente

Respuestas:

116

Si entiendo su pregunta correctamente, aquí hay cuatro métodos para hacer el equivalente de Excel VLOOKUPy completar usando R:

# load sample data from Q
hous <- read.table(header = TRUE, 
                   stringsAsFactors = FALSE, 
text="HouseType HouseTypeNo
Semi            1
Single          2
Row             3
Single          2
Apartment       4
Apartment       4
Row             3")

# create a toy large table with a 'HouseType' column 
# but no 'HouseTypeNo' column (yet)
largetable <- data.frame(HouseType = as.character(sample(unique(hous$HouseType), 1000, replace = TRUE)), stringsAsFactors = FALSE)

# create a lookup table to get the numbers to fill
# the large table
lookup <- unique(hous)
  HouseType HouseTypeNo
1      Semi           1
2    Single           2
3       Row           3
5 Apartment           4

A continuación, se muestran cuatro métodos para completar HouseTypeNoel largetableuso de los valores de la lookuptabla:

Primero con mergeen base:

# 1. using base 
base1 <- (merge(lookup, largetable, by = 'HouseType'))

Un segundo método con vectores con nombre en base:

# 2. using base and a named vector
housenames <- as.numeric(1:length(unique(hous$HouseType)))
names(housenames) <- unique(hous$HouseType)

base2 <- data.frame(HouseType = largetable$HouseType,
                    HouseTypeNo = (housenames[largetable$HouseType]))

En tercer lugar, usando el plyrpaquete:

# 3. using the plyr package
library(plyr)
plyr1 <- join(largetable, lookup, by = "HouseType")

Cuarto, usando el sqldfpaquete

# 4. using the sqldf package
library(sqldf)
sqldf1 <- sqldf("SELECT largetable.HouseType, lookup.HouseTypeNo
FROM largetable
INNER JOIN lookup
ON largetable.HouseType = lookup.HouseType")

Si es posible que algunos tipos de casas largetableno existan en, lookupse usaría una combinación izquierda:

sqldf("select * from largetable left join lookup using (HouseType)")

También se necesitarían cambios correspondientes en las otras soluciones.

¿Es eso lo que querías hacer? Déjame saber qué método te gusta y agregaré comentarios.

Ben
fuente
1
Me di cuenta de que es bastante tarde, pero gracias por tu ayuda. Probé el primer y el segundo método. Ambos funcionaron bien. Nuevamente, gracias por responder la pregunta.
user2142810
1
De nada. Si respondió a su pregunta, puede indicarlo haciendo clic en la marca debajo de las flechas en la parte superior izquierda. Eso será útil para otras personas que tengan la misma pregunta.
Ben
2
Creo que la solución # 2 funciona solo porque en su ejemplo los valores únicos están en orden creciente (= el primer nombre único es 1, el segundo nombre único es 2 y así sucesivamente). Si agrega 'hous', digamos en la segunda fila 'HousType = ECII', HousTypeNo = '17 ', la búsqueda sale mal.
ECII
1
@ECII, continúe y agregue su respuesta que ilustre el problema y muestre su solución
Ben
1
Buena publicación. ¡Gracias por compartir! # 4 funcionó muy bien para mi aplicación ... uniéndose a dos tablas muy grandes de 400 MB.
Nathaniel Payne
25

Creo que también puedes usar match():

largetable$HouseTypeNo <- with(lookup,
                     HouseTypeNo[match(largetable$HouseType,
                                       HouseType)])

Esto todavía funciona si mezclo el orden de lookup.

Ben Bolker
fuente
10

También me gusta usar qdapTools::lookupun operador binario o abreviado %l%. Funciona de manera idéntica a un vlookup de Excel, pero acepta argumentos de nombre opuestos a números de columna

## Replicate Ben's data:
hous <- structure(list(HouseType = c("Semi", "Single", "Row", "Single", 
    "Apartment", "Apartment", "Row"), HouseTypeNo = c(1L, 2L, 3L, 
    2L, 4L, 4L, 3L)), .Names = c("HouseType", "HouseTypeNo"), 
    class = "data.frame", row.names = c(NA, -7L))


largetable <- data.frame(HouseType = as.character(sample(unique(hous$HouseType), 
    1000, replace = TRUE)), stringsAsFactors = FALSE)


## It's this simple:
library(qdapTools)
largetable[, 1] %l% hous
maloneypatr
fuente
6

La solución # 2 de la respuesta de @ Ben no es reproducible en otros ejemplos más genéricos. Da la búsqueda correcta en el ejemplo porque los únicos HouseTypeen housesaparecen en orden creciente. Prueba esto:

hous <- read.table(header = TRUE,   stringsAsFactors = FALSE,   text="HouseType HouseTypeNo
  Semi            1
  ECIIsHome       17
  Single          2
  Row             3
  Single          2
  Apartment       4
  Apartment       4
  Row             3")

largetable <- data.frame(HouseType = as.character(sample(unique(hous$HouseType), 1000, replace = TRUE)), stringsAsFactors = FALSE)
lookup <- unique(hous)

Bens solución # 2 da

housenames <- as.numeric(1:length(unique(hous$HouseType)))
names(housenames) <- unique(hous$HouseType)
base2 <- data.frame(HouseType = largetable$HouseType,
                    HouseTypeNo = (housenames[largetable$HouseType]))

que cuando

unique(base2$HouseTypeNo[ base2$HouseType=="ECIIsHome" ])
[1] 2

cuando la respuesta correcta es 17 de la tabla de búsqueda

La forma correcta de hacerlo es

 hous <- read.table(header = TRUE,   stringsAsFactors = FALSE,   text="HouseType HouseTypeNo
      Semi            1
      ECIIsHome       17
      Single          2
      Row             3
      Single          2
      Apartment       4
      Apartment       4
      Row             3")

largetable <- data.frame(HouseType = as.character(sample(unique(hous$HouseType), 1000, replace = TRUE)), stringsAsFactors = FALSE)

housenames <- tapply(hous$HouseTypeNo, hous$HouseType, unique)
base2 <- data.frame(HouseType = largetable$HouseType,
  HouseTypeNo = (housenames[largetable$HouseType]))

Ahora las búsquedas se realizan correctamente

unique(base2$HouseTypeNo[ base2$HouseType=="ECIIsHome" ])
ECIIsHome 
       17

Intenté editar la respuesta de Bens pero fue rechazada por razones que no puedo entender.

ECII
fuente
5

Empezando con:

houses <- read.table(text="Semi            1
Single          2
Row             3
Single          2
Apartment       4
Apartment       4
Row             3",col.names=c("HouseType","HouseTypeNo"))

... puedes usar

as.numeric(factor(houses$HouseType))

... para dar un número único para cada tipo de casa. Puedes ver el resultado aquí:

> houses2 <- data.frame(houses,as.numeric(factor(houses$HouseType)))
> houses2
  HouseType HouseTypeNo as.numeric.factor.houses.HouseType..
1      Semi           1                                    3
2    Single           2                                    4
3       Row           3                                    2
4    Single           2                                    4
5 Apartment           4                                    1
6 Apartment           4                                    1
7       Row           3                                    2

... entonces terminas con diferentes números en las filas (porque los factores están ordenados alfabéticamente) pero con el mismo patrón.

(EDITAR: el texto restante en esta respuesta es en realidad redundante. Se me ocurrió verificar y resultó que read.table()ya había convertido las casas $ HouseType en un factor cuando se leyó en el marco de datos en primer lugar).

Sin embargo, es posible que sea mejor convertir HouseType en un factor, lo que le brindaría los mismos beneficios que HouseTypeNo, pero sería más fácil de interpretar porque los tipos de casas tienen nombres en lugar de numerados, por ejemplo:

> houses3 <- houses
> houses3$HouseType <- factor(houses3$HouseType)
> houses3
  HouseType HouseTypeNo
1      Semi           1
2    Single           2
3       Row           3
4    Single           2
5 Apartment           4
6 Apartment           4
7       Row           3
> levels(houses3$HouseType)
[1] "Apartment" "Row"       "Semi"      "Single"  
Simón
fuente
5

El póster no preguntó acerca de buscar valores si exact=FALSE, pero agrego esto como una respuesta para mi propia referencia y posiblemente para otras.

Si está buscando valores categóricos, use las otras respuestas.

Excel vlookuptambién le permite hacer coincidir aproximadamente valores numéricos con el cuarto argumento (1) match=TRUE. Pienso en match=TRUEcomo buscar valores en un termómetro. El valor predeterminado es FALSO, que es perfecto para valores categóricos.

Si desea hacer coincidir aproximadamente (realizar una búsqueda), R tiene una función llamada findInterval, que (como su nombre lo indica) encontrará el intervalo / bin que contiene su valor numérico continuo.

Sin embargo, digamos que lo desea findIntervalpara varios valores. Puede escribir un bucle o utilizar una función de aplicación. Sin embargo, me ha resultado más eficiente adoptar un enfoque vectorizado de bricolaje.

Digamos que tiene una cuadrícula de valores indexados por xey:

grid <- list(x = c(-87.727, -87.723, -87.719, -87.715, -87.711), 
             y = c(41.836, 41.839, 41.843, 41.847, 41.851), 
             z = (matrix(data = c(-3.428, -3.722, -3.061, -2.554, -2.362, 
                                  -3.034, -3.925, -3.639, -3.357, -3.283, 
                                  -0.152, -1.688, -2.765, -3.084, -2.742, 
                                   1.973,  1.193, -0.354, -1.682, -1.803, 
                                   0.998,  2.863,  3.224,  1.541, -0.044), 
                         nrow = 5, ncol = 5)))

y tiene algunos valores que desea buscar por x e y:

df <- data.frame(x = c(-87.723, -87.712, -87.726, -87.719, -87.722, -87.722), 
                 y = c(41.84, 41.842, 41.844, 41.849, 41.838, 41.842), 
                 id = c("a", "b", "c", "d", "e", "f")

Aquí está el ejemplo visualizado:

contour(grid)
points(df$x, df$y, pch=df$id, col="blue", cex=1.2)

Dibujo de contorno

Puede encontrar los intervalos xy los intervalos y con este tipo de fórmula:

xrng <- range(grid$x)
xbins <- length(grid$x) -1
yrng <- range(grid$y)
ybins <- length(grid$y) -1
df$ix <- trunc( (df$x - min(xrng)) / diff(xrng) * (xbins)) + 1
df$iy <- trunc( (df$y - min(yrng)) / diff(yrng) * (ybins)) + 1

Podría dar un paso más y realizar una interpolación (simplista) en los valores z de gridesta manera:

df$z <- with(df, (grid$z[cbind(ix, iy)] + 
                      grid$z[cbind(ix + 1, iy)] +
                      grid$z[cbind(ix, iy + 1)] + 
                      grid$z[cbind(ix + 1, iy + 1)]) / 4)

Lo que te da estos valores:

contour(grid, xlim = range(c(grid$x, df$x)), ylim = range(c(grid$y, df$y)))
points(df$x, df$y, pch=df$id, col="blue", cex=1.2)
text(df$x + .001, df$y, lab=round(df$z, 2), col="blue", cex=1)

Gráfico de contorno con valores

df
#         x      y id ix iy        z
# 1 -87.723 41.840  a  2  2 -3.00425
# 2 -87.712 41.842  b  4  2 -3.11650
# 3 -87.726 41.844  c  1  3  0.33150
# 4 -87.719 41.849  d  3  4  0.68225
# 6 -87.722 41.838  e  2  1 -3.58675
# 7 -87.722 41.842  f  2  2 -3.00425

Tenga en cuenta que ix e iy también podrían haberse encontrado con un bucle usando findInterval, por ejemplo, aquí hay un ejemplo para la segunda fila

findInterval(df$x[2], grid$x)
# 4
findInterval(df$y[2], grid$y)
# 2

Que coincide ixy iyendf[2]

Nota al pie: (1) El cuarto argumento de vlookup se llamaba anteriormente "coincidencia", pero después de que introdujeron la cinta se le cambió el nombre a "[range_lookup]".

geneorama
fuente
4

Puede utilizar mapvalues()el paquete plyr.

Datos iniciales:

dat <- data.frame(HouseType = c("Semi", "Single", "Row", "Single", "Apartment", "Apartment", "Row"))

> dat
  HouseType
1      Semi
2    Single
3       Row
4    Single
5 Apartment
6 Apartment
7       Row

Tabla de búsqueda / paso de peatones:

lookup <- data.frame(type_text = c("Semi", "Single", "Row", "Apartment"), type_num = c(1, 2, 3, 4))
> lookup
  type_text type_num
1      Semi        1
2    Single        2
3       Row        3
4 Apartment        4

Crea la nueva variable:

dat$house_type_num <- plyr::mapvalues(dat$HouseType, from = lookup$type_text, to = lookup$type_num)

O para reemplazos simples, puede omitir la creación de una tabla de búsqueda larga y hacer esto directamente en un solo paso:

dat$house_type_num <- plyr::mapvalues(dat$HouseType,
                                      from = c("Semi", "Single", "Row", "Apartment"),
                                      to = c(1, 2, 3, 4))

Resultado:

> dat
  HouseType house_type_num
1      Semi              1
2    Single              2
3       Row              3
4    Single              2
5 Apartment              4
6 Apartment              4
7       Row              3
Sam Firke
fuente
3

El uso mergees diferente de la búsqueda en Excel, ya que tiene el potencial de duplicar (multiplicar) sus datos si la restricción de clave principal no se aplica en la tabla de búsqueda o reducir la cantidad de registros si no los está usando all.x = T.

Para asegurarse de no meterse en problemas con eso y realizar la búsqueda de forma segura, sugiero dos estrategias.

El primero es hacer una verificación en una serie de filas duplicadas en la clave de búsqueda:

safeLookup <- function(data, lookup, by, select = setdiff(colnames(lookup), by)) {
  # Merges data to lookup making sure that the number of rows does not change.
  stopifnot(sum(duplicated(lookup[, by])) == 0)
  res <- merge(data, lookup[, c(by, select)], by = by, all.x = T)
  return (res)
}

Esto lo obligará a eliminar el conjunto de datos de búsqueda antes de usarlo:

baseSafe <- safeLookup(largetable, house.ids, by = "HouseType")
# Error: sum(duplicated(lookup[, by])) == 0 is not TRUE 

baseSafe<- safeLookup(largetable, unique(house.ids), by = "HouseType")
head(baseSafe)
# HouseType HouseTypeNo
# 1 Apartment           4
# 2 Apartment           4
# ...

La segunda opción es reproducir el comportamiento de Excel tomando el primer valor coincidente del conjunto de datos de búsqueda:

firstLookup <- function(data, lookup, by, select = setdiff(colnames(lookup), by)) {
  # Merges data to lookup using first row per unique combination in by.
  unique.lookup <- lookup[!duplicated(lookup[, by]), ]
  res <- merge(data, unique.lookup[, c(by, select)], by = by, all.x = T)
  return (res)
}

baseFirst <- firstLookup(largetable, house.ids, by = "HouseType")

Estas funciones son ligeramente diferentes de las lookupque agregan varias columnas.

Bulat
fuente