Здесь представлен перевод статьи [mailto: dusa.adrian@gmail.com Adrian Duşa] 'From SPSS to R'

О чем эта статья

править

Когда я только начал использовать R, мне потребовалось узнать ответы на те вопросы, которые раньше для меня таковыми не являлись, например:

  • где же мои данные?
  • м-м-м, как я могу установить метки значений?
  • и мне действительно необходима возможность устанавливать метки переменных!
  • (почесывая затылок) а теперь как я могу отсортировать данные, если здесь нет возможности нажать на переменной правой кнопкой мыши?
  • постойте, как же я смогу создавать те выходные таблицы с потрясающим форматированием?
  • и как я могу скопировать и вставить результат в текстовый редактор Word?
  • формат файлов SPSS — .sav; как сохранять данные в R?
  • и многое-многое другое…

Звучит знакомо? Думаю, да. Хорошо… давайте рассмотрим все внимательно и по порядку.

Во-первых, переход на R — это изменение в восприятии. Видеть данные — это похоже на наркотическую зависимость, от которой нужно избавится: вам не нужно их видеть. И вы их, действительно, не видите, поскольку то, что предлагает SPSS — это только маленький прямоугольный кусочек от большого массива. Эта маленькая зрительная хитрость, которую вы можете осуществить и в R с использованием простых команд.

Нам нужен экспериментальный массив данных; скажем, вот этот:

ONE TWO AGE
1 2 a 42
2 1 a 18
3 3 b 49
4 2 a 62
5 1 b 25

Просто скопируйте это и вставьте в R:

test <- data.frame(ONE = c(2, 1, 3, 2, 1), 
                   TWO = c("a", "a", "b", "a", "b"), 
                   AGE = c(42, 18, 49, 62, 25))
test

Другой путь: вы можете выделить скопировать содержимое таблицы в буфер обмена и набрать следующую команду:

test <- read.table('clipboard', header = TRUE)
test

В обоих случаях должно получиться так:

  ONE TWO AGE
1   2   a  42
2   1   a  18
3   3   b  49
4   2   a  62
5   1   b  25

Конечно, мы можем видеть (и редактировать) данные знакомым нам способом, используя команду fix(test) (но я никогда так не делал с того времени, как перешел на R).

Хорошо, далее несколько команд SPSS, выполнение которых в R может быть вам интересным.

Сортировка наблюдений

править

Мы хотим отсортировать наши данные в возрастающем порядке по переменной ONE:

test[order(test$ONE), ]

Ключ к пониманию этой команды (звучит как введение в R, но по вопросу основ лучше прочить руководство «Введение в R») лежит в том, как R индексирует строки и столбцы. Существует два ключевых инструмента, которые делают такую сортировку:

  • функция order(), смотрите справку через команду ?order;
  • содержимое, заключенное в квадратные скобки.

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

Мы можем захотеть отсортировать данные в убывающем порядке:

test[order(test$ONE, decreasing = TRUE), ]

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

> letter.vector <- c("c", "e", "d", "b", "a")
> sort(letter.vector)
[1] "a" "b" "c" "d" "e"

Ага! Наш вектор отсортирован. Но в чем же отличия от функции order()?

> order(letter.vector)
[1] 5 4 1 3 2

order() возвращает вектор индексов. Последний символ в нашем векторе — это «а» (пятая позиция), которая в векторе, возвращенным функцией order(), находится на пятой позиции (число 5 возвращается первым). И так далее:

> letter.vector[order(letter.vector)]
[1] "a" "b" "c" "d" "e"

Теперь разница понятна: функция sort() использует индексы, возвращенные функцией order().

Сортировка наблюдений массива данных имеет отношение к индексации; поэтому мы используем для этой цели функцию order(). Кажется несколько противоречащим здравому смыслу не использовать sort() для сортировки строк, но как только вы к этому привыкните, вы сразу почувствуете это естественным.

Фильтрация наблюдений

править

Это очень просто: мы можем индексировать массив данных используя «[ , ]».

Точно также как при сортировке, мы напишем что-нибудь перед запятой, так как мы предпринимаем операцию над строками.

Выберем все наблюдения, где переменная ONE равна 1:

 > test[test$ONE == 1, ]
   ONE TWO AGE
 2   1   a  18
 5   1   b  25

Выберем все наблюдения, где переменная ONE равна 1, а переменная TWO равна «а»:

 > test[test$ONE == 1 & test$TWO == "a", ]
   ONE TWO AGE
 2   1   a  18

Выберем все наблюдения, где переменная ONE равна 2, а переменная AGE меньше, чем 60:

 > test[test$ONE == 2 & test$AGE < 60, ]
   ONE TWO AGE
 1   2   a  42

Конечно, если мы используем функцию attach(), то синтаксис будет еще проще:

 > attach(test)
 > test[ONE == 2 & AGE < 60, ]
   ONE TWO AGE
 1   2   a  42

Метки значений

править

Существует огромная разница между SPSS и R в подходе к категориальным переменным. Не кажется ли странным, что приложение стоимостью в несколько тысяч долларов, как, например, SPSS, даже не пытается вывести предупреждение, когда кто-либо высчитывает арифметическое среднее номинальной переменной? Невероятно…

В R категориальная переменная называется фактор (factor). Номинальная переменная — это неупорядоченный (unordered) фактор, а порядковая переменная — это упорядоченный (ordered) фактор.

Категории номинальных и порядковых переменных называются уровнями (levels).

Первое разительное отличие R заключается в том, что факторы не выводятся как номера. Давайте, для примера, возьмем переменную TWO из нашего массива:

 > test$TWO
 [1] a a b a b
 Levels: a b

Ага… уровни (наши «метки») уже здесь. У нас есть два уровня: «a» и «b». Теперь давайте предположим, что переменная TWO обозначает пол: двумя категориями «мужчина» («male») и «женщина» («female»). Мы легко можем поменять наши «метки»:

 > levels(test$TWO) <- c("male", "female")
 > test$TWO
 [1] male   male   female male   female
 Levels: male female

Опа! Теперь, если кто-нибудь хотя бы помыслить об извлечении среднего значения такой переменной:

 > mean(levels$TWO)
 [1] NA
 Warning message:
 argument is not numeric or logical: returning NA in: mean.default(levels$TWO)

«Аргумент не является числовым или логическим». Точка.

Вы можете подумать, что использование меток вместо номеров — это глупо (правда, и я думал также). Почему кто-то захочет так делать? Когда мы вводим данные, мы используем цифры, а не буквы. Так в чем же загвоздка? Дело в том, что то, что вы видите на экране — это совсем не то, что сохранено в базе данных. Хранилище (если я не ошибаюсь) состоит из чисел, R только печатает на экране уровни по умолчанию.

Давайте попробуем следующим образом:

 > as.numeric(test$TWO)
 [1] 1 1 2 1 2

Так… мужчины — это 1, женщины — 2.

Возвращаясь к нашей проблеме: если у нас есть переменная, введенная в числовой форме (как переменная ONE), то первое, что мы должны сделать — это объявить ее фактором (допустим, что наша переменная принимает три возможных значения, то есть имеет три категории):

 > test$ONE <- factor(test$ONE, ordered = TRUE)
 > test$ONE
 [1] 2 1 3 2 1
 Levels: 1 < 2 < 3

Обратите внимание на знаки «меньше» («<») между уровнями: их наличие говорит нам о том, что переменная является порядковой.

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

> levels(test$ONE) <- c("Low", "Medium", "High")

Или, если вы предпочтете явно указать соответствие конкретного числа уровню:

 > levels(test$ONE) <- list("Low" = 1, "Medium" = 2, "High" = 3)
 > test$ONE
 [1] Medium Low    High   Medium Low
 Levels: Low < Medium < High

Метки переменных

править

Это еще проще, чем метки значений. Мне известно о двух способах.

Первый способ, это присвоение метки через функцию attr().

attr(test$TWO, "label") <- "Пол респондента"

Мы только что присвоили переменной TWO дополнительный атрибут под именем «label» (имя не играет никакой роли и могло быть другим, например «varlab»). Теперь давайте запросим метку (label) переменной TWO:

 > attr(test$TWO, "label")
 [1] "Пол респондента"

Второй способ, это использование функции label() из пакета Hmisc.

library(Hmisc)
label(test$TWO) <- "Пол респондента"

Эта функция делает то же самое, только в более интуитивной форме. Снова запросим метку:

 > label(test$TWO)
 [1] "Пол респондента"

Создание таблиц

править

Здесь трудно аргументировать, но эта задача, возможно, потребует от вас больше усилий, чем вы можете предполагать. Но, поскольку вы уже ступили на дорогу R и открытых исходников, почему бы не пойти до конца?

Есть два вопроса по поводу таблиц, по которым я должен вас убедить:

  • как таблица должна выглядеть?
  • каким легким способом таблицу можно вставить в документ?

Это может прозвучать неубедительно, но… вам не нужны таблицы с форматированием в стиле SPSS. Проблема действительно в этом: таблицы SPSS слишком «заформатированы». Слишком много выделений, слишком много линий, слишком много прямоугольников. Все это так притягательно, что отвлекает внимание от главного в таблице — числа в ней.

Позвольте задать вам один вопрос: как много вы встречали публикаций с таблицами, отформатированными в стиле SPSS? Я попробую угадать и скажу: ни одной. Профессиональные журналы используют очень простое форматирование для таблиц, всего лишь несколько линий, чтобы разделить важное содержимое. Такие таблицы могут быть созданы с использованием специального программного обеспечения, например LaTeX.

Ниже приведена частотная таблица, созданная в SPSS для переменной ONE из нашего массива.

То же самое в R:

> (mytable <- table(test$ONE))
    Low Medium   High
      2      2      1

Определенная информация, линий вообще никаких. Нам нужен общий итог или доли? Это просто:

 > sum(mytable)
 [1] 5
 > prop.table(mytable)
 
    Low Medium   High
    0.4    0.4    0.2

Функция table() очень мощна. Взгляните на нее и изучите, как она работает. Она вам действительно пригодится.

Существует множество способов, как вставить таблицу в текст. Это зависит от того, какое ПО вы используете:

  • Для LaTeX я бы снова использовал пакет Hmisc. В него входит специальная функция для создания LaTeX-таблиц (вас может заинтересовать функция Sweave())
  • Для OpenOffice.org существует другой замечательный пакет под названием odfWeave, который выполняет ту же саму задачу, что и Sweave(), но в документе OpenOffice. Вам даже не требуется копировать и вставлять таблицы или диаграммы: просто напишите код программы R в своем документе, пропустите его через R (да-да, R может так сделать) и на выходе вы получите новый документ ODF с таблицами и диаграммами именно в тех местах, где вы написали код.

Ну а если вдруг мне захочется создать в LaTeX точную копию таблицы SPSS, то я создам матричный объект (назовем его ONE), подобный этому:

        Frequency Percent Cumulative\nPercent
 Low            2    40.0                40.0
 Medium         2    40.0                80.0
 High           1    20.0               100.0
 Total          5   100.0                <NA>

И теперь генерируем код LaTeX следующим образом:

library(Hmisc)
latex(ONE, file = "")

Запускаем полученный код в LaTeX и получаем таблицу:

 

Аккуратно, не правда ли? Это тот самый вид таблиц, который мы привыкли видеть в профессиональных журналах.

Если вам нужны еще аргументы, то следующий факт точно убедит вас: в SPSS есть одна вещь, которую вы можете сделать с таблицей — это вставить ее в текстовый документ. Как красивую картинку. В R каждая таблица может быть определена как отдельный объект, из которого можно вытягивать любую информацию. У вас есть динамический доступ к любому и каждому числу внутри в любое время. Ну как?

Перекодировка переменных

править

Это то же самое, что создать одну переменную на основе значений другой переменной; и перекодировка в ту же переменную тоже проста.

В другую переменную

править

Используя индексирование

править

Предположим, мы хотим перекодировать переменную AGE в три категории: «Молодой», «Взрослый», «Пожилой». Мы создаем переменную AGEREC, значения которой по умолчанию являются пропущенными. Поскольку эта переменная у нас порядкового уровня, мы можем объявить ее таковой через функцию ordered():

test$AGEREC <- ordered(NA, levels = c("Young", "Adult", "Aged"))

Порядок уровней берется из очередности, в которой мы их указали (Уровень «Молодой» будет первым).

Теперь мы просто используем мощь индексации, заложенной в R.

test$AGEREC[test$AGE <= 35] <- "Young"
test$AGEREC[test$AGE > 35 & test$AGE <= 60] <- "Adult"
test$AGEREC[test$AGE > 60] <- "Aged"

Используя функцию cut()

править

Существует несколько способов достичь одного и того же результата. Один из них — это использование функции cut(). Допустим, у нас нет респондентов старше 100 лет.

 > test$AGEREC <- cut(test$AGE, breaks = c(0, 35, 60, 100))
 > test$AGEREC
 [1] (35,60]  [0,35]   (35,60]  (60,100] [0,35]
 Levels: [0,35] (35,60] (60,100]

Результирующая переменная является фактором, значения уровней которого понятны интуитивно. Мы можем объявить порядок и значения уровней явно:

 > levels(test$AGEREC) <- c("Young", "Adult", "Aged")
 > test$AGEREC <- ordered(test$AGEREC)
 > test$AGEREC
 [1] Adult Young Adult Aged  Young
 Levels: Young < Adult < Aged

Используя функцию recode()

править

Еще один способ сделать то же самое, это использовать функцию recode() из пакета car.

 > library(car)
 > test$AGEREC <- recode(test$AGE, "0:35='Young'; 36:60='Adult'; else='Aged'")
 > test$AGEREC
 [1] "Adult" "Young" "Adult" "Aged"  "Young"

Обратите внимание, что переменная, полученная в результате, не является фактором (не все символьные переменные имеют тип «фактор»). Мы можем использовать аргумент as.factor.result функции recode(), но по умолчанию это создаст уровни, упорядоченные по алфавиту, нам же больше понравится, если уровень «Молодой» будет первым.

Традиционное решение — это объявить переменную упорядоченным фактором:

 > test$AGEREC  <- ordered(test$AGEREC, levels = c("Young", "Adult", "Aged"))
 > test$AGEREC
 [1] Adult Young Adult Aged  Young
 Levels: Young < Adult < Aged

В ту же переменную

править

Здесь трудно добавить что-то новое. Просто с левой и правой стороны оператора присваивания «<-» мы используем имя одной и той же переменной:

test$AGE <- cut(test$AGE, breaks = c(0, 35, 60, 100))
# or
test$AGE <- recode(test$AGE, "0:35='Young'; 36:60='Adult'; else='Aged'")

Единственная хитрость появляется при индексировании. Покажем на примере:

 > aa <- test$AGE
 > aa
 [1] 42 18 49 62 25
 
 > # Производим первое замещение:
 > aa[aa <= 35] <- "Young"
 > aa
 [1] "42"    "Young" "49"    "62"    "Young"

В результате этой операции все значения были преобразованы в символьный формат, поэтому дальнейшие попытки сравнить переменную с числом 60 ни к чему не приведут. Решение, которое я здесь предлагаю, это создать временный объект, использовать его при индексации нашей переменной, а затем просто удалить его.

> age <- test$AGE
 > test$AGE[age <= 35] <- "Young"
 > test$AGE[age > 35 & age <= 60] <- "Adult"
 > test$AGE[age > 60] <- "Aged"
 > rm(age)
 > test$AGE
 [1] "Adult" "Young" "Adult" "Aged"  "Young"

Естесственно, все остальные приемы с уровнями и упорядочиванием остаются в силе.

Пропущенные значения

править

Как сохранять в формате R?

править

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

Поэтому вопрос: как сохранять что?

Поскольку R манипулирует большим количеством объектов, поэтому и понимает он много форматов данных (включая .sav и .por файлы).

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

  1. «.R», «.r»;
  2. «.RData», «rda»;
  3. «.tab», «.txt» или «.TXT»;
  4. «.csv», «.CSV».

Но этот список не является обязательным к исполнению. Можно назвать файл любым именем с любым расширением, потому что при чтении этого файла будет необходимо указать полное имя, включая расширение.

Обычно, файлы, содержащие код R (то есть последовательность команд для выполнения), имеют расширение «.R».

Еще одно важное замечание: так как R может работать сразу с множеством объектов в памяти одновременно (представьте сразу два загруженных в память массива данных SPSS), он так же позволяет записывать несколько объектов в один файл.

В следующих примерах я буду предполагать, что чтение и запись файлов ведется в текущей директории (для справки см. ?getwd и ?setwd).

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

dump() и source()

править

Функция dump() записывает комбинацию команд (например, такие, как мы использовали для создания нашего пробного массива) и внутреннюю структуру массива данных.

Код R обычно записывается файлы с расширением «.R», поэтому мы выберем такое же расширение:

dump("test", "test.R")

На следующем шаге мы можем использовать команды из файла «test.R» с использованием функции source():

source("test.R")

На диске файл «test.R» записан в обычном текстовом формате, поэтому он вполне читаем. Вы можете посмотреть на него, чтобы понять принцип работы dump().

save() и load()

править

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

Большое достоинство функции save() — это маленький размер получаемых файлов. Они могут быть дополнительно сжаты (или путем применения аргумента «compress», или последующего сжатия утилитой gzip), и R без каких-либо проблем прочтет эти файлы.

Синтакс функции очень прост:

save(test, file = "test.Rdata")

Первое отличие от функции dump(): первый аргумент не заключен в кавычки. Чтобы понять, почему, обратитесь к справке по обеим функциям.

Чтение данных производится просто:

load("test.Rdata")

Дополнительно, если пользователь хочет сохранить все объекты, находящиеся в данный момент в памяти, мы можем сохранить все рабочее пространство, используя функцию save.image()

save.image("2006_09_25.Rdata")

dput() и dget()

править

Функция dput() делает практически то же самое, что и dump(), но с одним отличием: результатом выполнения функции не являются инструкции R. Результат содержит в себе только внутреннюю структуру объекта.

dput(test, "test.Rdput", control = "all")

Здесь мы использовали расширение имени файла «.Rdput» (расширение может быть любым). Очень полезно использовать аргумент control = "all": это даст гарантию, что внутренняя структура объекта будет записана в файл полностью (включая все атрибуты объекта).

Считываем данные с диска:

test <- dget(test, "test.Rdput")

Еще одно отличие от команды dump() — это явное создание объекта, когда в предыдущем варианте создание объекта происходит через инструкции, записанные в файл.

write.table() и read.table()

править

Функция write.table() создает ASCII-представление массива данных, расширение имени выходного файла — «.tab», «.txt» или «.csv» в зависимости от вида заданного разделителя (аргумент sep).

Выходной файл потеряет всю внутреннюю структуру объекта, то есть по нему мы не сможем судить, является ли переменная фактором, или упорядоченным фактором и т. д.

Для файлов, разделенных табуляцией, инструкция будет следующей:

write.table(test, "test.tab", sep = "\t", row.names = FALSE)

Обычно, данные формата SPSS не имеют названий строк (подобно именам переменных). R, со своей стороны, такие имена содержит (еще одна причина предпочесть R), поэтому, если вы хотите получить имена переменных, установите значение соответствующего аргумента (header) в TRUE.

Функция read.table() одна из самых полезных, она имеет множество аргументов. Минимальная форма применения следующая:

read.table("test.tab", header = TRUE, sep = "\t")

Для файла со значениями, разделенными запятыми, команда такая же, только аргумент sep меняется:

write.table(test, "test.tab", sep = ",")

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

В случае с вектором, если он является символьным, в файл он будет записан по умолчанию в одну колонку; если вектор числовой, то необходимо использовать аргумент ncolumns:

temp <- letters[1:100]
write(temp, "temp.Rwrite")

В случае с матрицей, мы должны транспонировать ее перед тем, как записывать в файл:

temp <- matrix(1:100, ncol = 20)
write(t(temp), "temp.Rwrite")

Выходной файл не содержит имен строк и столбцов.

.saveRDS() и .readRDS()

править

Данный подход рекомендуется для смелых пользователей.

Обратите внимание на наличие точек в начале названия функций.

Иногда возникает ситуация, когда необходимо сохранить объекты R в базе данных, подобных MySQL. Обычно это связано с тем, что массив данных очень большой.

Эта операция проста для массивов данных (dataframe) — они имеют структуру, подобную структуре баз данных. Для более сложных объектов, например, списки или даже матрицы, такое прямое сохранение просто невозможно.

R может преобразовать любой свой объект в двоичную форму, как в формат BLOB (большой бинарный объект) или его ASCII-представление. Это преобразование называется сериализацией и оно дает большое преимущество, так как сериализованный объект — это ничто иное, как простой вектор (как колонка в таблице MySQL).

Чтобы сериализовать объект, используйте функцию serialize(), чтобы обратить сериализованный объект в нормальное состояние, используйте unserialize().

Функция .saveRDS() позволяет сохранить сериализованный объект в текущей директории, .readRDS() считывает его обратно.

Простой пример:

temp <- matrix(1:100, ncol = 20)
.saveRDS(temp, file="temp.Rdata", ascii=TRUE)

Эта команда сохранит сжатый ASCII-вариант сериализованного объекта R. Если же вас интересует, как выглядит сериализованный объект, выполните следующее:

.saveRDS(temp, file="temp.Rdata", ascii=TRUE, compress=FALSE)

и теперь вы можете просмотреть структуру объекта, используя любой текстовый редактор.

См. также

править

Для более детальной информации о R на русском языке смотрите: