web-dev-qa-db-fra.com

Comment faire vlookup et remplir (comme dans Excel) dans R?

J'ai un ensemble de données d'environ 105 000 lignes et 30 colonnes. J'ai une variable catégorique que j'aimerais attribuer à un nombre. Dans Excel, je ferais probablement quelque chose avec VLOOKUP et remplirait.

Comment ferais-je pour faire la même chose dans R?

Essentiellement, ce que j’ai est une variable HouseType, et j’ai besoin de calculer le HouseTypeNo. Voici quelques exemples de données:

HouseType HouseTypeNo
Semi            1
Single          2
Row             3
Single          2
Apartment       4
Apartment       4
Row             3
74
user2142810

Si je comprends bien votre question, voici quatre méthodes pour faire l’équivalent de VLOOKUP d’Excel et remplir avec 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

Voici quatre méthodes pour remplir la HouseTypeNo dans la largetable en utilisant les valeurs de la table lookup:

D'abord avec merge dans la base:

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

Une seconde méthode avec des vecteurs nommés 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]))

Troisièmement, en utilisant le paquetage plyr:

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

Quatrièmement, en utilisant le paquetage sqldf

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

S'il est possible que certains types de maisons dans largetable n'existent pas dans lookup, une jointure gauche serait utilisée:

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

Des modifications correspondantes des autres solutions seraient également nécessaires.

Est-ce ce que tu voulais faire? Faites-moi savoir quelle méthode vous aimez et je vais ajouter un commentaire.

103
Ben

Je pense que vous pouvez aussi utiliser match():

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

Cela fonctionne toujours si je brouille l'ordre de lookup.

17
Ben Bolker

J'aime aussi utiliser qdapTools::lookup ou l'opérateur binaire abrégé %l%. Il fonctionne de manière identique à un vlookup Excel, mais accepte les arguments de nom opposés aux numéros de colonne.

## 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
10
maloneypatr

Solution # 2 de la réponse de @Ben n'est pas reproductible dans d'autres exemples plus génériques. Il se trouve que la recherche est correcte dans l'exemple car l'unique HouseType dans houses apparaît par ordre croissant. Essaye ça:

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 solution n ° 2 donne

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

qui quand

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

lorsque la bonne réponse est 17 dans la table de recherche

La bonne façon de le faire est

 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]))

Maintenant, les recherches sont effectuées correctement

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

J'ai essayé de modifier la réponse à Bens mais celle-ci est rejetée pour des raisons que je ne comprends pas.

6
ECII

L'affiche ne demandait pas de rechercher des valeurs si exact=FALSE, mais j'ajoute ceci comme réponse pour ma propre référence et éventuellement pour d'autres.

Si vous recherchez des valeurs catégoriques, utilisez les autres réponses.

Le vlookup d'Excel vous permet également de faire correspondre approximativement les valeurs numériques avec le 4ème argument (1) match=TRUE. Je pense à match=TRUE comme à la recherche de valeurs sur un thermomètre. La valeur par défaut est FALSE, ce qui est parfait pour les valeurs catégorielles.

Si vous souhaitez faire correspondre approximativement (effectuer une recherche), R a une fonction appelée findInterval, qui (comme son nom l’indique) trouvera l’intervalle/bin qui contient votre valeur numérique continue.

Cependant, supposons que vous souhaitiez findInterval pour plusieurs valeurs. Vous pouvez écrire une boucle ou utiliser une fonction apply. Cependant, j'ai trouvé plus efficace d'adopter une approche vectorielle bricolée.

Disons que vous avez une grille de valeurs indexées par x et y:

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)))

et vous avez des valeurs que vous voulez rechercher par x et 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")

Voici l'exemple visualisé:

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

Contour Plot

Vous pouvez trouver les intervalles x et les intervalles y avec ce type de formule:

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

Vous pourriez aller un peu plus loin et effectuer une interpolation (simpliste) sur les valeurs z dans grid comme ceci:

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)

Ce qui vous donne ces valeurs:

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)

Contour plot with values

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

Notez que ix, et iy auraient également pu être trouvés avec une boucle utilisant findInterval, par ex. voici un exemple pour la deuxième rangée

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

Qui correspond à ix et iy dans df[2]

Footnote: (1) Le quatrième argument de vlookup s'appelait auparavant "match", mais après l'introduction du ruban, il a été renommé "[range_lookup]".

5
geneorama

Commençant par:

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

... vous pouvez utiliser

as.numeric(factor(houses$HouseType))

... pour donner un numéro unique pour chaque type de maison. Vous pouvez voir le résultat ici:

> 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

... donc vous vous retrouvez avec des nombres différents sur les lignes (car les facteurs sont classés par ordre alphabétique) mais le même motif.

(EDIT: le texte restant de cette réponse est en fait redondant. Je me suis alors mis à vérifier et il est apparu que read.table() avait déjà transformé les maisons $ HouseType en un facteur lorsqu’il a été lu dans le cadre de données en premier lieu) .

Cependant, il peut être préférable de convertir HouseType en facteur, ce qui vous donnerait les mêmes avantages que HouseTypeNo, mais serait plus facile à interpréter car les types de maison sont nommés plutôt que numérotés, par exemple:

> 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"  
5
Simon

Vous pouvez utiliser mapvalues() à partir du paquet plyr.

Données initiales:

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

Table de concordance:

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

Créer la nouvelle variable:

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

Ou, pour les remplacements simples, vous pouvez ignorer la création d'une longue table de recherche et le faire directement en une seule étape:

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

Résultat:

> 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
4
Sam Firke

L'utilisation de merge diffère de la recherche dans Excel car elle risque de dupliquer (multiplier) vos données si la contrainte de clé primaire n'est pas appliquée dans la table de recherche ou de réduire le nombre d'enregistrements si vous n'utilisez pas all.x = T.

Pour vous assurer de ne pas avoir de problème avec cela et de chercher en toute sécurité, je suggère deux stratégies.

La première consiste à vérifier un certain nombre de lignes dupliquées dans la clé de recherche:

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)
}

Cela vous obligera à dédoubler le jeu de données avant de l'utiliser:

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 deuxième option consiste à reproduire le comportement d'Excel en prenant la première valeur correspondante de l'ensemble de données de recherche:

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")

Ces fonctions diffèrent légèrement de lookup car elles ajoutent plusieurs colonnes.

3
Bulat