2016-07-28 5 views
2

Я ищу более короткое и более красивое решение (возможно, в tidyverse) к следующей проблеме. У меня есть data.frame «данные»:R - извлечь все строки, соответствующие шаблону, и создать реляционную таблицу

id   string 
1 A 1.001 xxx 123.123 
2 B 23,45 lorem ipsum 
3 C  donald trump 
4 D ssss 134, 1,45 

То, что я хотел сделать, это извлечь все номера (независимо от того, если разделитель или «» -> в этом случае я предполагаю, что строка «» «134, 1,45» может быть извлечена в два числа: 134 и 1.45) и создать data.frame «выход» ищет похожее на это:

id string 
1 A 1.001 
2 A 123.123 
3 B 23.45 
4 C <NA> 
5 D  134 
6 D 1.45 

мне удалось это сделать (код ниже), но решение довольно уродливое для меня также не так эффективно (два for-loops). Может кто-нибудь предложить лучший способ сделать это сделать (предпочтительно с использованием dplyr)

# data 
data <- data.frame(id = c("A", "B", "C", "D"), 
        string = c("1.001 xxx 123.123", 
          "23,45 lorem ipsum", 
          "donald trump", 
          "ssss 134, 1,45"), 
        stringsAsFactors = FALSE) 

# creating empty data.frame      
len <- length(unlist(sapply(data$string, function(x) gregexpr("[0-9]+[,|.]?[0-9]*", x)))) 
output <- data.frame(id = rep(NA, len), string = rep(NA, len)) 

# main solution 
start = 0 

for(i in 1:dim(data)[1]){ 
    tmp_len <- length(unlist(gregexpr("[0-9]+[,|.]?[0-9]*", data$string[i]))) 
    for(j in (start+1):(start+tmp_len)){ 
    output[j,1] <- data$id[i] 
    output[j,2] <- regmatches(data$string[i], gregexpr("[0-9]+[,|.]?[0-9]*", data$string[i]))[[1]][j-start] 
    } 
    start = start + tmp_len 
} 

# further modifications 
output$string <- gsub(",", ".", output$string) 
output$string <- as.numeric(ifelse(substring(output$string, nchar(output$string), nchar(output$string)) == ".", 
            substring(output$string, 1, nchar(output$string) - 1), 
            output$string)) 

output 

ответ

5

1) Основание R Это использует относительно простые регулярные выражения и никаких пакетов.

В первых двух строках кода замените любую запятую, а затем пробел с пространством , а затем замените все остальные запятые точкой. После этих двух линий s будет: c("1.001 xxx 123.123", "23.45 lorem ipsum", "donald trump", "ssss 134 1.45")

В следующих 4-х строк кода обрезки пробелы из начала и конца каждого поля строки и разделить поле строки на пробелы получения списка. grep из этих элементов состоят только из цифр и точек. (Регулярное выражение ^[0-9.]*$ соответствует началу слова, за которым следуют ноль или несколько цифр или точек, за которыми следует конец слова, поэтому сопоставляются только слова, содержащие только эти символы.) Замените любые компоненты нулевой длины на NA. Наконец, добавьте data$id в качестве имен. После запуска этих 4 строк список L будет list(A = c("1.001", "123.123"), B = "23.45", C = NA, D = c("134", "1.45")).

В последней строке кода конвертируйте список L в фрейм данных с соответствующими именами.

s <- gsub(", ", " ", data$string) 
s <- gsub(",", ".", s) 

L <- strsplit(trimws(s), "\\s+") 
L <- lapply(L, grep, pattern = "^[0-9.]*$", value = TRUE) 
L <- ifelse(lengths(L), L, NA) 
names(L) <- data$id 

with(stack(L), data.frame(id = ind, string = values)) 

давая:

id string 
1 A 1.001 
2 A 123.123 
3 B 23.45 
4 C <NA> 
5 D  134 
6 D 1.45 

2) magrittr Это изменение (1) записывает его в качестве magrittr трубопровода.

library(magrittr) 

data %>% 
    transform(string = gsub(", ", " ", string)) %>% 
    transform(string = gsub(",", ".", string)) %>% 
    transform(string = trimws(string)) %>% 
    with(setNames(strsplit(string, "\\s+"), id)) %>% 
    lapply(grep, pattern = "^[0-9.]*$", value = TRUE) %>% 
    replace(lengths(.) == 0, NA) %>% 
    stack() %>% 
    with(data.frame(id = ind, string = values)) 

3) dplyr/tidyr Это альтернативное решение трубопроводов с использованием dplyr и tidyr. unnest преобразует в длинную форму, id сделан таким образом, что мы можем позже использовать complete для восстановления идентификаторов, которые удаляются последующей фильтрацией, фильтр удаляет ненужные строки, а complete вставляет строки NA для каждого id, которые иначе не появлялись.

library(dplyr) 
library(tidyr) 

data %>% 
    mutate(string = gsub(", ", " ", string)) %>% 
    mutate(string = gsub(",", ".", string)) %>% 
    mutate(string = trimws(string)) %>% 
    mutate(string = strsplit(string, "\\s+")) %>% 
    unnest() %>% 
    mutate(id = factor(id)) 
    filter(grepl("^[0-9.]*$", string)) %>% 
    complete(id) 

4) data.table

library(data.table) 

DT <- as.data.table(data) 
DT[, string := gsub(", ", " ", string)][, 
    string := gsub(",", ".", string)][, 
    string := trimws(string)][, 
    string := setNames(strsplit(string, "\\s+"), id)][, 
    list(string = list(grep("^[0-9.]*$", unlist(string), value = TRUE))), by = id][, 
    list(string = if (length(unlist(string))) unlist(string) else NA_character_), by = id] 
DT 

Update Удалены предположение, что нежелательные слова не имеют цифру или точку. Также добавлены (2), (3) и (4) и некоторые улучшения.

+0

Unnest - это функция, которую я искал. Фантастическая почта - спасибо! Один вопрос: в чем причина написания L <- strsplit (trimws (s), "\\ s +") вместо L <- strsplit (trimws (s), "\\ s")? – user2280549

+0

«\\ s» в порядке, если всегда есть один символ пробела - не был уверен. –

+0

Сделали некоторые улучшения для (2) и (3). –

2

Мы можем заменить , между номерами с . (с использованием gsub), извлечь номера с str_extract_all (от stringr в list) , замените элементы list, которые имеют номер , равный 0, с NA, укажите имена list с колонкой «id», stack, чтобы преобразовать list в data.frame и переименовать столбцы.

library(stringr) 
setNames(stack(setNames(lapply(str_extract_all(gsub("(?<=[0-9]),(?=[0-9])", ".", 
     data$string, perl = TRUE), "[0-9.]+"), function(x) 
    if(length(x)==0) NA else as.numeric(x)), data$id))[2:1], c("id", "string")) 
# id string 
#1 A 1.001 
#2 A 123.123 
#3 B 23.45 
#4 C  NA 
#5 D  134 
#6 D 1.45 
+0

Спасибо. Не могли бы вы быть такими добрыми и сложными? »(? <= [0-9]), (? = [0-9])». Я не знаком с Perl-подобной нотацией – user2280549

+0

@ user2280549 Это обратное, что означает, что ',' находится между двумя номерами – akrun

+0

Да, я это понял. Мне было интересно, что означает «<=» или «=» перед [0-9] в нотации Perl. – user2280549

1

Та же идея, что и у Габора. Я надеялся использовать встроенные в разборе строк (type.convert, используемых в read.table), а не написание пользовательских регулярных выражений замены АиР:

sp = setNames(strsplit(data$string, " "), data$id) 

spc = lapply(sp, function(x) { 
    x = x[grep("[^0-9.,]$", x, invert=TRUE)] 

    if (!length(x)) 
    NA_real_ 
    else 
    mapply(type.convert, x, dec=gsub("[^.,]", "", x), USE.NAMES=FALSE) 
}) 

setNames(rev(stack(spc)), names(data)) 

    id string 
1 A 1.001 
2 A 123.123 
3 B 23.45 
4 C <NA> 
5 D  134 
6 D 1.45 

К сожалению, type.convert не достаточно прочным, чтобы рассмотреть как десятичные разделители на один раз, поэтому мы нужно это mapply malarkey вместо type.convert(x, dec = "[.,]").

Смежные вопросы