Как я могу разбить переменную на несколько столбцов в соответствии с несколькими условиями в R?

Я новичок в R, поэтому, пожалуйста, потерпите меня. Я просматриваю данные о заключении и имею переменную conviction, которая представляет собой беспорядочную строку, которая выглядит так:

[1] "Ct. 1: Conspiracy to distribute"                                                                         
[2] "Aggravated Assault"                                                                                      
[3] "Ct. 1: Possession of prohibited object; Ct. 2: criminal forfeiture"                                      
[4] "Ct. 1-6: Human Trafficking; Cts. 7, 8 Unlawful contact; Ct. 11: Involuntary Servitude; Ct. 36: Smuggling"

В идеале я хочу сделать две вещи. Во-первых, я хочу разобрать Ct. на несколько столбцов. Для первых трех строк данные будут выглядеть так:

     convictions                              conviction_1                      conviction_2                    
[1,] "Ct. 1: Conspiracy to distribute"        "Conspiracy to distribute"        NA                   
[2,] "Aggravated Assault"                     "Aggravated Assault"              NA                   
[3,] "Ct. 1: Possession of prohibited object" "Possession of prohibited object" "criminal forfeiture"

но все становится сложно, когда я добираюсь до третьей строки, потому что я хотел бы разобрать первую часть строки (Ct. 1-6: Human Trafficking) на 6 столбцов, а затем Ct. 7,8: Unlawful contact еще на 2 столбца.

Вторая часть заключается в том, что затем я хочу сгенерировать переменную convictions_total, которая будет находить наибольшее число в строке conviction, следующей за Ct:. для трех примеров записей, которые я включил сюда, convictions_total будет выглядеть так:

[1]  1  2 36

Это код, который я использовал для анализа гораздо более простой строковой переменной, но я не уверен, как настроить ее для этой переменной:

cols <- data.frame(str_split_fixed(data$convictions`,",",Inf))
colnames(cols) <- paste0("conviction_",rep(1:length(cols)))
data <- cbind(data,cols)

Заранее спасибо!


person seder163    schedule 04.02.2021    source источник


Ответы (2)


Следующее работает для вашего примера без использования большого количества регулярных выражений, в основном просто извлечение цифр или другое обнаружение строки:

library(stringr)
library(magrittr)
library(purrr)
library(plyr)

convictions_total <- sapply(stringr::str_extract_all(convictions, "\\d+"), 
                            function(x) max(as.numeric(x), 1))
convictions_split <- strsplit(convictions, ";")


reps <- lapply(convictions_split, FUN = function(x) {
    sapply(x, FUN = function(i) {
      num <- paste(stringr::str_extract_all(i, "[\\d+\\-,]")[[1]], collapse = "")
      # "-" indicates a range: take largest value
      if (stringr::str_detect(num, "-")){
        stringr::str_extract_all(num, "\\d+") %>% 
          unlist() %>% 
          as.numeric() %>%
          max() %>%  
          return()
      # "," indicates a sequence: get length of sequence
      } else if(stringr::str_detect(num, ",")){
        stringr::str_count(num, ",") + 1 %>% 
          as.numeric() %>%
          return()
      # otherwise return 1
      } else {
        return(1)
      }
    })
  })

convictions_str <- lapply(convictions_split, 
                          function(x) gsub(".*\\d:?\\s(.*)$", "\\1", x))

df <- purrr::map2(convictions_str, reps, rep) %>% 
  plyr::ldply(rbind) %>% 
  cbind(convictions_total, .) %>% 
  data.frame() %>% 
  dplyr::rename_with(~ gsub("X", "conviction_", .x), starts_with("X"))

Вывод

  convictions_total                    conviction_1        conviction_2      conviction_3
1                 1        Conspiracy to distribute                <NA>              <NA>
2                 1              Aggravated Assault                <NA>              <NA>
3                 2 Possession of prohibited object criminal forfeiture              <NA>
4                36               Human Trafficking   Human Trafficking Human Trafficking
       conviction_4      conviction_5      conviction_6     conviction_7     conviction_8
1              <NA>              <NA>              <NA>             <NA>             <NA>
2              <NA>              <NA>              <NA>             <NA>             <NA>
3              <NA>              <NA>              <NA>             <NA>             <NA>
4 Human Trafficking Human Trafficking Human Trafficking Unlawful contact Unlawful contact
           conviction_9 conviction_10
1                  <NA>          <NA>
2                  <NA>          <NA>
3                  <NA>          <NA>
4 Involuntary Servitude     Smuggling

Данные

convictions <- c("Ct. 1: Conspiracy to distribute",
                 "Aggravated Assault",
                 "Ct. 1: Possession of prohibited object; Ct.: 2 criminal forfeiture",
                 "Ct. 1-6: Human Trafficking; Cts. 7, 8 Unlawful contact; Ct. 11: Involuntary Servitude; Ct. 36: Smuggling")

Как это работает

  1. convictions_total легко извлечь, используя stringr::str_extract_all, чтобы взять все числа из каждой строки в convictions. Это возвращает список векторов. Затем sapply берет максимум из каждого вектора в списке и возвращает вектор.
  2. reps — это список, в котором элементы соответствуют элементам convictions, и в нем хранится числовой вектор, указывающий, сколько раз повторять каждый счет осуждения.

Код работает, сначала разбивая convictions на список векторов, где векторы содержат следующую извлеченную информацию: цифры (\\d+), тире (\\-) и запятые (,). Логика работает путем поиска этих извлечений строк:

  • Во-первых, если он находит "-" в подсчете судимостей, это указывает на диапазон, и снова принимается наибольшее значение. Например, "Ct. 1-6: Human Trafficking" вернет 6.
  • Далее, если он не находит "-", а вместо этого "," обозначает разделитель счета. Таким образом, он подсчитывает количество разделителей запятых и добавляет один. Например, "Cts. 7, 8 Unlawful contact" вернет 2.
  • Предполагается, что все остальное повторяется только один раз, поскольку это не последовательный список или диапазон.
reps
[[1]]
Ct. 1: Conspiracy to distribute 
                              1 

[[2]]
Aggravated Assault 
                 1 

[[3]]
Ct. 1: Possession of prohibited object             Ct.: 2 criminal forfeiture 
                                     1                                      1 

[[4]]
    Ct. 1-6: Human Trafficking     Cts. 7, 8 Unlawful contact  Ct. 11: Involuntary Servitude 
                             6                              2                              1 
             Ct. 36: Smuggling 
                             1 
  1. convictions_str просто извлекает фактическую информацию о судимости. Например, из "Ct. 1: Conspiracy to distribute" код извлечет "Conspiracy to distribute" и так далее для всех судимостей.
[[1]]
[1] "Conspiracy to distribute"

[[2]]
[1] "Aggravated Assault"

[[3]]
[1] "Possession of prohibited object" "criminal forfeiture"            

[[4]]
[1] "Human Trafficking"     "Unlawful contact"      "Involuntary Servitude"
[4] "Smuggling"                

На данный момент reps и convictions_str имеют родственную структуру:

  • convictions_str[[1]][1] нужно повторить reps[[1]][1] раз
  • convictions_str[[1]][2] нужно повторить reps[[1]][2] раз
  1. purrr::map2 использует преимущества этой структуры, используя функцию rep для повторения элементов в convictions_str значениями, хранящимися в reps, и выводит список. Строка plyr::ldply связывает этот список с заполнением NA, так как не у всех одинаковое количество судимостей. cbind добавляет столбец convictions_total, а dplyr::rename_with изменяет имена столбцов.
person LMc    schedule 05.02.2021
comment
Это круто! Спасибо за такое подробное объяснение. Я понимаю, что есть одна загвоздка: в данных есть несколько записей, в которых нет значения Ct.: (например, значение может быть просто Aggravated assault). В этих случаях счет должен быть равен 1. На данный момент, когда ваш код достигает этих значений, я получаю -Inf для значения convictions_total. - person seder163; 05.02.2021
comment
Пожалуйста! Пожалуйста, дополните свой пост примером этой ситуации, чтобы я мог соответствующим образом скорректировать. - person LMc; 05.02.2021
comment
отредактировано, чтобы включить третий тип записи. - person seder163; 05.02.2021
comment
Я обновил свой ответ, чтобы включить эту строку. Когда str_extract не находит цифру в коде, присваивающем вектору имя convictions_total, он возвращает character(0), который не имеет длины. as.numeric(character(0)) преобразует это в numeric(0), которое также не имеет длины. По умолчанию функция max с вектором нулевой длины возвращает -Inf (обратите внимание, min по умолчанию возвращает +Inf). Я просто изменил сравнение, чтобы воспользоваться преимуществами этого поведения по умолчанию, на max(..., 1), которое в этом конкретном случае будет разрешаться в max(-Inf, 1) и возвращать 1. - person LMc; 05.02.2021
comment
Вы получите неожиданное поведение, если у вас есть строка с несколькими ненумерованными счетчиками. Например: "Aggravated Assault; Human Trafficking" будет иметь значение 1 в convictions_total. Возможно, это ожидаемо, так как число для convictions_total не соответствует количеству столбцов осуждения для четвертой строки в вашем текущем примере. - person LMc; 05.02.2021

После двухдневного копания в кроличьей норе я придумал аккуратную версию кода @LMc, которая в итоге работала лучше, потому что вызов plyr испортил другой код, который я написал:

test_data <- 
  tibble(id = 1:5, 
         convictions = c("Ct. 1: Conspiracy to distribute"    ,                                                                     
                         "Aggravated Assault"              ,                                                                        
                         "Ct. 1: Possession of prohibited object; Ct. 2: criminal forfeiture"  ,                                    
                         "Ct. 1-6: Human Trafficking; Cts. 7, 8 Unlawful contact; Ct. 11: Involuntary Servitude; Ct. 36: Smuggling 50 grams",
                         "Ct. 1: Conspiracy; Cts. 2-7: Wire Fraud; Cts. 8-28:  Money Laundering"))
test_data <- test_data %>% 
  mutate(c2 = convictions) #this just duplicates the original variable convictions because I want to preserve it

test_data <- test_data %>%
  separate_rows(c2, sep = ";") %>%
  mutate(c2 = str_remove(c2, "Ct(s)?(\\. )(\\d|-|:|,|\\s)+")) %>%
  group_by(id) %>%
  mutate(conviction_number = paste0("c_", row_number())) %>%
  pivot_wider(values_from = c2, names_from = conviction_number) 


test_data <- test_data %>% 
  mutate(c2 = convictions) #again, just preserving the original variable

test_data <- test_data %>%
  separate_rows(c2, sep = ";") %>% 
  mutate(total_counts = as.numeric(ifelse(is.na(str_extract(c2, "((?<=\\-)\\d+)")), str_extract(c2, "\\d+"), str_extract(c2, "((?<=\\-)\\d+)")))) %>% 
  mutate(total_counts = ifelse(is.na(total_counts), 1, total_counts)) %>% 
  group_by(id) %>% 
  slice_max(total_counts) 

который создает следующий кадр данных:

     id convictions                                                  c_1                c_2           c_3            c_4          c2                 total_counts
  <int> <chr>                                                        <chr>              <chr>         <chr>          <chr>        <chr>                     <dbl>
1     1 Ct. 1: Conspiracy to distribute                              Conspiracy to dis~  NA            NA             NA          "Ct. 1: Conspirac~            1
2     2 Aggravated Assault                                           Aggravated Assault  NA            NA             NA          "Aggravated Assau~            1
3     3 Ct. 1: Possession of prohibited object; Ct. 2: criminal for~ Possession of pro~ " criminal f~  NA             NA          " Ct. 2: criminal~            2
4     4 Ct. 1-6: Human Trafficking; Cts. 7, 8 Unlawful contact; Ct.~ Human Trafficking  " Unlawful c~ " Involuntary~ " Smuggling~ " Ct. 36: Smuggli~           36
5     5 Ct. 1: Conspiracy; Cts. 2-7: Wire Fraud; Cts. 8-28:  Money ~ Conspiracy         " Wire Fraud" " Money Laund~  NA          " Cts. 8-28:  Mon~           28

Первый фрагмент кода разбивает счетчики на отдельные строки, а затем возвращается к c_ столбцам. Второй фрагмент кода выполняет такой же синтаксический анализ, но затем просматривает каждую запись, чтобы проанализировать цифры вместо слов.

//d+ ищет любую цифру, но оказывается, что у меня были данные, похожие на Cts. 2-7, где я хотел значение 7, а не 2.

((?<=\\-)\\d+)")) Ищет дефис, а затем анализирует цифры после него. Если дефиса нет, по умолчанию возвращается \\d+.

Наконец, slice_max сворачивает данные до 1 записи на идентификатор на основе максимального значения total_counts.

person seder163    schedule 07.02.2021