/ / Usuwanie podobnych, ale dłuższych duplikatów z wektora - r, regex, vector, set

Usuwanie podobnych, ale dłuższych duplikatów z wektora - r, regex, vector, set

Do czyszczenia bazy danych mam wektor, powiedzmy, potraw i chcę usunąć wszystkie warianty naczynia „podstawowego”, pozostawiając tylko naczynie podstawowe. Na przykład, jeśli mam ...

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

... Chcę usunąć wszystkie wpisy, które mają już krótszą pasującą wersję w wektorze. Wynikowy wektor zawierałby zatem tylko: „DAL BHAT”, „HAMBURGER,„ PIZZA ”.

Korzystanie z zagnieżdżonego for pętla i sprawdzanie wszystkiego ze wszystkimi innymi będzie działać w tym przykładzie, ale zajmie dużo czasu dla dużego zbioru danych pod ręką, a ponadto jest brzydkim kodowaniem, powiedziałbym.

Można założyć, że wszystkie wpisy są zapisane wielkimi literami i że wektor jest już posortowany. Nie można zakładać, że pierwsza pozycja następnej potrawy bazowej jest zawsze krótsza niż poprzednia pozycja.

Jakieś sugestie, jak rozwiązać ten problem w skuteczny sposób?

BONUSOWE PYTANIE: Idealnie, chcę usunąć tylko elementy zwektor początkowy, jeśli są o co najmniej 3 znaki dłuższe niż ich krótszy odpowiednik. W powyższym przypadku oznaczałoby to, że „HAMBURGER2” również zostałby zachowany w powstałym wektorze.

Odpowiedzi:

5 dla odpowiedzi № 1

Oto podejście, które bym przyjął.Stworzyłbym funkcję z niektórymi warunkami, które musiałbym wziąć pod uwagę, i użyłbym tego na wejściu. Dodałem komentarze, aby wyjaśnić, co się dzieje w funkcji.

Funkcja ma 4 argumenty:

  • invec: Wektor znaków wejściowych.
  • thresh: Ilu znaków możemy użyć do określenia „podstawowej” potrawy. Domyślnie = 5.
  • minlen: Twoje pytanie „BONUS”. Domyślnie = 3.
  • strict: Logiczne. Jeśli są naczynia podstawowe z nchar krótszy niż twój thresh, czy chcesz obniżyć próg, czy też dokładnie określić, czego szukasz w podstawie? Default = FALSE. Zobacz ostatni przykład, aby dowiedzieć się, jak to zrobić strict powinno działać.

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

Twoje przykładowe dane:

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

Oto wynik:

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

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

Oto więcej przykładowych danych. Zwróć uwagę, że w „naczynia2” dane nie są już sortowane i jest „nowy element” „DAL”, a w „potrawach3” są także potrawy pisane małymi literami.

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

Oto funkcja na tych wektorach:

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 dla odpowiedzi nr 2

OP złożył wniosek aby usunąć wszystkie wpisy, które mają już krótszą pasującą wersję w wektorze. Ponadto OP chce usunąć elementy z początkowego wektora, jeśli są one o co najmniej 3 znaki dłuższe niż ich krótszy odpowiednik.

The brutalna siła metoda spróbuje porównać wszystkie wpisy ze sobą, aby znaleźć, czy jeden ciąg jest częścią drugiego ciągu. To wymagałoby n x (n-1) porównania.

Poniższe podejście próbuje zmniejszyć liczbę porównań ciągów, sprawdzając wcześniej liczbę znaków. Spowoduje to co najmniej połowę liczby połączeń do 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"

Edytować

Z powodu non-equi join to podejście działa również bez zmiana kolejności DT uprzednio:

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"

Ma to tę zaletę, że pierwotna kolejność dishes jest utrzymywany (z usuniętymi „duplikatami”).

Dane

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

Zwróć uwagę, że dodano dwa elementy, aby uwzględnić dodatkowe przypadki testowe.


2 dla odpowiedzi nr 3

Możliwe rozwiązanie przy użyciu sapply z grepl i colSums:

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

co daje:

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

Co to robi:

  • sapply(dishes, function(x) grepl(x, setdiff(dishes, x))) porównuje każdy element dishes z innymi elementami i wygląda z grepl czy określony element jest częścią innych elementów.
  • Zwraca to macierz logiczną, gdzie a TRUE wartość wskazuje, czy plik nazwa dania jest częścią innego nazwa dania:

         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
    
  • Biorąc sumę kolumny z colSums, otrzymujesz liczbowy wektor ilu innych naczynia nazwa każdego danie zawiera:

    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
    
  • Tylko krótszy nazwy dań mają liczbę większą niż zero. W konsekwencji porównanie wektora numerycznego z zerem zwraca logiczny wektor, którego naczynia do zachowania.

  • Jako alternatywa do używania > 0możesz również użyć podwójnego znaku negacji (!!) przed colSums. Spowoduje to również wybranie elementów, których liczba jest różna od zera: dishes[!!colSums(sapply(dishes, function(x) grepl(x, setdiff(dishes, x))))].

Jeśli chcesz wziąć pod uwagę maksymalną różnicę w długości znaków, możesz użyć agrepl zamiast grepl gdzie można określić maksymalną różnicę edycji w znakach za pomocą max.distance-parametr:

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

co daje:

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

1 dla odpowiedzi nr 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"