Заменить значения в некоторых строках на основе другого сопоставления фрейма данных

У меня есть таблица (d.tab) с парами вопросов и ответов из опроса. Некоторые из них являются ответами с одним выбором, некоторые - с несколькими вариантами ответов. Я хочу найти текстовое значение ответа с одним выбором из его числового значения. Для этого у меня есть таблица поиска (d.lookup).

Я попытался merge их, но это немного некрасиво, так как теперь мне нужно отфильтровать все строки, где value != answer_id. Есть ли более красивый способ сделать это, возможно, используя plyr или dplyr или tidyr?

tab = '
question_id question_type   subject value
1   single-choice   1   1
2   multiple-choice 1   2
3   single-choice   1   2
1   single-choice   2   2
2   multiple-choice 2   3,4
3   single-choice   2   2
'

lookup = '
question_id answer_id   answer_text
1   1   female
1   2   male
3   1   no
3   2   yes
'

d.tab = read.table(text = tab, header = TRUE)
d.lookup = read.table(text = lookup, header = TRUE)

merge(d.tab, d.lookup, by = "question_id", all.x = TRUE)

Я не хочу ничего делать с multiple-choice строками, а просто обновляю исходный фрейм данных, чтобы заменить value фактическим текстом из answer_text d.tab, если answer_id соответствуют value.

Я знаю, что могу:

merge(d.tab, d.lookup, by.x = c("question_id", "value"), by.y = c("question_id", "answer_id"), all.x = TRUE)

Но это дает мне новый столбец answer_text с исходным value, который мне не нужен.


person slhck    schedule 27.02.2016    source источник
comment
Вы можете объединить несколько столбцов, которые могут даже иметь разные имена. Это правильно, что вы на самом деле хотите объединиться только для ответов с одним выбором? В противном случае строка с 3,4 будет немного сложной.   -  person Stibu    schedule 27.02.2016
comment
Да, я не хочу ничего делать со строками с множественным выбором. Это просто для того, чтобы показать, что мне нужно заменить только подмножество значений.   -  person slhck    schedule 27.02.2016
comment
См. обновленный вопрос; Я только что заметил, что могу сделать это слияние с несколькими ключами, но это не совсем то, что мне нужно.   -  person slhck    schedule 27.02.2016


Ответы (2)


У вас есть правильный вызов merge() в вашем вопросе. Остается только отфильтровать строки с ответами с одним выбором и выбрать все столбцы, кроме value. С помощью dplyr это можно сделать следующим образом:

library(dplyr)
filter(d.tab, question_type == "single-choice") %>%
  mutate(value = as.numeric(as.character(value))) %>%
  merge(d.lookup, by.x = c("question_id", "value"),
        by.y = c("question_id", "answer_id")) %>%
  select(-value)

Вторая строка содержит явное преобразование факторной переменной value в числовую. Это важно, потому что преобразование коэффициентов в числовые может привести к странным результатам. Ниже я добавлю несколько строк по этой теме.

Обратите внимание, что dplyr также имеет свои собственные функции для замены слияния. Если ваш стол большой, вы заметите, что они более эффективны. Используя left_join из dplyr, решение гласит:

library(dplyr)
filter(d.tab, question_type == "single-choice") %>%
  mutate(value = as.numeric(as.character(value))) %>%
  left_join(d.lookup,
            by = c("question_id" = "question_id",
                   "value" = "answer_id")) %>%
  select(-value)

Итак, вот комментарий относительно факторов, которые я обещал. Проблема с факторами заключается в том, что они на самом деле являются целыми числами, где каждое целочисленное значение имеет связанную с ним метку. Когда вы наивно преобразуете коэффициенты в числовые с помощью as.numeric(), вы получите целое число, связанное с меткой. Вы почти наверняка столкнетесь с этой проблемой с вашими данными, и вот почему.

Я создаю факторную переменную, которая имитирует ваши данные:

values <- factor(c("1", "2", "3,4", "3", "4"))

Теперь я отбрасываю третье значение ("3,4") и конвертирую в числовое:

as.numeric(values[-3])
## [1] 1 2 3 5

Это, вероятно, не то, что вы ожидали. Причина в том, что числа от 1 до 5 были связаны с пятью уровнями, которые мы определили выше. Если вы хотите получить числа, соответствующие меткам, вам нужно сначала преобразовать их в символы:

as.numeric(as.character(values[-3]))
## [1] 1 2 3 4

Таким образом, хотя merge() где-то и выполняет преобразование коэффициентов в числовые, я бы не стал полагаться на то, что он сделает это так, как вы хотите. Таким образом, вы должны сделать преобразование явно.

person Stibu    schedule 27.02.2016
comment
Вот это отличный совет, спасибо большое! Теперь у меня есть небольшая вариация проблемы. Допустим, у меня был другой тип вопроса likert и таблица поиска с единственными столбцами answer_id и answer_text (например, ответ 1 означает категорически не согласен, 2 означает несогласие и т. д.). Как мне заменить это подмножество значений моего исходного фрейма данных (при условии, что я сохранил исходный столбец value нетронутым)? Когда я выполняю здесь слияние, я получаю столбцы answer_text.x и answer_text.y, причем первый по-прежнему NA для строк, где я бы его заменил. - person slhck; 27.02.2016
comment
Не могли бы вы просто объединить value и answer_id. Ничего страшного, если в d.tab много совпадений. Соответствующая строка из таблицы поиска будет просто объединена в d.tab столько раз, сколько потребуется. Или ваш вопрос был о чем-то другом? - person Stibu; 27.02.2016
comment
Да, я мог бы выполнить это слияние, но предположим, что я уже выполнил другое слияние раньше, поэтому у меня уже есть столбец answer_text, содержащий NA значений для строк, где question_type равно likert. Если я затем выполню еще одно слияние, у меня уже будет столбец answer_text, а затем я получу answer_text.x и answer_text.y в своем результате. Если это немного сложно представить без данных — что я могу понять — я, конечно, могу опубликовать новый вопрос :) - person slhck; 27.02.2016

Альтернативное решение с data.table:

library(data.table)

# converting to datatables & setting the 'answer_id' to character
setDT(d.tab)
setDT(d.lookup)[, answer_id := as.character(answer_id)]

# join 'd.tab' with 'd.lookup' and update 'value' by reference
d.tab[d.lookup, value := answer_text, on = c("question_id", "value"="answer_id")]

который дает:

   question_id   question_type subject  value
1:           1   single-choice       1 female
2:           2 multiple-choice       1      2
3:           3   single-choice       1    yes
4:           1   single-choice       2   male
5:           2 multiple-choice       2    3,4
6:           3   single-choice       2    yes

Как уже упоминалось @Stibu, вероятно, лучше разделить строки с несколькими значениями. Пример с функцией cSplit из пакета splitstackshape:

library(splitstackshape)
cSplit(d.tab, "value", sep=",", 
       direction="long", 
       type.convert = FALSE)[d.lookup, 
                             value := answer_text, 
                             on = c("question_id", "value"="answer_id")]

# or everything in 'data.table'
d.tab[, lapply(.SD, function(x) unlist(tstrsplit(x, ','))), setdiff(names(d.tab),"value")
      ][d.lookup, value := answer_text, on = c("question_id", "value"="answer_id")][]

которые оба дают:

   question_id   question_type subject  value
1:           1   single-choice       1 female
2:           2 multiple-choice       1      2
3:           3   single-choice       1    yes
4:           1   single-choice       2   male
5:           2 multiple-choice       2      3
6:           2 multiple-choice       2      4
7:           3   single-choice       2    yes
person Jaap    schedule 27.02.2016
comment
Выглядит очень чисто, спасибо! (Почему опять же существует миллион решений для достижения одной цели в R?) - person slhck; 27.02.2016