Mar 13: ETL

Запись занятия

Запись занятия 13 марта:

Все записи организованы в плейлист


Манипуляции с таблицами

reshape (Advanced)

Обычная форма представления данных в таблицах — когда одна строка является одним наблюдением, а в значениях колонок отражены те или иные характеристики этого наблюдения. Такой формат традиционно называется wide-форматом, потому что при увеличении количества характеристик таблица будет расти вширь, путем увеличения числа колонок. Пример таблицы в wide-формате.

library(data.table)
# создаем таблицу с идентификатором респондента, его возрастом, ростом и весом
dt_wide <- data.table(
  wave = paste0('wave_', rep(1:2, each = 2)),
  id = paste0('id_', rep(1:2)),
  age = c(45, 68, 47, 69),
  height = c(163, 142, 164, 140),
  weight = c(55, 40, 50, 47))
dt_wide
##      wave   id age height weight
## 1: wave_1 id_1  45    163     55
## 2: wave_1 id_2  68    142     40
## 3: wave_2 id_1  47    164     50
## 4: wave_2 id_2  69    140     47

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

# создаем таблицу с идентификатором респондента, его возрастом, ростом и весом
dt_long <- data.table(
  # две волны, по два респондента в каждой
  wave = paste0('wave_', rep(1:2, each = 6)),
  # на каждого респондента задаем три строки
  id = paste0('id_', rep(rep(1:2, each = 3), 2)),
  # три характеристики повторяем для четырех респондентов
  variable = rep(c('age', 'height', 'weight'), 4),
  # задаем значения характеристик, с учетом того, как упорядочены первые две колонки
  value = c(45, 163, 55,
            68, 142, 40,
            47, 164, 50,
            69, 140, 47))

dt_long
##       wave   id variable value
##  1: wave_1 id_1      age    45
##  2: wave_1 id_1   height   163
##  3: wave_1 id_1   weight    55
##  4: wave_1 id_2      age    68
##  5: wave_1 id_2   height   142
##  6: wave_1 id_2   weight    40
##  7: wave_2 id_1      age    47
##  8: wave_2 id_1   height   164
##  9: wave_2 id_1   weight    50
## 10: wave_2 id_2      age    69
## 11: wave_2 id_2   height   140
## 12: wave_2 id_2   weight    47


0.0.0.1 dcast()

Для того, чтобы трансформировать long-формат в wide-формат, используется функция dcast() пакета data.table (либо cast() пакета reshape2). Также можно использовать функцию reshape() из базового набора функций R, однако эта функция достаточно медленная по скорости работы.

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

dcast(data = dt_long, formula = wave + id ~ variable, value.var = 'value')
##      wave   id age height weight
## 1: wave_1 id_1  45    163     55
## 2: wave_1 id_2  68    142     40
## 3: wave_2 id_1  47    164     50
## 4: wave_2 id_2  69    140     47

Здесь аргумент data - определяет таблицу, которую мы хотим трансформировать.

Аргумент formula задает, что в результирующей таблице будет задавать уникальное наблюдение, и значения какой колонки будут разделены на самостоятельные колонки. Формулу можно прочитать как строки ~ колонки в результирующей таблице. В нашем случае уникальное наблюдение мы задаем парой переменных wave и id, поэтому мы их указываем до тильды через +. Колонки же мы создаем по значениям переменной variable, после тильды. Следует отметить, что ситуация, когда строка задается несколькими переменными через оператор +, весьма частая, а вот в правой части формулы несколько переменных встречаются достаточно редко, обычно все же на колонки раскладывают по значениям одной переменной.

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

Иногда случаются ситуации, когда необходимо провести сначала агрегацию по одной из колонок, описывающих наблюдение. Например, вычислить средние значения возраста, роста и веса для каждой волны. Это можно сделать в два этапа - сначала провести агрегацию, и потом решейп. Также можно сразу сделать решейп, и воспользоваться дополнительным аргументом fun.aggregate, который сразу, при решейпе, агрегирует данные. Например, если использовать сначала агрегацию, а потом трансформацию в wide-формат:

# агрегируем наблюдения по волнам и характеристикам
tmp <- dt_long[, list(value = mean(value)), by = list(wave, variable)]
tmp
##      wave variable value
## 1: wave_1      age  56.5
## 2: wave_1   height 152.5
## 3: wave_1   weight  47.5
## 4: wave_2      age  58.0
## 5: wave_2   height 152.0
## 6: wave_2   weight  48.5
# трансформируем в wide-формат. колонки id уже нет в таблице, поэтому удаляем из формулы
dcast(data = tmp, formula = wave ~ variable, value.var = 'value')
##      wave  age height weight
## 1: wave_1 56.5  152.5   47.5
## 2: wave_2 58.0  152.0   48.5

Аналогично, но с использованием аргумента fun.aggregate. В значения аргумента передаём название функции без кавычек и скобок, в нашем случае это fun.aggregate = mean:

dcast(data = tmp, formula = wave ~ variable, value.var = 'value', fun.aggregate = mean)
##      wave  age height weight
## 1: wave_1 56.5  152.5   47.5
## 2: wave_2 58.0  152.0   48.5


0.0.0.2 melt()

Обратная трансформация также возможна, из wide-формата в long-формат. Для этого используется функция melt():

melt(data = dt_wide, 
     id.vars = c('wave', 'id'),
     measure.vars = c('age', 'height', 'weight'),
     variable.name = 'variable',
     value.name = 'value')
##       wave   id variable value
##  1: wave_1 id_1      age    45
##  2: wave_1 id_2      age    68
##  3: wave_2 id_1      age    47
##  4: wave_2 id_2      age    69
##  5: wave_1 id_1   height   163
##  6: wave_1 id_2   height   142
##  7: wave_2 id_1   height   164
##  8: wave_2 id_2   height   140
##  9: wave_1 id_1   weight    55
## 10: wave_1 id_2   weight    40
## 11: wave_2 id_1   weight    50
## 12: wave_2 id_2   weight    47

Здесь аргумент id.vars задает переменные, которые будут использоваться для уникальной идентификации наблюдения. Аргумент measure.vars определяет те колонки, которые войдут длинную таблицу как значения переменной характеристик наблюдений (когда каждая строка — отдельная характеристика наблюдения, несколько строк на одного пользователя). Аргументы variable.name и value.name задают, соответственно, названия колонок характеристик наблюдения и значений этих характеристик в финальной таблице.

Чтение файлов SPSS

Для импорта файлов SPSS (.sav) обычно используют функции какого-либо из двух пакетов - foreign::read.spss(), который идет в базовом наборе пакетов R, а так же haven::read_spss(), авторства Хэдли Викхэма.

Исходный файл имеет такую структуру (скриншот из SPSS Output):

Функции read_sav() и read_spss() пакета haven обладают схожим с foreign::read.spss() функционалом при меньшем количестве настроек. При этом эти функции несколько быстрее, а так же не имеют проблем с длинными строками. В результате импорта получается tibble-таблица (так как haven принадлежит экосистеме tidyverse).

library(haven)
spss_data <- read_sav('./data/spss_example.sav')
spss_data
## # A tibble: 6 × 3
##    var1  var2                  var3
##   <dbl> <dbl>             <dbl+lbl>
## 1     3     4   1 [среднее]        
## 2     4     5  NA                  
## 3     3     5  99 [отказ от ответа]
## 4     3     5   1 [среднее]        
## 5     4     4   4                  
## 6     3     5 999 [отказ от ответа]

Функция не имеет настроек, как импортировать значения, для которых заданы метки, и в таблице представлены числовые значения. При необходимости получить значения метки переменных или значений необходимо идти в атрибуты колонок таблицы:

str(spss_data)
## tibble [6 × 3] (S3: tbl_df/tbl/data.frame)
##  $ var1: num [1:6] 3 4 3 3 4 3
##   ..- attr(*, "label")= chr "Практичный"
##   ..- attr(*, "format.spss")= chr "F8.2"
##  $ var2: num [1:6] 4 5 5 5 4 5
##   ..- attr(*, "label")= chr "Порядочный"
##   ..- attr(*, "format.spss")= chr "F8.2"
##  $ var3: dbl+lbl [1:6]   1,  NA,  99,   1,   4, 999
##    ..@ label      : chr "Образование"
##    ..@ format.spss: chr "F8.2"
##    ..@ labels     : Named num [1:4] 1 2 99 999
##    .. ..- attr(*, "names")= chr [1:4] "среднее" "высшее" "отказ от ответа" "отказ от ответа"

Пакет haven импортирует данные в свой формат, однако полученный объект можно преобразовать в data.table:

library(data.table)
setDT(spss_data)
class(spss_data)
## [1] "data.table" "data.frame"

У нас есть колонка var3, для которой в SPSS были заданы метки. В полученном формате метки хранятся в атрибутах колонки — это что-то вроде мета-информации. Посмотрим все атрибуты колонки с помощью функции attributes() (иногда это проще делать через синтаксис списков и оператор $):

# смотрим атрибуты вектора-колонки var3
attributes(spss_data[, var3])
## $label
## [1] "Образование"
## 
## $format.spss
## [1] "F8.2"
## 
## $class
## [1] "haven_labelled" "vctrs_vctr"     "double"        
## 
## $labels
##         среднее          высшее отказ от ответа отказ от ответа 
##               1               2              99             999
# через синтаксис списков
attributes(spss_data$var3)
## $label
## [1] "Образование"
## 
## $format.spss
## [1] "F8.2"
## 
## $class
## [1] "haven_labelled" "vctrs_vctr"     "double"        
## 
## $labels
##         среднее          высшее отказ от ответа отказ от ответа 
##               1               2              99             999

Мы видим четыре атрибута — метку переменной ($label), SPSS-формат данных ($format.spss), обозначение, что переменная в SPSS имеет метки значений ($class) и собственно вектор меток, которые используются для значений этой колонки ($labels).

Для того, чтобы сохранить метки значений (а не сами значения), можно создать новую колонку и в нее записать с результат применения функции as_factor пакета haven — эта функция извлекает и сопоставляет значения и их метки.

spss_data[, var4 := as_factor(var3)]
spss_data
##    var1 var2 var3            var4
## 1:    3    4    1         среднее
## 2:    4    5   NA            <NA>
## 3:    3    5   99 отказ от ответа
## 4:    3    5    1         среднее
## 5:    4    4    4               4
## 6:    3    5  999 отказ от ответа

Как мы видим, для значений 1, 99, 999 были проставлены метки, и они оказались в новой переменной, а не атрибутом. Для значения 4 метки не было, поэтому оно было записано именно значением.