/ / Removendo duplicatas semelhantes, mas mais longas, de vetor - r, regex, vetor, conjunto

Removendo duplicatas semelhantes, mas mais longas, de vetor - r, regex, vetor, conjunto

Para a limpeza do banco de dados, eu tenho um vetor de pratos, digamos, e quero remover todas as variantes do prato "base", mantendo apenas o prato base. Por exemplo, se eu tiver ...

dishes <- c("DAL BHAT", "DAL BHAT-(SPICY)", "DAL BHAT WITH EXTRA RICE",
"HAMBURGER", "HAMBURGER-BIG", "HAMBURGER2", "PIZZA",
"PIZZA (PROSCIUTO)", "PIZZA_BOLOGNESE")

... Desejo remover todas as entradas que já possuem uma versão correspondente mais curta no vetor. O vetor resultante, portanto, incluiria apenas: "DAL BHAT", "HAMBURGER," PIZZA ".

Usando um aninhado for Fazer um loop e verificar tudo contra todos os outros funcionará para este exemplo, mas demorará muito para o grande conjunto de dados disponível e, além disso, será uma codificação feia que eu diria.

Pode-se supor que todas as entradas estão em maiúsculas e que o vetor já está classificado. Não se pode presumir que o primeiro item da próxima base seja sempre menor que a entrada anterior.

Alguma sugestão sobre como resolver isso de maneira eficiente?

PERGUNTA DE BÔNUS: Idealmente, eu só quero remover itens dovetor inicial se eles tiverem pelo menos 3 caracteres a mais que sua contraparte mais curta. No caso acima, isso significaria que "HAMBURGER2" também seria retido no vetor resultante.

Respostas:

5 para resposta № 1

Aqui está a abordagem que eu tomaria com isso. Eu criei uma função com algumas das condições que eu precisava considerar e usá-la na entrada. Eu adicionei comentários para explicar o que está acontecendo na função.

A função tem 4 argumentos:

  • invec: O vetor de caractere de entrada.
  • thresh: Quantos caracteres podemos usar para determinar o prato "base". Padrão = 5.
  • minlen: Sua pergunta "BONUS". Padrão = 3.
  • strict: Lógico. Se houver pratos de base com nchar mais curto que o seu thresh, você quer diminuir o thresh ou ser rigoroso sobre o que você está olhando para a base? FALSE. Veja o último exemplo de como strict pode funcionar.

myfun <- function(invec, thresh = 5, minlen = 3, strict = FALSE) {
# Bookkeeping -- sort, unique, all upper case
invec <- sort(unique(toupper(invec)))
# More bookkeeping -- min should not be longer
# than min base dish unless strict = TRUE
thresh <- if (isTRUE(strict)) thresh else min(min(nchar(invec)), thresh)
# Use `thresh` to get the `stubs``
stubs <- invec[!duplicated(substr(invec, 1, thresh))]
# loop through the stubs and do two things:
#   - Match the dish with the stub
#   - Return the base dish and any dishes within the minlen
unlist(
lapply(stubs, function(x) {
temp <- grep(x, invec, value = TRUE, fixed = TRUE)
temp[temp == x | nchar(temp) <= nchar(x) + minlen]
}),
use.names = FALSE)
}

Seus dados de amostra:

dishes <- c("DAL BHAT", "DAL BHAT-(SPICY)", "DAL BHAT WITH EXTRA RICE",
"HAMBURGER", "HAMBURGER-BIG", "HAMBURGER2", "PIZZA",
"PIZZA (PROSCIUTO)", "PIZZA_BOLOGNESE")

Aqui está o resultado:

myfun(dishes, minlen = 0)
# [1] "DAL BHAT"  "HAMBURGER" "PIZZA"

myfun(dishes)
# [1] "DAL BHAT"   "HAMBURGER"  "HAMBURGER2" "PIZZA"

Veja mais alguns dados de amostra. Note que em "dishes2" os dados não são mais classificados e há um novo item "DAL", e em "dishes3" você também tem pratos minúsculos.

dishes2 <- c("DAL BHAT", "DAL BHAT-(SPICY)", "DAL BHAT WITH EXTRA RICE",
"HAMBURGER", "HAMBURGER-BIG", "HAMBURGER2", "PIZZA",
"PIZZA (PROSCIUTO)", "PIZZA_BOLOGNESE", "DAL")

dishes3 <- c("DAL BHAT", "DAL BHAT-(SPICY)", "DAL BHAT WITH EXTRA RICE",
"HAMBURGER", "HAMBURGER-BIG", "HAMBURGER2", "PIZZA",
"PIZZA (PROSCIUTO)", "PIZZA_BOLOGNESE", "DAL", "pizza!!")

Aqui está a função desses vetores:

myfun(dishes2, 4)
# [1] "DAL"        "HAMBURGER"  "HAMBURGER2" "PIZZA"

myfun(dishes3)
# [1] "DAL"        "HAMBURGER"  "HAMBURGER2" "PIZZA"      "PIZZA!!"

myfun(dishes3, strict = TRUE)
# [1] "DAL"        "DAL BHAT"   "HAMBURGER"  "HAMBURGER2" "PIZZA"      "PIZZA!!"

2 para resposta № 2

O OP solicitou para remover todas as entradas que já possuem uma versão correspondente mais curta no vetor. Além disso, o OP deseja remover itens do vetor inicial se eles tiverem pelo menos 3 caracteres a mais que sua contraparte mais curta.

o força bruta O método tentaria comparar todas as entradas entre si para descobrir se uma string faz parte da outra string. Isso exigiria n x (n-1) comparações.

A abordagem abaixo tenta reduzir o número de comparações de strings, verificando o número de caracteres de antemão. Isso reduzirá pelo menos o número de chamadas para grepl().

library(data.table)
# prepare data
DT <- data.table(dish = dishes)[, len := nchar(dish)][order(len)]
DT
                        dish len
1:                      NAN   3
2:                    PIZZA   5
3:                 DAL BHAT   8
4:                HAMBURGER   9
5:               HAMBURGER2  10
6:            HAMBURGER-BIG  13
7:           SLICE OF PIZZA  14
8:          PIZZA_BOLOGNESE  15
9:         DAL BHAT-(SPICY)  16
10:        PIZZA (PROSCIUTO)  17
11: DAL BHAT WITH EXTRA RICE  24
# use non-equi join to find row numbers of "duplicate" entries
tmp <- DT[.(len + 3L, dish), on = .(len > V1), nomatch = 0L, allow = TRUE,
by = .EACHI, .I[grepl(V2, dish)]]
tmp
   len V1
1:   8  7
2:   8  8
3:   8 10
4:  11  9
5:  11 11
6:  12  6
# anti-join to remove "duplicates"
DT[!tmp$V1, dish]
[1] "NAN"        "PIZZA"      "DAL BHAT"   "HAMBURGER"  "HAMBURGER2"

Editar

Devido ao junção não equi esta abordagem funciona também sem reordenando DT antecipadamente:

delta_len <- 3L
DT <- data.table(dish = dishes)[, len := nchar(dish)]
DT[!DT[.(len + delta_len, dish), on = .(len > V1), nomatch = 0L, allow = TRUE,
by = .EACHI, .I[grepl(V2, dish)]]$V1, dish]
[1] "DAL BHAT"   "HAMBURGER"  "HAMBURGER2" "PIZZA"      "NAN"

Isso tem o benefício de que a ordem original de dishes é mantido (com as "duplicatas" removidas).

Dados

dishes <- c("DAL BHAT", "DAL BHAT-(SPICY)", "DAL BHAT WITH EXTRA RICE",
"HAMBURGER", "HAMBURGER-BIG", "HAMBURGER2", "PIZZA",
"PIZZA (PROSCIUTO)", "PIZZA_BOLOGNESE", "NAN", "SLICE OF PIZZA")

Observe que dois itens foram adicionados para cobrir casos de teste adicionais.


2 para resposta № 3

Uma possível solução usando sapply com grepl e colSums:

dishes[colSums(sapply(dishes, function(x) grepl(x, setdiff(dishes, x)))) > 0]

que dá:

[1] "DAL BHAT"  "HAMBURGER" "PIZZA"

O que isto faz:

  • sapply(dishes, function(x) grepl(x, setdiff(dishes, x))) compara cada elemento de dishes com os outros elementos e olha com grepl se o elemento específico é uma parte dos outros elementos.
  • Isso retorna uma matriz lógica, onde um TRUE valor indica se um nome do prato faz parte de outro nome do prato:

         DAL BHAT DAL BHAT-(SPICY) DAL BHAT WITH EXTRA RICE HAMBURGER HAMBURGER-BIG HAMBURGER2 PIZZA PIZZA (PROSCIUTO) PIZZA_BOLOGNESE
    [1,]     TRUE            FALSE                    FALSE     FALSE         FALSE      FALSE FALSE             FALSE           FALSE
    [2,]     TRUE            FALSE                    FALSE     FALSE         FALSE      FALSE FALSE             FALSE           FALSE
    [3,]    FALSE            FALSE                    FALSE     FALSE         FALSE      FALSE FALSE             FALSE           FALSE
    [4,]    FALSE            FALSE                    FALSE      TRUE         FALSE      FALSE FALSE             FALSE           FALSE
    [5,]    FALSE            FALSE                    FALSE      TRUE         FALSE      FALSE FALSE             FALSE           FALSE
    [6,]    FALSE            FALSE                    FALSE     FALSE         FALSE      FALSE FALSE             FALSE           FALSE
    [7,]    FALSE            FALSE                    FALSE     FALSE         FALSE      FALSE  TRUE             FALSE           FALSE
    [8,]    FALSE            FALSE                    FALSE     FALSE         FALSE      FALSE  TRUE             FALSE           FALSE
    
  • Tomando a soma da coluna com colSums, você obtém um vetor numérico de quantos outros pratos o nome de cada prato contém:

    DAL BHAT   DAL BHAT-(SPICY) DAL BHAT WITH EXTRA RICE  HAMBURGER  HAMBURGER-BIG    HAMBURGER2    PIZZA    PIZZA (PROSCIUTO)   PIZZA_BOLOGNESE
    2                  0                        0          2              0             0        2                    0                 0
    
  • Apenas o mais curto nomes de prato tem uma contagem maior que zero. Conseqüentemente, comparar o vetor numérico com zero retorna um vetor lógico do qual pratos manter.

  • Como alternativa ao uso > 0, você também pode usar um sinal de negação duplo (!!) em frente de colSums. Isso também seleciona os elementos que possuem uma contagem diferente de zero: dishes[!!colSums(sapply(dishes, function(x) grepl(x, setdiff(dishes, x))))].

Se você quiser levar em conta uma diferença máxima no tamanho do personagem, então você pode usar agrepl ao invés de grepl onde você pode especificar a diferença máxima de edição em caracteres com o max.distance-parâmetro:

dishes[colSums(sapply(dishes, function(x) agrepl(x, setdiff(dishes, x), max.distance = 3))) > 0]

que dá:

[1] "DAL BHAT"   "HAMBURGER"  "HAMBURGER2" "PIZZA"

1 para resposta № 4
unlist(sapply(split(dishes, substr(dishes, 1, 5)), function(x){
N = nchar(x)
x[(N - N[1]) < 3]
}))
#       DAL B       HAMBU1       HAMBU2        PIZZA
#  "DAL BHAT"  "HAMBURGER" "HAMBURGER2"      "PIZZA"