КулЛиб - Классная библиотека! Скачать книги бесплатно 

Наглядная статистика. Используем R! [Коллектив Авторов] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]
А. Б. Шипунов, Е. М. Балдин, П. А. Волкова,
А. И. Коробейников, С. А. Назарова,
С. В. Петров, В. Г. Суфиянов

Наглядная
статистика
Используем R!

26 июля 2020 г.
(исправленная версия)

Оглавление
Предисловие . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

7

Глава 1. Что такое данные и зачем их обрабатывать?
1.1. Откуда берутся данные . . . . . . . . . . . . . . . .
1.2. Генеральная совокупность и выборка . . . . . . . .
1.3. Как получать данные . . . . . . . . . . . . . . . . .
1.4. Что ищут в данных . . . . . . . . . . . . . . . . . .

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

10
10
12
13
17

Глава 2. Как обрабатывать данные . . . . . . . . . . .
2.1. Неспециализированные программы . . . . . . . .
2.2. Специализированные статистические программы
2.2.1. Оконно-кнопочные системы . . . . . . . . .
2.2.2. Статистические среды . . . . . . . . . . . .
2.3. Из истории S и R . . . . . . . . . . . . . . . . . . .
2.4. Применение, преимущества и недостатки R . . .
2.5. Как скачать и установить R . . . . . . . . . . . .
2.6. Как начать работать в R . . . . . . . . . . . . . .
2.6.1. Запуск . . . . . . . . . . . . . . . . . . . . .
2.6.2. Первые шаги . . . . . . . . . . . . . . . . .
2.7. R и работа с данными: вид снаружи . . . . . . . .
2.7.1. Как загружать данные . . . . . . . . . . . .
2.7.2. Как сохранять результаты . . . . . . . . .
2.7.3. R как калькулятор . . . . . . . . . . . . . .
2.7.4. Графики . . . . . . . . . . . . . . . . . . . .
2.7.5. Графические устройства . . . . . . . . . . .
2.7.6. Графические опции . . . . . . . . . . . . . .
2.7.7. Интерактивная графика . . . . . . . . . . .

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

21
21
22
22
24
24
25
27
28
28
29
30
30
36
37
38
40
42
43

Глава 3. Типы данных . . . . . . . . . . . . . . . . . . . .
3.1. Градусы, часы и километры: интервальные данные
3.2. «Садись, двойка»: шкальные данные . . . . . . . .
3.3. Красный, желтый, зеленый: номинальные данные .
3.4. Доли, счет и ранги: вторичные данные . . . . . . .
3.5. Пропущенные данные . . . . . . . . . . . . . . . . .
3.6. Выбросы и как их найти . . . . . . . . . . . . . . .

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

46
46
49
50
56
59
61

4
3.7.
3.8.

Меняем данные: основные принципы преобразования .
Матрицы, списки и таблицы данных . . . . . . . . . .
3.8.1. Матрицы . . . . . . . . . . . . . . . . . . . . . . .
3.8.2. Списки . . . . . . . . . . . . . . . . . . . . . . . .
3.8.3. Таблицы данных . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.

.
.
.
.
.
.

62
64
64
66
68

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

73
73
83
84
88
91

Глава 5. Анализ связей: двумерные данные . . . . . .
5.1. Что такое статистический тест . . . . . . . . . . . .
5.1.1. Статистические гипотезы . . . . . . . . . . .
5.1.2. Статистические ошибки . . . . . . . . . . . .
5.2. Есть ли различие, или Тестирование двух выборок
5.3. Есть ли соответствие, или Анализ таблиц . . . . .
5.4. Есть ли взаимосвязь, или Анализ корреляций . . .
5.5. Какая связь, или Регрессионный анализ . . . . . .
5.6. Вероятность успеха, или Логистическая регрессия
5.7. Если выборок больше двух . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

95
95
95
96
97
103
110
115
125
128

Глава 6. Анализ структуры: data mining . . . . . . . . . . .
6.1. Рисуем многомерные данные . . . . . . . . . . . . . . . .
6.1.1. Диаграммы рассеяния . . . . . . . . . . . . . . . .
6.1.2. Пиктограммы . . . . . . . . . . . . . . . . . . . . .
6.2. Тени многомерных облаков: анализ главных компонент
6.3. Классификация без обучения, или Кластерный анализ .
6.4. Классификация с обучением,
или Дискриминантный анализ . . . . . . . . . . . . . . .

144
144
145
148
151
157

Глава 4. Великое в малом: одномерные данные
4.1. Как оценивать общую тенденцию . . . . . .
4.2. Ошибочные данные . . . . . . . . . . . . . .
4.3. Одномерные статистические тесты . . . . .
4.4. Как создавать свои функции . . . . . . . . .
4.5. Всегда ли точны проценты . . . . . . . . . .

.
.
.
.
.
.

.
.
.
.
.

166

Глава 7. Узнаем будущее: анализ временных рядов
7.1. Что такое временные ряды . . . . . . . . . . . . .
7.2. Тренд и период колебаний . . . . . . . . . . . . .
7.3. Построение временного ряда . . . . . . . . . . . .
7.4. Прогноз . . . . . . . . . . . . . . . . . . . . . . . .

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

175
175
175
176
183

Глава 8. Статистическая разведка . . .
8.1. Первичная обработка данных . . .
8.2. Окончательная обработка данных
8.3. Отчет . . . . . . . . . . . . . . . .

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

192
192
192
193

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

5
Приложение А. Пример работы в R . . . . . . . . . . . . . . 198
Приложение Б. Графический
Б.1. R Сommander . . . . . .
Б.2. RStudio . . . . . . . . .
Б.3. RKWard . . . . . . . . .
Б.4. Revolution-R . . . . . . .
Б.5. JGR . . . . . . . . . . .
Б.6. Rattle . . . . . . . . . .
Б.7. rpanel . . . . . . . . . .
Б.8. ESS и другие IDE . . .

интерфейс (GUI) для
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .

Приложение В. Основы программирования в R . . .
В.1. Базовые объекты языка R . . . . . . . . . . . . . .
В.1.1. Вектор . . . . . . . . . . . . . . . . . . . . .
В.1.2. Список . . . . . . . . . . . . . . . . . . . . .
В.1.3. Матрица и многомерная матрица . . . . .
В.1.4. Факторы . . . . . . . . . . . . . . . . . . . .
В.1.5. Таблица данных . . . . . . . . . . . . . . .
В.1.6. Выражение . . . . . . . . . . . . . . . . . .
В.2. Операторы доступа к данным . . . . . . . . . . .
В.2.1. Оператор [ с положительным аргументом
В.2.2. Оператор [ с отрицательным аргументом .
В.2.3. Оператор [ со строковым аргументом . . .
В.2.4. Оператор [ с логическим аргументом . . .
В.2.5. Оператор $ . . . . . . . . . . . . . . . . . .
В.2.6. Оператор [[ . . . . . . . . . . . . . . . . . .
В.2.7. Доступ к табличным данным . . . . . . . .
В.2.8. Пустые индексы . . . . . . . . . . . . . . . .
В.3. Функции и аргументы . . . . . . . . . . . . . . . .
В.4. Циклы и условные операторы . . . . . . . . . . .
В.5. R как СУБД . . . . . . . . . . . . . . . . . . . . .
В.6. Правила переписывания. Векторизация . . . . . .
В.7. Отладка . . . . . . . . . . . . . . . . . . . . . . . .
В.8. Элементы объектно-ориентированного
программирования в R . . . . . . . . . . . . . . . .
Приложение Г. Выдержки
Г.1. Среда R . . . . . . .
Г.2. R и S . . . . . . . . .
Г.3. R и статистика . . .
Г.4. Получение помощи .
Г.5. Команды R . . . . .

из
. .
. .
. .
. .
. .

документации
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .

R
. .
. .
. .
. .
. .

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

R
. .
. .
. .
. .
. .
. .
. .
. .

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

209
209
211
213
213
216
217
218
220

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

222
222
222
223
224
225
226
226
227
227
228
228
229
229
230
231
233
233
236
237
240
245

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

. . . . 248
.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

251
251
252
252
252
253

6
Г.6.
Г.7.
Г.8.
Г.9.
Г.10.
Г.11.
Г.12.
Г.13.
Г.14.
Г.15.

Повтор и коррекция предыдущих команд . . . .
Сохранение данных и удаление объектов . . . . .
Внешнее произведение двух матриц . . . . . . . .
c() . . . . . . . . . . . . . . . . . . . . . . . . . . .
Присоединение . . . . . . . . . . . . . . . . . . . .
scan() . . . . . . . . . . . . . . . . . . . . . . . . .
R как набор статистических таблиц . . . . . . . .
Область действия . . . . . . . . . . . . . . . . . .
Настройка окружения . . . . . . . . . . . . . . . .
Графические функции . . . . . . . . . . . . . . . .
Г.15.1. plot() . . . . . . . . . . . . . . . . . . . . .
Г.15.2. Отображение многомерных данных . . . .
Г.15.3. Другие графические функции высокого
уровня . . . . . . . . . . . . . . . . . . . . .
Г.15.4. Параметры функций высокого уровня . . .
Г.15.5. Низкоуровневые графические команды . .
Г.15.6. Математические формулы . . . . . . . . . .
Г.15.7. Интерактивная графика . . . . . . . . . . .
Г.15.8. par() . . . . . . . . . . . . . . . . . . . . . .
Г.15.9. Список графических параметров . . . . . .
Г.15.10. Края рисунка . . . . . . . . . . . . . . . . .
Г.15.11. Составные изображения . . . . . . . . . . .
Г.15.12. Устройства вывода . . . . . . . . . . . . . .
Г.15.13. Несколько устройств вывода одновременно
Г.16. Пакеты . . . . . . . . . . . . . . . . . . . . . . . . .
Г.16.1. Стандартные и сторонние пакеты . . . . .
Г.16.2. Пространство имен пакета . . . . . . . . .

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

254
254
255
256
256
257
258
258
262
263
264
265

.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.

266
267
268
271
271
272
274
277
278
279
280
281
282
282

Приложение Д. Краткий словарь языка R . . . . . . . . . . 284
Приложение Е. Краткий словарь терминов . . . . . . . . . 287
Литература . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 293
Об авторах . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 295

Предисловие
Эта книга написана для тех, кто хочет научиться обрабатывать данные. Такая задача возникает очень часто, особенно тогда, когда нужно
выяснить ранее неизвестный факт. Например: есть ли эффект от нового лекарства? Или: различаются ли рейтинги двух политиков? Или:
как будет меняться курс доллара на следующей неделе?
Многие люди думают, что этот неизвестный факт можно выяснить,
если просто немного подумать над данными. К сожалению, часто это
совершенно не так. Например, по опросу 262 человек, выходящих с избирательных участков, выяснилось, что 52% проголосовало за кандидата А, а 48% — за кандидата Б (естественно, мы упрощаем ситуацию).
Значит ли это, что кандидат А победил? Подумав, многие сначала скажут «Да», а через некоторое время, возможно, «Кто его знает». Но
есть простой (с точки зрения современных компьютерных программ)
«тест пропорций», который позволяет не только ответить на вопрос (в
данном случае «Нет»), но и вычислить, сколько надо было опросить
человек, чтобы можно было бы ответить на такой вопрос. В описанном
случае это примерно 5000 человек (см. объяснение в конце главы про
одномерные данные)!
В общем, если бы люди знали, что можно сделать методами анализа данных, ошибок и неясностей в нашей жизни стало бы гораздо
меньше. К сожалению, ситуация в этой области далека от благополучия. Тем из нас, кто заканчивал институты, часто читали курс «Теория вероятностей и математическая статистика», однако кроме ужаса
и/или тоски от длинных математических формул, набитых греческими буквами, большинство ничего из этих курсов не помнит. А ведь на
теории вероятностей основаны большинство методов анализа данных!
С другой стороны, ведь совсем не обязательно знать радиофизику для
того, чтобы слушать любимую радиостанцию по радиоприемнику. Значит, для того чтобы анализировать данные в практических целях, не
обязательно свободно владеть математической статистикой и теорией
вероятностей. Эту проблему давно уже почувствовали многие английские и американские авторы — названиями типа «Статистика без слез»
пестрят книжные полки магазинов, посвященные книгам по анализу
данных.
Тут, правда, следует быть осторожным как авторам, так и читателям таких книг: многие методы анализа данных имеют, если можно так

8

Предисловие

выразиться, двойное дно. Их (эти методы) можно применять, глубоко
не вникая в сущность используемой там математики, получать результаты и обсуждать эти результаты в отчетах. Однако в один далеко
не прекрасный день может выясниться, что данный метод совершенно не подходил для ваших данных, и поэтому полученные результаты
и результатами-то назвать нельзя... В общем, будьте бдительны, внимательно читайте про все ограничения методов анализа, а при чтении
примеров досконально сравнивайте их со своими данными.
Про примеры: мы постарались привести как можно больше примеров, как простых, так и сложных, и по возможности из разных областей жизни, поскольку читателями этой книги могут быть люди самых
разных профессий. Еще мы попробовали снизить объем теоретического материала, потому что мы знаем — очень многие учатся только
на примерах. Поскольку книга посвящена такой компьютерной программе, которая «работает на текстовом коде», логично было поместить эти самые коды в текстовый файл, а сам файл сделать общедоступным. Так мы и поступили — приведенные в книге примеры можно найти на веб-странице по адресу http://ashipunov.info/shipunov/
software/r/. Там же находятся разные полезные ссылки и те файлы
данных, которые не поставляются вместе с программой.
О структуре книги: первая глава, по сути, целиком теоретическая.
Если лень читать общие рассуждения, можно сразу переходить ко второй главе. Однако в первой главе есть много такой информации, которая позволит в будущем не «наступать на грабли». В общем, решайте
сами. Во второй главе самые важные — разделы, начиная с «Как скачать и установить R», в которых объясняется, как работать с программой R. Если не усвоить этих разделов, все остальное чтение будет почти
бесполезным. Советуем внимательно прочитать и обязательно проработать все примеры из этого раздела. Последующие главы составляют
ядро книги, там рассказывается про самые распространенные методы
анализа данных. Глава «Статистическая разведка», в которой обсуждается общий порядок статистического анализа, подытоживает книгу; в
ней еще раз рассказывается про методы, обсуждавшиеся в предыдущих
главах. В приложениях к книге содержится много полезной информации: там рассказано о графических интерфейсах к R, приведен простой
практический пример работы, описаны основы программирования в R,
приведены выдержки из перевода официальной документации. По сути,
каждое приложение — это отдельный небольшой справочник, который
можно использовать более или менее независимо от остальной книги.
Конечно, множество статистических методов, в том числе и довольно популярных, в книгу не вошли. Мы почти не касаемся статистических моделей, ничего не пишем о контрастах, не рассказываем о стандартных распределениях (за исключением нормального), эффектах, кри-

Предисловие

9

вых выживания, байесовых методах, факторном анализе, геостатистике, не объясняем, как делать многофакторный и блочный дисперсионный анализ, планировать эксперимент и т. д., и т. п. Наша цель — научить основам статистического анализа. А если читатель хорошо освоит основы, то любой продвинутый метод он сможет одолеть без особого
труда, опираясь на литературу, встроенную справку и Интернет.
Несколько технических замечаний: все десятичные дроби в книге
представлены в виде чисел с разделителем-точкой (типа 10.4), а не запятой (типа 10,4). Это сделано потому, что программа R по умолчанию
«понимает» только первый вариант дробей. И еще: многие приведенные
в книге примеры можно (и нужно!) повторить самостоятельно. Такие
примеры напечатаны машинописным шрифтом и начинаются со значка
«больше» — «>». Если пример не умещается на одной строке, все последующие его строки начинаются со знака «плюс» — «+» (не набирайте
эти знаки, когда будете выполнять примеры!). Если в книге идет речь
о загрузке файлов данных, то предполагается, что все они находятся
в поддиректории data в текущей директории. Если вы будете скачивать файлы данных с упомянутого выше сайта, не забудьте создать эту
поддиректорию и скопировать туда файлы данных.

Глава 1
Что такое данные и зачем их
обрабатывать?
В этой главе рассказывается о самых общих понятиях анализа данных.

1.1. Откуда берутся данные
«Без пруда не выловишь и рыбку из него»,— говорит народная мудрость. Действительно, если хочешь анализировать данные, надо их сначала получить. Способов получения данных много, а самые главные —
наблюдения и эксперименты.
Наблюдением будем называть такой способ получения данных, при
котором воздействие наблюдателя на наблюдаемый объект сведено к
минимуму. Эксперимент тоже включает наблюдение, но сначала на
наблюдаемый объект оказывается заранее рассчитанное воздействие.
Для наблюдения очень важно это «сведение воздействия к минимуму».
Если этого не сделать, мы получим данные, отражающие не свойства
объекта, а его реакцию на наше воздействие.
Вот, например, встала задача исследовать, чем питается какое-то
редкое животное. Оптимальная стратегия наблюдения здесь состоит в
установке скрытых камер во всех местах, где это животное обитает.
После этого останется только обработать снятое, чтобы определить вид
пищи. Очень часто, однако, оптимальное решение совершенно невыполнимо, и тогда пытаются обойтись, скажем, наблюдением за животным
в зоопарке. Ясно, что в последнем случае на объект оказывается воздействие, и немалое. В самом деле, животное поймали, привезли в совершенно нетипичные для него условия, да и корм, скорее всего, будет
непохож на тот, каким оно питалось на родине. В общем, если наблюдения в зоопарке поставлены грамотно, то выяснено будет не то, чем
вообще питается данное животное, а то, чем оно питается при содержании в определенном зоопарке. К сожалению, многие (и исследователи,
и те, кто потом читает их отчеты) часто не видят разницы между этими
двумя утверждениями.

Откуда берутся данные

11

Вернемся к примеру из предисловия. Предположим, мы опрашиваем выходящих с избирательных участков. Часть людей, конечно, вообще окажется отвечать. Часть ответит что-нибудь, не относящееся к
делу. Часть вполне может намеренно или случайно исказить свой ответ.
Часть ответит правду. И все это серьезным образом зависит от наблюдателя — человека, проводящего опрос.
Даже упомянутые выше скрытые камеры приведут к определенному воздействию: они же скрытые, но не невидимые и невесомые. Нет
никакой гарантии, что наше животное или его добыча не отреагирует
на них. А кто будет ставить камеры? Если это люди, то чем больше камер поставить, тем сильнее будет воздействие на окружающую среду.
Сбрасывать с вертолета? Надеемся, что вам понятно, к чему это может
привести.
В общем, из сказанного должно быть понятно, что наблюдение «в
чистом виде» более или менее неосуществимо, поскольку всегда будет
внесено какое-нибудь воздействие. Поэтому для того, чтобы адекватно
работать с данными наблюдений, надо всегда четко представлять, как
они проводились. Если воздействие было значительным, то надо представлять (хотя бы теоретически), какие оно могло повлечь изменения,
а в отчете обязательно указать на те ограничения, которые были вызваны способом наблюдения. Не следует без необходимости применять
экстраполяцию: если мы увидели, что А делает Б, нельзя писать «А
всегда делает Б» и даже «А обычно делает Б». Можно лишь писать
нечто вроде «в наших наблюдениях А делал Б, это позволяет с некоторой вероятностью предположить, что он может делать Б».
У эксперимента свои проблемы. Наиболее общие из них — это точный учет воздействия и наличие контроля. Например, мы исследуем
действие нового лекарства. Классический эксперимент состоит в том,
что выбираются две группы больных (как выбрать такие группы, сколько должно быть человек и прочее, рассмотрено дальше). Всем больным
сообщают, что проводится исследование нового лекарства, но его дают
только больным первой группы, остальные получают так называемое
плацебо, внешне неотличимое от настоящего лекарства, но не содержащее ничего лекарственного. Зачем это делается? Дело в том, что если
больной будет знать, что ему дают «ненастоящее» лекарство, то это
скажется на эффективности лечения, потому что результат зависит не
только от того, что больной пьет, но и от того, что он чувствует. Иными словами, психологическое состояние больного — это дополнительный фактор воздействия, от которого в эксперименте лучше избавиться. Очень часто не только больным, но и их врачам не сообщают, кому
дают плацебо, а кому — настоящее лекарство (двойной слепой метод).
Это позволяет гарантировать, что и психологическое состояние врача
не повлияет на исход лечения.

12

Что такое данные и зачем их обрабатывать?

Группа, которой дают плацебо (она называется контроль), нужна
для того, чтобы отделить эффект, который может произвести лекарство, от эффекта какого-нибудь постороннего внешнего фактора. Известно, например, что уменьшение длины светового дня осенью и зимой провоцирует многие нервные заболевания. Если наше исследование
придется на это время и у нас не будет контроля, то увеличение частоты заболеваний мы вполне можем принять за результат применения
лекарства.

1.2. Генеральная совокупность и выборка
«Статистика знает все»,— писали Ильф и Петров в «Двенадцати
стульях», имея в виду то, что обычно называют статистикой,— сбор
всевозможной информации обо всем на свете. Чем полнее собрана информация, тем, как считается, лучше. Однако лучше ли?
Возьмем простой пример. Допустим, фирма-производитель решила
выяснить, какой из двух сортов производимого мороженого предпочитают покупатели. Проблем бы не было, если бы все мороженое продавалось в одном магазине. На самом же деле продавцов очень много: это
оптовые рынки и гипермаркеты, средние и малые магазины, киоски,
отдельные мороженщики с тележками, те, кто торгует в пригородных
поездах, и т. п. Можно попробовать учесть доход от продажи каждого из
0
0
двух сортов. Если они стоят одинаково, то большая сумма дохода должна отразить больший спрос. Представим, однако, что спрос одинаков,
но по каким-то причинам мороженое первого сорта тает быстрее. Тогда
потерь при его транспортировке будет в среднем больше, продавцы будут покупать его несколько чаще, и получится, что доход от продажи
первого сорта будет несколько выше, чем от второго. Это рассуждение,
конечно, упрощает реальную ситуацию, но подумайте, сколько других
неучтенных факторов стоит на пути такого способа подсчета! Анализ
товарных чеков получше, однако многие конечные продавцы таких чеков не имеют и поэтому в анализ не попадут. А нам-то необходимо как
раз учесть спрос покупателей, а не промежуточных продавцов.
Можно поступить иначе — раздать всем конечным продавцам анкеты, в которых попросить указать, сколько какого мороженого продано;
а чтобы анкеты были обязательно заполнены, вести с этими продавцами
дела только при наличии заполненных анкет. Только ведь никто не будет контролировать, как продавцы заполняют анкеты... Вот и получит
фирма большую, подробную сводную таблицу о продажах мороженого,
которая ровным счетом ничего отражать не будет.
Как же поступить? Здесь на помощь приходит идея выборочных исследований. Всех продавцов не проконтролируешь, но ведь нескольких-

Как получать данные

13

то можно! Надо выбрать из общего множества несколько торговых точек (как выбирать — это особая наука, об этом ниже) и проконтролировать тамошние продажи силами самой фирмы или такими нанятыми людьми, которым можно доверять. В итоге мы получим результат,
который является частью общей картины. Теперь самый главный вопрос: можно ли этот результат распространить на всю совокупность
продаж? Оказывается, можно, поскольку на основе теории вероятностей уже много лет назад была создана теория выборочных исследований. Ее-то и называют чаще всего математической статистикой, или
просто статистикой.
Пример с мороженым показывает важную вещь: выборочные исследования могут быть (и часто бывают) значительно более точными (в
смысле соответствия реальности), чем сплошные.
Еще один хороший пример на эту же тему есть в результатах сплошной переписи населения России 1897 г. Если рассмотреть численность
населения по возрастам, то получается, что максимальные численности (пики) имеют возрасты, кратные 5 и в особенности кратные 10.
0
Понятно, как это получилось. Большая часть населения в те времена была неграмотна и свой возраст помнила только приблизительно, с
точностью до пяти или до десяти лет. Чтобы все-таки узнать, каково
было распределение по возрастам на самом деле, нужно не увеличивать
объем данных, а наоборот, создать выборку из нескольких процентов
населения и провести комплексное исследование, основанное на перекрестном анализе нескольких источников: документов, свидетельств и
личных показаний. Это даст гораздо более точную картину, нежели
сплошная перепись.
Естественно, сам процесс создания выборки может являться источником ошибок. Их принято называть «ошибками репрезентативности».
Однако правильная организация выборки позволяет их избежать. А
поскольку с выборкой можно проводить гораздо более сложные исследования, чем со всеми данными (их называют генеральной совокупностью, или популяцией), те ошибки (ошибки точности), которые возникают при сплошном исследовании, в выборочном исследовании можно
исключить.

1.3. Как получать данные
В предыдущих разделах неоднократно упоминалось, что от правильного подбора выборки серьезным образом будет зависеть качество
получаемых данных. Собственно говоря, есть два основных принципа
составления выборки: повторность и рандомизация. Повторности нужны для того, чтобы быть более уверенными в полученных результатах,

14

Что такое данные и зачем их обрабатывать?

а рандомизация — для того, чтобы избежать отклонений, вызванных
посторонними причинами.
Принцип повторностей предполагает, что один и тот же эффект
будет исследован несколько раз. Собственно говоря, для этого мы в
предыдущих примерах опрашивали множество избирателей, ловили
в заповеднике много животных, подбирали группы из нескольких десятков больных и контролировали различных продавцов мороженого.
Нужда в повторностях возникает оттого, что все объекты (даже только
что изготовленные на фабрике изделия) пусть в мелочах, но отличаются друг от друга. Эти отличия способны затуманить общую картину, если мы станем изучать объекты поодиночке. И наоборот, если мы берем
несколько объектов сразу, их различия часто «взаимно уничтожаются».
Не стоит думать, что создать повторности — простое дело. К сожалению, часто именно небрежное отношение к повторностям сводит
на нет результаты вроде бы безупречных исследований. Главное правило — повторности должны быть независимы друг от друга. Это
значит, например, что нельзя в качестве повторностей рассматривать
данные, полученные в последовательные промежутки времени с одного
и того же объекта или с одного и того же места. Предположим, что мы
хотим определить размер лягушек какого-то вида. Для этого с интервалом в 15 минут (чтобы лягушки успокоились) ловим сачком по одной лягушке. Как только наберется двадцать лягушек, мы их меряем и
вычисляем средний размер. Однако такое исследование не будет удовлетворять правилу независимости, потому что каждый отлов окажет
влияние на последующее поведение лягушек (например, к концу лова
будут попадаться самые смелые, или, наоборот, самые глупые). Еще
хуже использовать в качестве повторностей последовательные наблюдения за объектом. Например, в некотором опыте выясняли скорость
зрительной реакции, показывая человеку на доли секунды предмет, а
затем спрашивая, что это было. Всего исследовали 10 человек, причем
каждому показывали предмет пять раз. Авторы опыта посчитали, что
у них было 50 повторностей, однако на самом деле — только десять. Это
произошло потому, что каждый следующий показ не был независим от
предыдущего (человек мог, например, научиться лучше распознавать
предмет).
Надо быть осторожным не только с данными, собранными в последовательные промежутки времени, но и просто с данными, собранными
с одного и того же места. Например, если мы определяем качество телевизоров, сходящих с конвейера, не годится в качестве выборки брать
несколько штук подряд — с большой вероятностью они изготовлены
в более близких условиях, чем телевизоры, взятые порознь, и, стало
быть, их характеристики не независимы друг от друга.

Как получать данные

15

Второй важный вопрос о повторностях: сколько надо собрать данных. Есть громадная литература по этому поводу, но ответа, в общем, два: (1) чем больше, тем лучше и (2) 30. Выглядящее несколько
юмористически «правило 30» освящено десятилетиями опытной работы. Считается, что выборки, меньшие 30, следует называть малыми,
0
а большие — большими. Отсюда то значение, которое придают числу
тридцать в анализе данных. Бывает так, что и тридцати собрать нельзя,
0
однако огорчаться этому не стоит, поскольку многие процедуры анализа данных способны работать с очень малыми выборками, в том числе
из пяти и даже из трех повторностей. Следует, однако, иметь в виду,
что чем меньше повторностей, тем менее надежными будут выводы.
Существуют, кроме того, специальные методы, которые позволяют посчитать, сколько надо собрать данных, для того чтобы с определенной
вероятностью высказать некоторое утверждение. Это так называемые
«тесты мощности» (см. пример в главе про одномерные данные).
Рандомизация — еще одно условие создания выборки, и также «с
подвохом». Каждый объект генеральной совокупности должен иметь
равные шансы попасть в выборку. Очень часто исследователи полагают, что выбрали свои объекты случайно (проделали рандомизацию), в
то время как на самом деле их материал был подобран иначе. Предположим, нам поручено случайным образом отобрать сто деревьев в лесу,
чтобы впоследствии померить степень накопления тяжелых металлов в
листьях. Как мы будем выбирать деревья? Если просто ходить по лесу и собирать листья с разных деревьев, с большой вероятностью они
не будут собранными случайно, потому что вольно или невольно мы
будем собирать листья, чем-то привлекшие внимание (необычностью,
окраской, доступностью). Этот метод, стало быть, не годится. Возьмем
метод посложнее — для этого нужна карта леса с размеченными координатами. Мы выбираем случайным образом два числа, например 123 м
к западу и 15 м к югу от точки, находящейся примерно посередине леса, затем высчитываем это расстояние на местности и выбираем дерево,
которое ближе всего к нужному месту. Будет ли такое дерево выбрано
случайно? Оказывается, нет. Ведь деревья разных пород растут неодинаково, поэтому у деревьев, растущих теснее (например, у елок), шанс
быть выбранными окажется больше, чем у разреженно растущих дубов. Важным условием рандомизации, таким образом, является то, что
каждый объект должен иметь абсолютно те же самые шансы быть
выбранным, что и все прочие объекты.
Как же быть? Надо просто перенумеровать все деревья, а затем выбрать сто номеров по жребию. Но это только звучит просто, а попробуйте так сделать! А если надо сравнить 20 различных лесов?.. В общем,
требование рандомизации часто оборачивается весьма серьезными затратами на проведение исследования. Естественно поэтому, что нередко

16

Что такое данные и зачем их обрабатывать?

рандомизацию осуществляют лишь частично. Например, в нашем случае можно случайно выбрать направление, протянуть в этом направлении бечевку через весь лес, а затем посчитать, скольких деревьев
касается бечевка, и выбрать каждое энное (пятое, пятнадцатое...) дерево, так чтобы всего в выборке оказалось 100 деревьев. Заметьте, что в
данном случае метод рандомизации состоит в том, чтобы внести в исследуемую среду такой порядок, которого там заведомо нет. Конечно,
у этого последнего метода есть недостатки, а какие — попробуйте догадаться сами (ответ см. в конце главы).
Теперь вы знаете достаточно, чтобы ответить на еще один вопрос.
В одной лаборатории изучали эффективность действия ядохимикатов
на жуков-долгоносиков (их еще называют «слоники»). Для этого химикат наносили на фильтровальную бумагу, а бумагу помещали в стеклянную чашку с крышкой (чашку Петри). Жуков выбирали из банки,
в которой их разводили для опытов, очень простым способом: банку с
жуками открывали, и первого выползшего на край жука пересаживали
в чашку с ядохимикатом. Затем засекали, сколько пройдет времени от
посадки жука в банку до его гибели. Потом брали другого жука и так
повторяли 30 раз. Потом меняли ядохимикат и начинали опыт сначала. Но однажды один умный человек заметил, что в этом эксперименте
самым сильным всегда оказывался тот химикат, который был взят для
исследования первым. Как вы думаете, в чем тут дело? Какие нарушения принципов повторности и рандомизации были допущены? Как
надо было поставить этот опыт? (См. ответ в конце главы).
Для рандомизации, конечно, существует предел. Если мы хотим выяснить возрастной состав посетителей какого-то магазина, не нужно во
имя рандомизации опрашивать прохожих с улицы. Нужно четко представлять себе генеральную совокупность, с которой идет работа, и не
выходить за ее границы. Помните пример с питанием животного? Если генеральная совокупность — это животные данного вида, содержащиеся в зоопарках, нет смысла добавлять к исследованию данные о
питании этих животных в домашних условиях. Если же такие данные
просто необходимо добавить (например, потому что данных из зоопарков очень мало), то тогда генеральная совокупность будет называться
«множество животных данного вида, содержащихся в неволе».
Интересный вариант рандомизации используют, когда в эксперименте исследуются одновременно несколько взаимодействий. Например, мы хотим выяснить эффективность разных типов средств против
обледенения тротуаров. Для этого логично выбрать (случайным образом) несколько разных (по возрасту застройки, плотности населения,
расположению) участков города и внутри каждого участка случайным
образом распределить разные типы этих средств. Потом можно, например, фиксировать (в баллах или как-нибудь еще) состояние тротуаров

Что ищут в данных

17

каждый день после нанесения средства, можно также повторить опыт
при разной погоде. Такой подход называется «блочный дизайн». Блоками здесь являются разные участки города, а повторность обеспечивается тем, что в каждом блоке повторяются одни и те же воздействия.
При этом даже не обязательно повторять однотипные воздействия по
нескольку раз внутри блоков, важно выбрать побольше отличающихся друг от друга блоков. Можно считать разными блоками и разные
погодные условия, и тогда у нас получится «вложенный блочный дизайн»: в каждый погодный блок войдет несколько «городских» блоков,
и уже внутри этих блоков будут повторены все возможные воздействия
(типы средств).
В области рандомизации лежит еще одно коренное различие между
наблюдением и экспериментом. Допустим, мы изучаем эффективность
действия какого-то лекарства. Вместо того, чтобы подбирать две группы больных, использовать плацебо и т. п., можно просто порыться в
архивах и подобрать соответствующие примеры (30 случаев применения лекарства и 30 случаев неприменения), а затем проанализировать
разницу между группами (например, число смертей в первый год после
окончания лечения). Однако такой подход сопряжен с опасностью того,
что на наши выводы окажет влияние какой-то (или какие-то) неучтенный фактор, выяснить наличие которого из архивов невозможно. Мы
просто не можем гарантировать, что соблюдали рандомизацию, анализируя архивные данные. К примеру, первая группа (случайно!) окажется состоящей почти целиком из пожилых людей, а вторая — из людей
среднего возраста. Ясно, что это окажет воздействие на выводы. Поэтому в общем случае эксперимент всегда предпочтительней наблюдения.

1.4. Что ищут в данных
Прочитав предыдущие разделы, читатель, наверное, уже не раз задавался вопросом: «Если так все сложно, зачем он вообще, этот анализ
данных? Неужели и так не видно, что в один магазин ходит больше
народу, одно лекарство лучше другого и т. п.?» В общем, так бывает
видно довольно часто, но обычно тогда, когда либо (1) данных и/или
исследуемых факторов очень мало, либо (2) разница между ними очень
резка. В этих случаях действительно запускать всю громоздкую маши0
ну анализа данных не стоит. Однако гораздо чаще встречаются случаи,
когда названные выше условия не выполняются. Давно, например, доказано, что средний человек может одновременно удержать в памяти
лишь 5–9 объектов. Стало быть, анализировать в уме данные, которые
насчитывают больше 10 компонентов, уже нельзя. А значит, не обой-

18

Что такое данные и зачем их обрабатывать?

тись без каких-нибудь, пусть и самых примитивных (типа вычисления
процентов и средних величин), методов анализа данных.
Бывает и так, что внешне очевидные результаты не имеют под собой
настоящего основания. Вот, например, одно из исследований насекомыхвредителей. Агрономы определяли, насколько сильно вредят кукурузе
гусеницы кукурузного мотылька. Получились вполне приемлемые результаты: разница в урожае между пораженными и непораженными
растениями почти вдвое. Казалось, что и обрабатывать ничего не надо — «и так все ясно». Однако нашелся вдумчивый исследователь, который заметил, что пораженные растения, различающиеся по степени
поражения, не различаются по урожайности. Здесь, очевидно, что-то
не так: если гусеницы вредят растению, то чем сильнее они вредят, тем
меньше должен быть урожай. Стало быть, на какой-то стадии исследования произошла ошибка. Скорее всего, дело было так: для того чтобы
измерять урожайность, среди здоровых растений отбирали самые здоровые (во всех смыслах этого слова), ну а среди больных старались
подобрать самые хилые. Вот эта ошибка репрезентативности и привела
к тому, что возникли такие «хорошие» результаты. Обратите внимание, что только анализ взаимосвязи «поражение—урожай» (на языке
анализа данных он называется «регрессионный анализ», см. главу про
двумерные данные) привел к выяснению истинной причины. А кукурузный мотылек, оказывается, почти и не вредит кукурузе...
Итак, анализ данных необходим всегда, когда результат неочевиден,
и часто даже тогда, когда он кажется очевидным. Теперь разберемся,
к каким последствиям может привести анализ, что он умеет.
1. Во-первых, анализ данных умеет давать общие характеристики
для больших выборок. Эти характеристики могут отражать так
называемую центральную тенденцию, то есть число (или ряд чисел), вокруг которых, как пули вокруг десятки в мишени, «разбросаны» данные. Всем известно, как считать среднее значение,
но существует еще немало полезных характеристик «на ту же тему». Другая характеристика — это разброс, который отражает не
вокруг чего «разбросаны» данные, а насколько сильно они разбросаны.
2. Во-вторых, можно проводить сравнения между разными выборками. Например, можно выяснить, в какой из групп больных инфарктом миокарда частота смертей в первый год после лечения
выше — у тех, к кому применяли коронарное шунтирование, или у
тех, к кому применяли только медикаментозные способы лечения.
«На взгляд» этой разницы может и не быть, а если она и есть, то
где гарантия того, что эти различия не вызваны случайными при-

Что ищут в данных

19

чинами, не имеющими отношения к лечению? Скажем, заболел
человек острым аппендицитом и умер после операции: к лечению
инфаркта это может не иметь никакого отношения. Сравнение
данных при помощи статистических тестов позволяет выяснить, насколько велика вероятность, что различия между группами вызваны случайными причинами. Заметьте, что гарантий
анализ данных тоже не дает, зато позволяет оценить (численным
образом) шансы. Анализ данных позволяет оценить и упомянутые
выше общие характеристики.
3. Третий тип результата, который можно получить, анализируя данные,— это сведения о взаимосвязях. Изучение взаимосвязей — наверное, самый серьезный и самый развитый раздел анализа данных. Существует множество методик выяснения и, главное, проверки «качества» связей. В дальнейшем нам понадобятся сведения о том, какие бывают взаимосвязи. Есть так называемые соответствия, например когда два явления чаще встречаются вместе,
нежели по отдельности (как гром и молния). Соответствия нетрудно найти, но силу их измерить трудно. Следующий тип взаимосвязей — это корреляции. Корреляции показывают силу взаимосвязи, но не могут определить ее направления. Другими словами,
если выяснилась корреляция между качанием деревьев и ветром,
то нельзя решить, дует ли ветер оттого, что деревья качаются,
или наоборот. Наконец, есть зависимости, для которых можно
измерить и силу, и направление, и оценить, насколько вероятно то, что они — результат случайных причин. Кстати говоря,
последнее можно, как водится в анализе данных, сделать и для
корреляций, и даже для соответствий. Еще одно свойство зависимостей состоит в том, что можно предсказать, как будет «вести»
себя зависимая переменная в каких-нибудь до сих пор не опробованных условиях. Например, можно прогнозировать колебания
спроса, устойчивость балок при землетрясении, интенсивность поступления больных и т. п.
4. И наконец, анализ данных можно использовать для установления структуры. Это самый сложный тип анализа, поскольку для
выяснения структуры обычно используются сразу несколько характеристик. Есть и специальное название для такой работы —
«многомерная статистика». Самое главное, на что способен многомерный анализ,— это создание и проверка качества классификации объектов. В умелых руках хорошая классификация очень
полезна. Вот, например, мебельной фабрике потребовалось выяснить, какую мебель как лучше перевозить: в разобранном или
в собранном виде. Рекомендации по перевозке зависят от уймы

20

Что такое данные и зачем их обрабатывать?

причин (сложность сборки, хрупкость, стоимость, наличие стеклянных частей, наличие ящиков и полок и т. д.). Одновременно
оценить эти факторы может лишь очень умелый человек. Однако
существуют методы анализа, которые с легкостью разделят мебель на две группы, а заодно и проверят качество классификации,
например ее соответствие сложившейся практике перевозок.
Существует и другой подход к результатам анализа данных. В нем
все методы делятся на предсказательные и описательные. К первой
группе методов относится все, что можно статистически оценить, то
есть выяснить, с какой вероятностью может быть верным или неверным наш вывод. Ко второй — методы, которые «просто» сообщают информацию о данных без подтверждения какими-либо вероятностными
методами. В последние годы все для большего числа методов находятся
способы их вероятностнойоценки, и поэтому первая группа все время
увеличивается.
***
Ответ к задаче про случайный выбор деревьев в лесу. В этом
случае шанс быть выбранными у елок выше, чем у дубов. Кроме того,
лес может иметь какую-то структуру именно в выбранном направлении,
и поэтому одной такой «диагонали» будет недостаточно для того, чтобы
отобразить весь лес. Чтобы улучшить данный метод, надо провести
несколько «диагоналей», а расстояния между выбираемыми деревьями
по возможности увеличить.
Ответ к задаче про выбор жуков. Дело в том, что первыми вылезают самые активные особи, а чем активнее особь, тем быстрее она
набирает на лапки смертельную дозу ядохимиката и, стало быть, быстрее гибнет. Это и было нарушением принципа рандомизации. Кроме
того, нарушался принцип повторности: в чашку последовательно сажали жука за жуком, что не могло не повлиять на исход опыта. Для
того чтобы поставить опыт правильно, надо было сначала подготовить
(30 × количество ядохимикатов) чашек, столько же листочков с бумагой, случайным образом распределить ядохимикаты по чашкам, а затем
перемешать жуков в банке, достать соответствующее количество и рассадить по чашкам.

Глава 2
Как обрабатывать данные
В принципе, обрабатывать данные можно и без компьютера. Так
поступали в те годы, когда компьютерная техника была недоступна.
Однако многие статистические расчеты настолько тяжеловесны, что
уже в XIX веке стали придумывать способы их автоматизации.

2.1. Неспециализированные программы
Почти в любом компьютере с предустановленной системой есть программа-калькулятор. Такая программа обычно умеет выполнять четыре арифметических действия, часто — считать квадратные корни и степени, иногда логарифмы. В принципе, этого достаточно для того, чтобы
делать простейшую обработку: считать среднее значение, стандартное
отклонение, некоторые тесты.
Вообще говоря, для того чтобы делать тесты, кроме калькулятора
потребуются еще и статистические таблицы, из которых можно узнать
примерные значения так называемых статистик — величин, характеризующих данные в целом. Таблицы используются потому, что точный (по-английски «exact») подсчет многих статистик слишком сложен, порой даже для продвинутых компьютерных программ, поэтому
используются оценочные («estimated») значения. Статистические таблицы можно найти во многих книгах по классической статистике, они
также «встроены» во многие специализированные программы.
Главный недостаток калькулятора — сложность работы с сериями
чисел, в то время как обычно данные как раз «идут» сериями (колонками, векторами). Чтобы работать с сериями более эффективно (и не
только для этого), были придуманы электронные таблицы. Объяснять,
как они устроены, наверное, не нужно. Сила электронных таблиц прежде всего в том, что они помогают визуализировать данные.
Глядя в электронную таблицу, можно сразу понять, как выглядят
данные в целом, и «на глазок» оценить их основные параметры. Это
очень полезно. Кроме визуализации, программы электронных таблиц
снабжены развитым инструментарием для ввода и преобразования данных — автодополнением, автокопированием, сортировкой и т. п. Однако

22

Как обрабатывать данные

большинство таких программ имеет своеобразное «родимое пятно» —
они создавались в основном для офисного применения и были изначально ориентированы на бухгалтерские задачи. Для обработки данных
нужен гораздо более специализированный инструмент. Конечно, развитые программы электронных таблиц, такие как MS Excel, Gnumeric или
OpenOffice.org Calc, имеют, среди прочего, набор статистических функций. Но поскольку это — не основной компонент, на статистику в этих
программах традиционно обращается мало внимания. Набор статистических тестов невелик, многие методы, особенно многомерные, отсутствуют, реализация (то есть как именно идут внутри программы сами
вычисления) часто далека от оптимальной, нет специализированной системы отчетов, много неудобных ограничений, возможны ошибки, которые будут исправляться не слишком быстро, опять-таки потому, что
статистика — не первоочередная функция электронных таблиц.
Кроме того, базовый принцип электронных таблиц — визуализация
данных — имеет и свои оборотные стороны. Что, если данные не помещаются в окне? В этом случае их надо будет прокручивать или скрывать часть ячеек. И то, и другое оказывает пользователю медвежью
услугу, потому что он рассчитывал на облегчение восприятия данных,
в то время как программа, скорее, затрудняет его. Или другой пример —
надо провести операции с тремя несмежными столбцами. Сделать это
через выделение нельзя, потому что выделение всегда одного типа (да
и буфер обмена очень часто всего один), приходится делать много движений мышкой с большим риском ошибиться. И уж совсем никуда не
годится графическая система, если надо сочетать методы обработки
каким-нибудь сложным образом.
Выход — в пользовании специализированными статистическими программами.

2.2. Специализированные статистические
программы
2.2.1. Оконно-кнопочные системы
Есть две группы специализированных статистических программ. Первые не особенно отличаются внешне от электронных таблиц, однако
снабжены значительно большим арсеналом доступных статистических
приемов. Кроме того, у них традиционно мощная графическая часть
(возможных графиков больше, и управление ими более гибкое), а часто и подсистема подготовки отчетов. Многие такие программы имеют
значительно меньше ограничений, чем электронные таблицы.

Специализированные статистические программы

23

Очень распространена в России относящаяся к этой группе система
STATISTICA. Как сказано выше, она отличается мощной графической
частью, то есть имеет множество возможных вариантов графического
вывода, которые при этом довольно гибко настраиваются, так что количество «стандартных» графиков можно смело увеличить в несколько
раз. Другим, и очень серьезным, преимуществом STATISTICA является наличие переведенной на русский язык системы помощи, свободно
доступной в Интернете. Эта система может служить заодно и руководством по статистике, поскольку там освещены и общие вопросы. Издано
немало книг, посвященных STATISTICA. Надо, однако, заметить, что
популярным этот пакет является в основном только в России. Весьма
редко можно встретить ссылки на обработку данных этой системой в
статьях из ведущих научных (в основном англоязычных) журналов, и
это несмотря на то, что система делается в Америке. Поэтому легко
можно представить проблемы с обменом данными. Кроме того, как и
у всех визуальных систем, однажды проведенное исследование нелегко
повторить, если, скажем, появились новые данные. Гибкость STATISTICA велика, но только в пределах так называемых модулей. Если надо скомбинировать работу нескольких модулей, то придется отойти от
графического подхода — например, начать писать макросы. Алгоритмы
вычисления в STATISTICA, естественно, закрыты, поэтому иногда приходится проводить целое исследование, чтобы выяснить, что на самом
деле в данном случае делает программа. К тому же к системе в свое время было немало претензий по поводу «быстрых и грязных» алгоритмов
работы, и есть подозрение, что ситуация не слишком изменилась.
Другой программой, популярной в свое время на российском рынке, является система STADIA. Написанная русскоязычными авторами,
она отличается продуманным интерфейсом и очень хорошей системой
помощи. К сожалению, это тоже закрытая программа. Немного похожа на STADIA программа PAST. Изначально она предназначалась для
специализированной обработки данных в геологии, но затем функции
значительно расширились, и в сейчас в PAST представлены практически все широко распространенные средства анализа данных. Графическая часть PAST небогата, но достаточна для базового исследования.
Следует отметить, что, в отличие от двух предыдущих программ, PAST
распространяется бесплатно.
SPSS и MiniTab широко используются за рубежом, однако в России
эти системы не слишком распространены. Общий интерфейс их схож со
STATISTICA, хотя имеется и множество своих особенностей, например
в подсистемах генерации отчетов. Нужно упомянуть также StatGraphics, который был доступен в России еще со времен господства MSDOS, а в настоящее время приобрел развитый графический интерфейс
и стал похож на остальные программы этой группы.

24

Как обрабатывать данные

2.2.2. Статистические среды
Эта группа программ использует в основном интерфейс командной
строки. Пользователь вводит команды, система на них отвечает. Звучит это просто, однако сами эти программы — одни из самых сложных
систем обработки. Вообще говоря, командный интерфейс имеет немало
недостатков. Например, пользователь лишен возможности выбрать тип
обработки из списка (меню), вместо этого он должен помнить, какие
типы обработки доступны. Кроме того, ввод команд схож (а иногда и
неотличим) от «настоящего» программирования, так что для работы
с подобными системами нужны некоторые навыки программиста (или
достаточно смелости, для того чтобы эти навыки приобрести по ходу
дела). Зато пользователь получает полный контроль над системой: он
может комбинировать любые типы анализа, записывать процедуры в
скрипты, которые можно запустить в любое время, модифицировать
вывод графиков, сохранять их в любые графические форматы, легко
писать расширения для системы, а если она к тому же еще имеет открытый код, то и модифицировать саму систему (или, по крайней мере,
легко выяснять, как именно работают вычислительные алгоритмы).
Одна из наиболее продвинутых систем этого плана — это SAS. Это
коммерческая, очень мощная система, обладающая развитой системой
помощи и имеющая долгую историю развития. Создавалась она для научной и экономической обработки данных и до сих пор является одним
из лидеров в этом направлении. Написано множество книг, описывающих работу с SAS и некоторые ее алгоритмы. Вместе с тем система
сохраняет множество рудиментов 70-х годов, и пользоваться ей поначалу не очень легко даже человеку, знакомому с командной строкой
и программированием. А стоимость самой системы просто запредельная — многие тысячи долларов!

2.3. Из истории S и R
R — это среда для статистических расчетов. R задумывался как свободный аналог среды S-Plus, которая, в свою очередь, является коммерческой реализацией языка расчетов S. Язык S — довольно старая
разработка. Он возник еще в 1976 году в компании Bell Labs и был
назван, естественно, «по мотивам» языка С. Первая реализация S была написана на FORTRAN и работала под управлением операционной
системы GCOS. В 1980 г. реализация была переписана под UNIX, и
с этого момента S стал распространяться в основном в научной среде.
Начиная с третьей версии (1988 г.), коммерческая реализация S называется S-Plus. Последняя распространялась компанией Insightful, а сейчас
распространяется компанией TIBCO Software. Версии S-Plus доступны

Применение, преимущества и недостатки R

25

под Windows и различные версии UNIX — естественно, за плату, причем весьма и весьма немаленькую (версия для UNIX стоит порядка
$6500). Собственно, высокая цена и сдерживала широкое распространение этого во многих отношениях замечательного продукта. Тут-то и
начинается история R.
В августе 1993 г. двое молодых новозеландских ученых анонсировали свою новую разработку, которую они назвали R (буква «R» была
выбрана просто потому, что она стоит перед «S», тут есть аналогия с
языком программирования C, которому предшествовал язык B). По замыслу создателей (это были Robert Gentleman и Ross Ihaka), это должна
была быть новая реализация языка S, отличающаяся от S-Plus некоторыми деталями, например обращением с глобальными и локальными
переменными, а также работой с памятью. Фактически они создали не
аналог S-Plus, а новую «ветку» на «дереве S» (многие вещи, которые отличают R от S-Plus, связаны с влиянием языка Scheme). Проект вначале развивался довольно медленно, но когда в нем появилось достаточно
возможностей, в том числе уникальная по легкости система написания
дополнений (пакетов), все большее количество людей стало переходить
на R с S-Plus. Когда же, наконец, были устранены свойственные первым
версиям проблемы работы с памятью, на R стали переходить и «любители» других статистических пакетов (прежде всего тех, которые имеют
интерфейс командной строки: SAS, Stata, SYSTAT). Количество книг,
написанных про R, за последние годы выросло в несколько раз, а количество пакетов уже приближается к трем с половиной тысячам!

2.4. Применение, преимущества и недостатки R
Коротко говоря, R применяется везде, где нужна работа с данными.
Это не только статистика в узком смысле слова, но и «первичный» анализ (графики, таблицы сопряженности) и продвинутое математическое
моделирование. В принципе, R может использоваться и там, где в настоящее время принято использовать специализированные программы
математического анализа, такие как MATLAB или Octave. Но, разумеется, более всего его применяют для статистического анализа — от вы0
числения средних величин до вейвлет-преобразований и временных рядов. Географически R распространен тоже очень широко. Трудно найти
американский или западноевропейский университет, где бы не работали
с R. Очень многие серьезные компании (скажем, Boeing) устанавливают
R для работы.
У R два главных преимущества: неимоверная гибкость и свободный
код. Гибкость позволяет создавать приложения (пакеты) практически
на любой случай жизни. Нет, кажется, ни одного метода современно-

26

Как обрабатывать данные

го статистического анализа, который бы не был сейчас представлен в
R. Свободный код — это не просто бесплатность программы (хотя в
сравнении с коммерческими пакетами, продающимися за совершенно
безумные деньги, это, конечно, преимущество, да еще какое!), но и возможность разобраться, как именно происходит анализ, а если в коде
встретилась ошибка — самостоятельно исправить ее и сделать исправление доступным для всех.
У R есть и немало недостатков. Самый главный из них — это трудность обучения программе. Команд много, вводить их надо вручную,
запомнить все трудно, а привычной системы меню нет. Поэтому порой
очень трудно найти, как именно сделать какой-нибудь анализ. Если
функция известна, то узнать, что она делает, очень легко, обычно достаточно набрать команду help(название функции). Увидеть код функции тоже легко, для этого надо просто набрать ее название без скобок
или (лучше) ввести команду getAnywhere(название функции). А вот
что делать, если «задали» провести, скажем, дисперсионный анализ, а
функция неизвестна? (См. ответ в конце главы.)
Не стоит забывать, однако, что сила R — там же, где его слабость.
Интерфейс командной строки позволяет делать такие вещи, которых
рядовой пользователь других статистических программ может достичь
только часами ручного труда. Вот, например, простая задача: требуется превратить выборку, состоящую из цифр от 1 до 9, в таблицу из
трех колонок (допустим, это были данные за три дня, и каждый день
делалось три измерения). Чтобы сделать это в программе с визуальным интерфейсом, скажем в STATISTICA, требуется: (1) учредить две
новые переменные, (2–3) скопировать дважды кусок выборки в буфер,
(4–5) скопировать его в одну и другую переменную и (6) уничтожить
лишние строки. В R это делается одной командой:
> b q
function (save = "default", status = 0, runLast = TRUE)
.Internal(quit(save, status, runLast))


Как узнать, как правильно вызывать функцию? Для этого надо научиться получать справку. Есть два пути. Первый — вызвать команду
справки:
help(q)
или
?q
И вы увидите отдельное окно либо (если вы работаете в Linux) текст
помощи в основном окне программы (выход из этого режима — по клавише «q»). Если внимательно прочитать текст, становится ясно, что
выйти из R можно, и не отвечая на вопрос, если ввести q("no").
Зачем же нужен этот вопрос? Или, другими словами, что будет, если
ответить положительно? В этом случае в рабочую папку R (ту, из которой он вызван) запишутся два файла: бинарный .RData и текстовый
.Rhistory. Первый содержит все объекты, созданные вами за время
сессии. Второй — полную историю введенных команд.
Когда вы работаете в R, предыдущую команду легко вызвать, нажав
клавишу-стрелку «вверх». Но если вы сохраните файл .Rhistory, ваши
команды будут доступны и в следующей сессии — при условии, что
вы вызовете R из той же самой папки. И наоборот, если вы случайно
сохраните рабочую среду (эти два файла), то при следующем старте
они загрузятся автоматически. Иногда такое поведение R становится
причиной различных недоумений, так что будьте внимательны!

2.7. R и работа с данными: вид снаружи
2.7.1. Как загружать данные
Сначала о том, как набрать данные прямо в R. Можно, например,
использовать команду c():

R и работа с данными: вид снаружи

31

> a a
[1] 1 2 3 4 5
Здесь мы получаем объект a, состоящий из чисел от одного до пяти.
Можно использовать команды rep(), seq(), scan(), а также оператор двоеточия:
> b b
[1] 1 2 3 4 5
Можно воспользоваться встроенной в R подпрограммой — электронной таблицей наподобие сильно упрощенного Excel, для этого надо набрать команду data.entry(b).
В появившейся таблице можно редактировать данные «на месте», то
есть все, что вы ввели, непосредственно скажется на содержании объекта. Это несколько противоречит общим концепциям, заложенным в
R, и поэтому есть похожая функция de(), которая не меняет объект, а
выдает результат «наружу». Если же у вас уже есть таблица данных,
можно использовать команды fix() или edit(), которые в данном случае вызовут тот же Excel-подобный редактор.
Функция edit(), вызванная для другого типа объекта, запустит
его редактирование в текстовом редакторе (под Windows это обычно
Notepad или Блокнот), и вы сможете отредактировать объект там. Если вы хотите поменять редактор, напишите, например:
options(editor="c:\\Program Files\\CrazyPad\\crazypad.exe")
Однако лучше всего научиться загружать в R файлы, созданные при
помощи других программ, скажем, при помощи Excel. Имейте в виду,
что такие данные имеют очень разный формат, и написать по этому
вопросу сколько-нибудь компактное руководство трудно. Но мы все же
попробуем.
В целом данные, которые надо обрабатывать, бывают двух типов —
текстовые и бинарные. Не вдаваясь в детали, примем, что текстовые —
это такие, которые можно прочитать и отредактировать в любом текстовом редакторе («Блокнот», Notepad, TextEdit, Vi). Для того чтобы
отредактировать бинарные данные, нужна, как правило, программа,
которая эти данные когда-то вывела. Текстовые данные для статистической обработки — это обычно текстовые таблицы, в которых каждая
строка соответствует строчке таблицы, а колонки определяются при помощи разделителей (обычно пробелов, знаков табуляции, запятых или
точек с запятой). Для того чтобы R «усвоил» такие данные, надо, во-

32

Как обрабатывать данные

первых, убедиться, что текущая папка в R и та папка, откуда будут
загружаться ваши данные,— это одно и то же. Для этого в запущенной
сессии R надо ввести команду
> getwd()
[1] "d:/programs/R/R-2.14.1"
Допустим, что это вовсе не та папка, которая вам нужна. Поменять
рабочую папку можно командой:
setwd("e:\\wrk\\temp")
> getwd()
[1] "e:/wrk/temp"
Обратите внимание на работу с обратными слэшами под Windows:
вместо одного слэша надо указывать два, только тогда R их поймет. Под
Linux и Mac OS X все проще: там по умолчанию используются прямые
слэши «/», с которыми проблем никаких нет. Кстати, под Windows тоже
можно использовать прямые слэши, например:
setwd("e:/wrk/temp")
> getwd()
[1] "e:/wrk/temp"
Дальше надо проверить, есть ли в нужной поддиректории нужный
файл:
> dir("data")
[1] "mydata.txt" ...
Теперь можно, наконец, загружать данные. (Предполагается, что в
текущей директории у вас есть папка data, а в ней — файл mydata.txt.
Если это не так, то для того, чтобы запустить этот пример, нужно
скачать файл с упомянутого в предисловии сайта, создать папку data
и поместить туда файл). Данные загружает команда read.table():
> read.table("data/mydata.txt", sep=";", head=TRUE)
a b c
1 1 2 3
2 4 5 6
3 7 8 9

R и работа с данными: вид снаружи

33

Это ровно то, что надо, за тем исключением, что перед нами «рояль в кустах» — авторы заранее знали структуру данных, а именно то,
что у столбцов есть имена (head=TRUE), а разделителем является точка
с запятой (sep=";"). Функция read.table() очень хороша, но не настолько умна, чтобы определять формат данных «на лету». Поэтому
вам придется выяснить нужные сведения заранее, скажем, в том же
самом текстовом редакторе. Есть и способ выяснить это через R, для
этого используется команда file.show("data/mydata.txt"). Она выводит свои данные таким же образом, как и help(),— отдельным окном
или в отдельном режиме основного окна. Вот что вы должны увидеть:
a;b;c
1;2;3
4;5;6
7;8;9
Вернемся к предыдущему примеру. В R многие команды, в том числе
и read.table(), имеют умалчиваемые значения аргументов. Например,
значение sep по умолчанию "", что в данном случае означает, что разделителем является любое количество пробелов или знаков табуляции,
поэтому если в ваших данных вместо точек с запятыми — пробелы,
можно аргумент sep не указывать. Естественно, бывает безумное множество различных частных случаев, и как бы мы ни старались, все не
описать. Отметим, однако, еще несколько важных вещей:
1. Русский текст в файлах обычно читается без проблем. Если все же
возникли проблемы, то лучше перевести все в кодировку UTF-8
(при помощи любой доступной утилиты перекодирования, скажем
iconv) и добавить соответствующую опцию:
> read.table("data/mydata.txt", sep=";", head=TRUE,
+ encoding="UTF-8")
2. Иногда нужно, чтобы R прочитал, кроме имен столбцов, еще и
имена строк. Для этого используется такой прием:
> read.table("data/mydata2.txt", sep=";", head=TRUE)
a b c
one
1 2 3
two
4 5 6
three 7 8 9

34

Как обрабатывать данные

В файле mydata2.txt в первой строке было три колонки, а в
остальных строках — по четыре. Проверьте это при помощи
file.show().
3. Данные, которые выдают многие русифицированные программы,
в качестве десятичного разделителя обычно используют запятую,
а не точку. В этих случаях надо указывать аргумент dec:
> read.table("data/mydata3.txt", dec=",", sep=";", h=T)
Обратите внимание на сокращенное обозначение аргумента и его
значения. Сокращать можно, но с осторожностью, поэтому дальше в тексте мы всегда будем писать TRUE или FALSE, хотя на практике многие используют просто T или F.
Итак, можно сказать, что один из возможных способов работы с
данными в R такой:
1) данные набирают в какой-нибудь «внешней» программе;
2) записывают их в текстовый файл с разделителями (см. выше);
3) загружают как объект в R и работают с этим объектом, возможно,
внося изменения;
4) если были изменения, командой write.table() (про нее см. следующий подраздел) записывают обратно в файл;
5) импортируют как текстовый файл в исходную программу и работают дальше.
Такой способ выглядит сложновато, но позволяет использовать все
преимущества программ по набору электронных таблиц и текстовых
редакторов.
С электронными таблицами в текстовом формате больших проблем
обычно не возникает. Разные экзотические текстовые форматы, как
правило, можно преобразовать к «типичным», если не с помощью R, то
с помощью каких-нибудь текстовых утилит (вплоть до «тяжеловесов»
типа языка Perl). А вот с «посторонними» бинарными форматами дело
гораздо хуже. Здесь возникают прежде всего проблемы, связанные с
закрытыми и/или недостаточно документированными форматами, такими, например, как формат программы MS Excel. Вообще говоря, ответ на вопрос, как прочитать бинарный формат в R, часто сводится к
совету по образцу известного анекдота — «выключим газ, выльем воду

R и работа с данными: вид снаружи

35

и вернемся к условию предыдущей задачи». То есть надо найти способ,
как преобразовать (с минимальными потерями значимой информации,
естественно) бинарные данные в текстовые таблицы. Проблем на этом
пути возникает обычно не слишком много.
Второй путь — найти способ прочитать данные в R без преобразования. В R есть пакет foreign, который может читать бинарные данные, выводимые пакетами MiniTab, S, SAS, SPSS, Stata, Systat, а также
формат DBF. Чтобы узнать про это подробнее, надо загрузить пакет
(командой library(foreign)) и вызвать общую справку по его командам (например, командой help(package=foreign) или просто посмотреть справку по пакету через браузер, который откроется по команде
help.start()).
Что касается других распространенных форматов, скажем, форматов MS Excel, здесь дело хуже. Есть не меньше пяти разных способов, как загружать в R эти файлы, но все они имеют ограничения.
Изо всех способов нам наиболее привлекательным представляется обмен с R через буфер. Если у вас открыт Excel, то можно скопировать
в буфер любое количество ячеек, а потом загрузить их в R командой
read.table("clipboard").
Это просто и, главное, работает с любой Excel-подобной программой,
в том числе с «новым» Excel, Gnumeric и OpenOffice.org / LibreOffice
Calc.
Добавим еще несколько деталей:
1. R может загружать изображения. Для этого есть сразу несколько пакетов, наиболее разработанный из них — pixmap. R может
также загружать карты в формате ArcInfo и др. (пакеты maps,
maptools) и вообще много чего еще. Чтобы загрузить такие пакеты, нужно, в отличие от foreign, их сначала скачать из репозитория. Для этого используется меню (под Windows) или команда
install.packages() (она работает на всех системах).
2. У R есть собственный бинарный формат. Он быстро записывается и быстро загружается, но его нельзя использовать с другими
программами:
> x save(x, file="x.rd") # Сохранить объект "x"
> exists("x")
[1] TRUE
> rm(x)
> exists("x")
[1] FALSE

36

Как обрабатывать данные

> dir()
[1] "x.rd" ...
> load("x.rd") # Загрузить объект "x"
> x
[1] "apple"
(Здесь есть несколько доселе не описанных команд. Для сохранения и загрузки бинарных файлов служат команды save() и
load(), для удаления объекта — команда rm(). Для того чтобы показать вам, что объект удален, мы использовали командупроверку exists(). Все, что написано на строчке после символа
«#»,— это комментарий. Комментарии R пропускает, не читая).
3. Для R написано множество интерфейсов к базам данных, в частности для MySQL, PostgresSQL и sqlite (последний может вызываться прямо из R, см. документацию к пакетам RSQLite и sqldf).
Рассматривать в подробностях мы их здесь не будем.
4. Наконец, R может записывать таблицы и другие результаты обработки данных и, разумеется, графики. Об этом мы поговорим
ниже.

2.7.2. Как сохранять результаты
Начинающие работу с R обычно просто копируют результаты работы (скажем, данные тестов) из консоли R в текстовый файл. И действительно, на первых порах этого может быть достаточно. Однако рано
или поздно возникает необходимость сохранять объемные объекты (скажем, таблицы данных), созданные в течение работы. Можно, как уже
говорилось, использовать внутренний бинарный формат, но это не всегда удобно. Лучше всего сохранять таблицы данных в виде текстовых
таблиц, которые потом можно будет открывать другими (в частности,
офисными) приложениями. Для этого служит команда write.table():
> write.table(file="trees.csv", trees, row.names=FALSE, sep=";",
+ quote=FALSE)
В текущую папку будет записан файл trees.csv, созданный из встроенной в R таблицы данных trees. «Встроенная таблица» означает, что
эти данные доступны в R безо всякой загрузки, напрямую. Кстати говоря, узнать, какие таблицы уже встроены, можно командой data()
(именно так, без аргументов).
А что, если надо записать во внешний файл результаты выполнения
команд? В этом случае используется команда sink():

R и работа с данными: вид снаружи

37

> sink("1.txt", split=TRUE)
> 2+2
[1] 4
> sink()
Тогда во внешний файл запишется строчка «[1] 4», то есть результат выполнения команды. (Мы задали параметр split=TRUE, для того
чтобы вывести данные еще и на экран.) Сама команда записана не будет, а если вы хотите, чтобы она была записана, придется писать что-то
вроде:
> print("2+2")
[1] "2+2"
> 2+2
[1] 4
Другими словами, придется повторять каждую команду два раза.
Для сохранения истории команд служит, как мы уже писали выше,
команда savehistory(), а для сохранения всех созданных объектов —
save.image(). Последняя может оказаться полезной для сохранения
промежуточных результатов работы, если вы не уверены в стабильности работы компьютера.

2.7.3. R как калькулятор
Самый простой способ использования R — это арифметические вычисления. Например, выражение log10(((sqrt(sum(c(2,2))))^2)*2.5)
вычисляется так:
1. Из двух двоек создается вектор: c(2,2).
2. Подсчитывается сумма его членов: 2+2=4.
3. Извлекается квадратный корень: sqrt(4)=2.
4. Он возводится в квадрат: 2^2=4.
5. Результат умножается на 2.5: 4*2.5=10.
6. Вычисляется десятичный логарифм: log10(10)=1.
Как видите, круглые скобки можно вкладывать друг в друга. R раскрывает их, вычисляя значение «изнутри наружу», а если скобки отсутствуют, то следует правилам «старшинства» арифметических операций, похожим на те, которым нас учили в школе. Например, в выражении 2+3*5 сначала будет вычислено произведение (3*5=15), а потом
сумма (2+15=17). Проверьте это в R самостоятельно.

38

Как обрабатывать данные

2.7.4. Графики
Одним из основных достоинств статистического пакета служит разнообразие типов графиков, которые он может построить. R в этом смысле — один из рекордсменов. В базовом наборе есть несколько десятков
типов графиков, еще больше в рекомендуемом пакете lattice, и еще
больше — в пакетах с CRAN, из которых не меньше половины строят
как минимум один оригинальный график (а половина — это больше
полутора тысяч!). Таким образом, по прикидкам получается, что разнообразных типов графики в R никак не меньше тысячи. При этом они
все еще достаточно хорошо настраиваются, то есть пользователь легко может разнообразить эту исходную тысячу на свой вкус. Здесь мы
постараемся, однако, не описывать разнообразие R-графики, а остановимся на нескольких фундаментальных принципах, понимание которых
должно существенно облегчить новичку построение графиков в R.
Рассмотрим такой пример (рис. 1):
> plot(1:20, main="Заголовок")
> legend("topleft", pch=1, legend="Мои любимые точки")
Тут много такого, о чем речи пока не шло. Самое главное — то,
что первая команда рисует график «с нуля», тогда как вторая — добавляет к уже нарисованному графику детали. Это и есть два типа
графических команд, используемых в базовом графическом наборе R.
Теперь немного подробнее. plot() — основная графическая команда,
причем команда «умная» (а правильнее сказать — generic, или «общая»). Это значит, что она распознает тип объекта, который подлежит
рисованию, и строит соответствующий график. Например, в приведенном примере 1:20 — это последовательность чисел от 1 до 20, то есть
вектор (подробнее о векторах см. ниже), а для «одиночного» вектора
предусмотрен график, где по оси абсцисс — индексы (то есть номера
каждого элемента вектора по порядку), а по оси ординат — сами эти
элементы. Если в аргументе команды будет что-то другое, будет, скорее
всего, построен иной график. Вот пример (рис. 2):
> plot(cars)
> title(main="Автомобили двадцатых годов")
Здесь тоже есть команды обоих типов, но немного иначе оформленные. Как видите, не беда, что мы забыли дать заголовок в команде
plot(), его всегда можно добавить потом, командой title(). cars —
это встроенная в R таблица данных, которая использована здесь по прямому назначению, для демонстрации возможностей программы. Прочитать, что такое cars, можно, вызвав справку обычным образом (?cars).

39

R и работа с данными: вид снаружи

Мои любимые точки

5

10

15

20

Заголовок

5

10

15

20

Рис. 1. Пример графика с заголовком и легендой
Для нас сейчас важно, что это — не вектор, а таблица из двух колонок — speed и distance (скорость и тормозная дистанция). Функция
plot() автоматически нарисовала коррелограмму (scatterplot), где по
оси x откладывается значение одной переменной (колонки), а по оси y —
другой, и еще присвоила осям имена этих колонок. Любопытным советуем проверить, что нарисует plot(), если ему «подложить» таблицу
с тремя колонками, скажем встроенную таблицу trees.
Как мы уже писали, очень многие пакеты расширяют графические
функции R. Вот, например, что будет, если мы применим к тем же данным (1:20) функцию qplot() из пакета ggplot2 (рис. 3):
> library(ggplot2)
> qplot(1:20, 1:20, main="Заголовок")
(Мы уже писали выше, что команда library() загружает нужный
пакет. Но пакета ggplot2 в вашей системе могло не быть! Если R не смог
загрузить пакет, то нужно его скачать из Интернета и установить. Делается это через меню или командой install.packages("ggplot2"). Дан-

40

Как обрабатывать данные

0

20

40

60

80

100

120

Автомобили двадцатых годов

5

10

15

20

25

Рис. 2. Пример графика, отражающего встроенные данные cars
ные 1:20 мы повторили дважды потому, что qplot() работает немного
иначе, чем plot()).
Получилось почти то же самое, но оформление выглядит сложнее.
Пакет ggplot2 и создавался для того, чтобы разнообразить слишком
аскетичное, по мнению автора (это Hadley Wickham), оформление графиков в R по умолчанию.

2.7.5. Графические устройства
Это второй очень важный момент для понимания устройства графики в R. Когда вы вводите команду plot(), R открывает так называемое
экранное графическое устройство и начинает вывод на него. Если следующая команда того же типа (то есть не добавляющая), то R «сотрет»
старое изображение и начнет выводить новое. Если вы дадите команду
dev.off(), то R закроет графическое окно (что, впрочем, можно сделать, просто щелкнув по кнопке закрытия окна). Экранных устройств
в R предусмотрено несколько, в каждой операционной системе — свое

41

R и работа с данными: вид снаружи

Заголовок
20

1:20

15

10

5

5

10

1:20

15

20

Рис. 3. Пример графика с заголовком, полученного при помощи команды qplot() из пакета ggplot2
(а в Mac OS X даже два). Но все это не так важно, пока вы не захотите
строить графики и сохранять их в файлы автоматически. Тогда вам
надо будет познакомиться с другими графическими устройствами. Их
несколько (количество опять-таки зависит от операционной системы),
а пакеты предоставляют еще около десятка. Работают они так:
> png(file="1-20.png", bg="transparent")
> plot(1:20)
> dev.off()
Команда png() открывает одноименное графическое устройство, причем задается параметр, включающий прозрачность базового фона (удобно, например, для веб-дизайна). Такого параметра у экранных устройств
нет. Как только вводится команда dev.off(), устройство закрывается,
и на диске появляется файл 1-20.png (на самом деле файл появляется еще при открытии устройства, но сначала в нем ничего не записано). png() — одно из самых распространенных устройств при записи

42

Как обрабатывать данные

файлов. Недостатком его является, разумеется, растровость. R поддерживает и векторные форматы, например PDF. Здесь, однако, могут
возникнуть специфические для русскоязычного пользователя трудности со шрифтами. Остановимся на этом чуть подробнее. Вот как надо
«правильно» создавать PDF-файл, содержащий русский текст:
>
>
>
>

pdf("1-20.pdf", family="NimbusSan", encoding="CP1251.enc")
plot(1:20, main="Заголовок")
dev.off()
embedFonts("1-20.pdf")

Как видим, требуется указать, какой шрифт мы будем использовать,
а также кодировку. Затем нужно закрыть графическое устройство и
встроить в полученный файл шрифты. В противном случае кириллица может не отобразиться! Важно отметить, что шрифт «NimbusSan»
и возможность встраивания шрифтов командой embedFonts() обеспечивается взаимодействием R с «посторонней» программой Ghostscript,
в поставку которой входят шрифты, содержащие русские буквы. Кроме того, если вы работаете под Windows, то R должен «знать» путь к
программе gswin32c.exe (другими словами, нужно, чтобы путь к этой
программе был записан в системную переменную PATH), иначе встраивание шрифтов не сработает1 . К счастью, если Ghostscript был установлен
нормально, то проблем возникнуть не должно2 .
Кроме PDF, R «знает» и другие векторные форматы, например,
PostScript, xfig и picTeX. Есть отдельный пакет svglite, который поддерживает популярный векторный формат SVG. График в этом формате можно, например, открыть и видоизменить в свободном векторном
редакторе Inkscape.

2.7.6. Графические опции
Как уже говорилось, графика в R настраивается в очень широких
пределах. Один из способов настройки — это видоизменение встроенных графических опций. Вот, к примеру, распространенная задача —
нарисовать два графика один над другим на одном рисунке. Чтобы это
сделать, надо изменить исходные опции — разделить пространство рисунка на две части, примерно так (рис. 4):
> old.par hist(cars$speed, main="")
> hist(cars$dist, main="")
> par(old.par)

5

10

15

20

25

0

5

10

15

0

0

20

40

60

80

100

120

Рис. 4. Две гистограммы на одном графике
Ключевая команда здесь — par(). В первой строчке изменяется
один из ее параметров, mfrow, который регулирует, сколько изображений и как будет размещено на «листе». Значение mfrow по умолчанию —
c(1,1), то есть один график по вертикали и один по горизонтали. Чтобы не печатать каждый раз команду par() со всеми ее аргументами, мы
«запомнили» старое значение в объекте old.par, а в конце вернули состояние к запомненному. Команда hist() строит график-гистограмму
(подробнее о ней рассказано в главе про одномерные данные).

2.7.7. Интерактивная графика
Интерактивная графика позволяет выяснить, где именно на графике расположены нужные вам точки, поместить объект (скажем, подпись)

44

Как обрабатывать данные

в нужное место, а также проследить «судьбу» одних и тех же точек
на разных графиках. Кроме того, если данные многомерные, то можно
вращать облако точек в плоскости разных переменных, с тем чтобы выяснить структуру данных. Еще несколько лет назад мы бы написали,
что здесь вам вместо R следует воспользоваться другими аналитическими инструментами, но R развивается так быстро, что все эти методы теперь доступны, и даже в нескольких вариантах. Приведем лишь
один пример. Вот так можно добавлять подписи в указанную мышкой область графика (пока вы еще не начали проверять — после того
как введена вторая команда, надо щелкнуть левой кнопкой мыши на
какой-нибудь точке в середине, а затем щелкнуть в любом месте графика правой кнопкой мыши и в получившемся меню выбрать Stop):
> plot(1:20)
> text(locator(), "Моя любимая точка", pos=4)
Интерактивная графика других типов реализована командой identify(), а также дополнительными пакетами, в том числе iplot, manipulate, playwith, rggobi, rpanel, и TeachingDemos.
***
Ответ на вопрос про то, как найти функцию R, зная только то, что она должна делать. Чтобы, не выходя из R, узнать, как
сделать, скажем, дисперсионный анализ, есть несколько способов. Первый — это команда «??» (два вопросительных знака):
> ??anova
Help files with alias or concept or title matching ‘anova’
using fuzzy matching:
...
stats::anova
stats::anova.glm
stats::anova.lm
...
stats::stat.anova
...

Anova Tables
Analysis of Deviance for Generalized
Linear Model Fits
ANOVA for Linear Model Fits
GLM Anova Statistics

Type ’?PKG::FOO’ to inspect entries ’PKG::FOO’, or
’TYPE?PKG::FOO’ for entries like ’PKG::FOO-TYPE’.

R и работа с данными: вид снаружи

45

Надо только знать, что дисперсионный анализ по-английски называется «Analysis of Variance», или «ANOVA». Похожего результата можно добиться, если сначала запустить интерактивную помощь
(help.start()), а там перейти на поиск и задать в поле поиска то же
самое слово anova.
Ну а если это не помогло, надо искать в Интернете. Сделать это
можно прямо изнутри R:
> RSiteSearch("anova")
A search query has been submitted to
http://search.r-project.org
The results page should open in your browser shortly
В браузере должно открыться окно с результатами запроса.

Глава 3
Типы данных
Чтобы обрабатывать данные, надо не просто их получить. Надо еще
перевести их на язык цифр, ведь математика, на которой основан анализ данных, оперирует по большей части именно с числами. Сделать
это можно самыми разными способами, иногда — удачно, иногда — с
натяжками.
Со времен Галилея, который говорил, что «следует измерять то,
что измеримо, и делать измеримым то, что таковым не является», европейская наука накопила громадный опыт в переводе окружающих
явлений в измерения. Возникла и специальная наука об измерении —
метрология. Ко многим достижениям метрологии мы уже настолько
привыкли, что совершенно их не замечаем. Если, например, ставится
опыт с расширением металлического бруска при нагревании, само собой
подразумевается, что для этого нам нужны термометр и линейка. Эти
два прибора (да, именно прибора!) как раз и являются устройствами,
переводящими температуру и расстояние на язык цифр.

3.1. Градусы, часы и километры: интервальные
данные
Очень важно, что температура и расстояние изменяются плавно и
непрерывно. Это значит, что если у нас есть две разные температуры, то всегда можно представить температуру, промежуточную между
ними. Любые два показателя температуры или расстояния представляют собой интервал, куда «умещается» бесконечное множество других
показателей. Поэтому такие данные и называются интервальными. Интервальные данные чаще всего сравнивают с хорошо известной нам из
курса математики числовой прямой, на которой расположены так называемые действительные (или, как их еще называют, вещественные)
числа. Можно еще вспомнить о рациональных числах — то есть таких
числах, которые можно представить в виде дроби. И те, и другие очень
близки по своей сути к интервальным данным.

Градусы, часы и километры: интервальные данные

47

Не всегда, однако, интервальные данные изменяются плавно и непрерывно, от (как говорят математики) плюс бесконечности к минус бесконечности. Пример перед глазами: температура соответствует не прямой,
а лучу, потому что со стороны отрицательной (слева) она ограничена
абсолютным нулем, ниже которого температуры просто не бывает. Но
на остальном протяжении этого луча показатели температуры можно уподобить действительным числам. Еще интереснее измерять углы.
Угол изменяется непрерывно, но вот после 359◦ следует 0◦ — вместо
прямой имеем отрезок без отрицательных значений. Есть даже особый
раздел статистики, так называемая круговая статистика (directional,
or circular statistics), которая работает с углами.
А вот другая ситуация. Допустим, мы считаем посетителей магазина. Понятно, что если в один день в магазин зашло 947 человек, а в
другой — 832, то очень легко представить промежуточные значения. К
тому же очевидно, что в первый день в магазине было больше народа.
Однако если взять два «соседних»числа, например 832 и 831, то промежуточное значение представить нельзя, потому что люди на части не
делятся. Получается, что такие данные соответствуют не действительным, а скорее натуральным числам. У этих чисел тоже есть отношение
порядка, но вот промежуточное значение есть не всегда. И отрицательных значений у них нет. Перед нами — другой тип интервальных данных, не непрерывный, а дискретный.
С интервальностью и непрерывностью данных неразрывно связан
важный водораздел в методах статистики. Эти методы часто делят на
две большие группы: параметрические и непараметрические. Параметрические тесты предназначены для обработки так называемых параметрических данных. Для того чтобы данные считались параметрическими, должны одновременно выполняться три условия:
1) распределение данных близко к нормальному (незнакомые термины можно посмотреть в словаре);
2) выборка — большая (обычно не менее 30 наблюдений);
3) данные — интервальные непрерывные.
Если хотя бы одно из этих условий не выполняется, данные считаются непараметрическими и обрабатываются непараметрическими
методами. Несомненным достоинством непараметрических методов является, как ни банально это звучит, их способность работать с непараметрическими (то есть «неидеальными») данными. Зато параметриче0
ские методы имеют большую мощность (то есть при прочих равных
вероятность не заметить существующую закономерность ниже). Этому
есть простое объяснение: непараметрические данные (если они, как это

48

Типы данных

очень часто бывает, дискретны) имеют свойство «скрывать» имеющиеся различия, объединяя отдельные значения в группы.
Так как параметрические методы доступнее непараметрических (например, в курсах статистики изучают в основном параметрические методы), то часто хочется как-нибудь «параметризировать» данные. На
распределение данных мы, естественно, никак повлиять не можем (хотя
иногда преобразования данных могут «улучшить» распределение и даже сделать его нормальным — об этом написано ниже). Что мы можем
сделать, так это постараться иметь достаточно большой объем выборки
(что, как вы помните, увеличивает и ее репрезентативность), а также
работать с непрерывными данными.
В R интервальные данные представляют в виде числовых векторов
(numerical vectors). Чаще всего один вектор — это одна выборка. Допустим, у нас есть данные о росте семи сотрудников небольшой компании.
Вот так можно создать из этих данных простейший числовой вектор:
> x is.vector(x)
[1] TRUE
Вообще говоря, в R есть множество функций «is.что-то()» для
подобной проверки, например:
> is.numeric(x)
[1] TRUE
А еще есть функции конверсии «as.что-то()», с которыми мы поработаем ниже. Называть объекты можно в принципе как угодно, но
лучше придерживаться некоторых правил:

«Садись, двойка»: шкальные данные

49

1. Использовать для названий только латинские буквы, цифры и
точку (имена объектов не должны начинаться с точки или цифры).
2. Помнить, что R чувствителен к регистру, X и x — это разные имена.
3. Не давать объектам имена, уже занятые распространенными функциями (типа c()), а также ключевыми словами (особенно T, F, NA,
NaN, Inf, NULL, а также pi — единственное встроенное в R число).
Для создания «искусственных» векторов очень полезен оператор
«:», обозначающий интервал, а также функции создания последовательностей («sequences») seq() и повторения («replications») rep().

3.2. «Садись, двойка»: шкальные данные
Если интервальные данные можно получить непосредственно (например, посчитать) или при помощи приборов (измерить), то шкальные
данные не так просто сопоставить числам. Предположим, нам надо составить, а затем проанализировать данные опросов об удобстве мебели.
Ясно, что «удобство» — вещь субъективная, но игнорировать ее нельзя, надо что-то с ней сделать. Как правило, «что-то» — это шкала, где
каждому баллу соответствует определенное описание, которое и включается в опрос. Кроме того, в такой шкале все баллы часто можно ранжировать, в нашем случае — от наименее удобной мебели к наиболее
удобной.
Число, которым обозначено значение шкалы,— вещь более чем условная. По сути, можно взять любое число. Зато есть отношение порядка и,
более того, подобие непрерывности. Например, если удобную во всех отношениях мебель мы станем обозначать цифрой «5», а несколько менее
удобную — цифрой «4», то в принципе можно представить, какая мебель
могла бы быть обозначена цифрой «4.5». Именно поэтому к шкальным
данным применимы очень многие из тех методов, которые используются для обработки интервальных непрерывных данных. Однако к числовым результатам обработки надо подходить с осторожностью, всегда
помнить об условности значений шкалы.
Больше всего трудностей возникает, когда данные измерены в разных шкалах. Разные шкалы часто очень нелегко перевести друг в друга.
По умолчанию R будет распознавать шкальные данные как обычный
числовой вектор. Однако для некоторых задач может потребоваться
преобразовать его в так называемый упорядоченный фактор («ordered
factor» — см. ниже). Если же стоит задача создать шкальные дан-

50

Типы данных

ные из интервальных, то можно воспользоваться функцией cut(...,
ordered=TRUE).
Для статистического анализа шкальных данных всегда требуются
непараметрические методы. Если же хочется применить параметрические методы, то нужно иначе спланировать сбор данных, чтобы в результате получить интервальные данные. Например, при исследованиях размеров листьев не делить их визуально на «маленькие», «средние»
и «большие», а измерить их длину и ширину при помощи линейки. Однако иногда сбор непрерывных данных требует использования труднодоступного оборудования и сложных методик (например, если вы решите исследовать окраску цветков как непрерывную переменную, вам
понадобится спектрофотометр для измерения длины волны отраженного света — количественного выражения видимого цвета). В этом случае можно выйти из положения путем последующего перекодирования
данных на стадии их обработки. Например, цвет можно закодировать в
значениях красного, зеленого и синего каналов компьютерной цветовой
шкалы RGB.
Вот еще один пример перекодирования. Предположим, вы изучаете высоту зданий в различных городах земного шара. Можно в графе
«город» написать его название (номинальные данные). Это, конечно,
проще всего, но тогда вы не сможете использовать эту переменную в
статистическом анализе данных. Можно закодировать города цифрами
в порядке их расположения, например с севера на юг (если вас интересует географическая изменчивость высоты зданий в городе),— тогда
получатся шкальные данные, которые можно обработать непараметрическими методами. И наконец, каждый город можно обозначить его
географическими координатами или расстоянием от самого южного города — тогда мы получим интервальные данные, которые можно будет
попробовать обработать параметрическими методами.

3.3. Красный, желтый, зеленый: номинальные
данные
Номинальные данные (их часто называют «категориальными»), в
отличие от шкальных, нельзя упорядочивать. Поэтому они еще дальше от чисел в строгом смысле слова, чем шкальные данные. Вот, например, пол. Даже если мы присвоим мужскому и женскому полам
какие-нибудь числовые значения (например, 1 и 2), то из этого не будет
следовать, что какой-то пол «больше» другого. Да и промежуточное
значение (1.5) здесь непросто представить.

Красный, желтый, зеленый: номинальные данные

51

В принципе, можно обозначать различные номинальные показатели
не цифрами, а буквами, целыми словами или специальными значками —
суть от этого не изменится.
Обычные численные методы для номинальных данных неприменимы. Однако существуют способы их численной обработки. Самый простой — это счет, подсчет количеств данных разного типа в общем массиве
данных. Эти количества и производные от них числа уже гораздо легче
поддаются обработке.
Особый случай как номинальных, так и шкальных данных — бинарные данные, то есть такие, которые проще всего передать числами
0 и 1. Например, ответы «да» и «нет» на вопросы анкеты. Или наличие/отсутствие чего-либо. Бинарные данные иногда можно упорядочить (скажем, наличие и отсутствие), иногда — нет (скажем, верный
и неверный ответы). Можно бинарные данные представить и в виде
«логического вектора», то есть набора значений TRUE или FALSE. Самая главная польза от бинарных данных — в том, что в них можно
перекодировать практически все остальные типы данных (хотя иногда
при этом будет потеряна часть информации). После этого к ним можно
применять специальные методы анализа, например логистическую регрессию (см. главу о двумерных данных) или бинарные коэффициенты
сходства (см. главу о многомерных данных).
Для обозначения номинальных данных в R есть несколько способов,
разной степени «правильности». Во-первых, можно создать текстовый
(character) вектор:
> sex is.character(sex)
[1] TRUE
> is.vector(sex)
[1] TRUE
> str(sex)
chr [1:7] "male" "female" "male" "male" "female" "male" ...
Обратите внимание на функцию str()! Это очень важная функция,
мы бы рекомендовали выучить ее одной из первых. На первых порах
пользователь R не всегда понимает, с каким типом объекта (вектором,
таблицей, списком и т. п.) он имеет дело. Разрешить сомнения помогает
str().
Предположим теперь, что sex — это описание пола сотрудников
небольшой организации. Вот как R выводит содержимое этого вектора:
> sex

52

[1] "male"

Типы данных

"female" "male"

"male"

"female" "male"

"male"

Кстати, пора раскрыть загадку единицы в квадратных скобках —
это просто номер элемента вектора. Вот как его можно использовать
(да-да, квадратные скобки — это тоже команда, можно это проверить,
набрав помощь ?"["):
> sex[1]
[1] "male"
«Умные», то есть объект-ориентированные, команды R кое-что понимают про объект sex, например команда table():
> table(sex)
sex
female
male
2
5
А вот команда plot(), увы, не умеет ничего хорошего сделать с
таким вектором. Сначала нужно сообщить R, что этот вектор надо рассматривать как фактор (то есть номинальный тип данных). Делается
это так:
> sex.f sex.f
[1] male
female male
Levels: female male

male

female male

male

И теперь команда plot() уже «понимает», что ей надо делать —
строить столбчатую диаграмму (рис. 5):
> plot(sex.f)
Это произошло потому, что перед нами специальный тип объекта,
предназначенный для категориальных данных,— фактор с двумя уровнями (градациями) (levels):
> is.factor(sex.f)
[1] TRUE
> is.character(sex.f)
[1] FALSE
> str(sex.f)
Factor w/ 2 levels "female","male": 2 1 2 2 1 2 2

53

0

1

2

3

4

5

Красный, желтый, зеленый: номинальные данные

female

male

Рис. 5. Вот так команда plot() рисует фактор
Очень многие функции R (скажем, тот же самый plot()) предпочитают факторы текстовым векторам, при этом некоторые умеют конвертировать текстовые векторы в факторы, а некоторые — нет, поэтому
надо быть внимательным. Еще несколько свойств факторов надо знать
заранее. Во-первых, подмножество фактора — это фактор с тем же количеством уровней, даже если их в подмножестве не осталось:
> sex.f[5:6]
[1] female male
Levels: female male
> sex.f[6:7]
[1] male male
Levels: female male
«Избавиться» от лишнего уровня можно, применив специальный аргумент или выполнив преобразование данных «туда и обратно»:
> sex.f[6:7, drop=TRUE]

54

Типы данных

[1] male male
Levels: male
> factor(as.character(sex.f[6:7]))
[1] male male
Levels: male
Во-вторых, факторы (в отличие от текстовых векторов) можно легко преобразовать в числовые значения:
> as.numeric(sex.f)
[1] 2 1 2 2 1 2 2
Зачем это нужно, становится понятным, если рассмотреть вот такой пример. Положим, кроме роста, у нас есть еще и данные по весу
сотрудников:
> w plot(x, w, pch=as.numeric(sex.f), col=as.numeric(sex.f))
> legend("topleft", pch=1:2, col=1:2, legend=levels(sex.f))
Тут, разумеется, нужно кое-что объяснить. pch и col — эти параметры предназначены для определения соответственно типа значков и
их цвета на графике. Таким образом, в зависимости от того, какому
полу принадлежит данная точка, она будет изображена кружком или
треугольником и черным или красным цветом. При условии, разумеется, что все три вектора соответствуют друг другу. Еще надо отметить,
что изображение пола при помощи значка и цвета избыточно, для «нормального» графика хватит и одного из этих способов.
В-третьих, факторы можно упорядочивать, превращая их в один из
вариантов шкальных данных. Введем четвертую переменную — размер
маек для тех же самых гипотетических восьмерых сотрудников:
> m m.f m.f
[1] L
S
XL XXL S
M
L
Levels: L M S XL XXL
Как видим, уровни расположены просто по алфавиту, а нам надо,
чтобы S (small) шел первым. Кроме того, надо как-то сообщить R, что
перед нами — шкальные данные. Делается это так:

Красный, желтый, зеленый: номинальные данные

55

60

65

70

75

80

85

90

female
male

165

170

175

180

185

190

Рис. 6. График, показывающий одновременно три переменные
> m.o m.o
[1] L
S
XL XXL S
M
L
Levels: S < M < L < XL < XXL
Теперь R «знает», какой размер больше. Это может сыграть критическую роль — например, при вычислениях коэффициентов корреляции.
Работая с факторами, нужно помнить и об одной опасности. Если
возникла необходимость перевести фактор в числа, то вместо значений
вектора мы получим числа, соответствующие уровням фактора! Чтобы
этого не случилось, надо сначала преобразовать фактор, состоящий из
значений-чисел, в текстовый вектор, а уже потом — в числовой:
> a a
[1] 3 4 5
Levels: 3 4 5

56

Типы данных

> as.numeric(a) # Неправильно!
[1] 1 2 3
> as.numeric(as.character(a)) # Правильно!
[1] 3 4 5
Когда файл данных загружается при помощи команды read.table(),
то все столбцы, где есть хотя бы одно нечисло, будут преобразованы в
факторы1 . Если хочется этого избежать (для того, например, чтобы не
столкнуться с вышеописанной проблемой), то нужно задать дополнительный параметр: read.table(..., as.is=TRUE).

3.4. Доли, счет и ранги: вторичные данные
Из названия ясно, что такие данные возникают в результате обработки первичных, исходных данных.
Наибольшее применение вторичные данные находят при обработке
шкальных и в особенности номинальных данных, которые нельзя обрабатывать «в лоб». Например, счет («counts») — это просто количество
членов какой-либо категории. Такие подсчеты и составляют суть статистики в бытовом смысле этого слова. Проценты (доли, «rates») тоже
часто встречаются в быту, так что подробно описывать их, наверное, не
нужно. Одно из самых полезных их свойств — они позволяют вычленить числовые закономерности там, где исходные данные отличаются
по размеру.
Для того чтобы визуализировать счет и проценты, придумано немало графических способов. Самые из них распространенные — это, наверное, графики-пироги и столбчатые диаграммы. Почти любая компьютерная программа, имеющая дело с таблицами данных, умеет строить
такие графики. Однако надо заметить, что столбчатые диаграммы и
в особенности «пироги» — неудачный способ представления информации. Многочисленные эксперименты доказали, что читаются такие графики гораздо хуже остальных. Самое печальное, что главная задача
графиков — показать, где цифры различаются, а где сходны, практически не выполняется. В экспериментах людям предлагали несколько
минут смотреть на такие графики, а затем просили расположить группы, отраженные на графике, в порядке значений подсчитанного признака. Оказалось, что это нелегко сделать. Вот пример. На рисунке 7 —
столбчатый график результатов гадания на ромашках:
> romashka.t romashka
>
>
>
+
>

57

names(romashka)
>
>

a1 h h
[1] 8 10 NA NA 8 NA 8
Как видим, NA надо вводить без кавычек, а R нимало не смущается,
что среди цифр находится вроде бы текст. Отметим, что пропущенные
данные очень часто столь же разнородны, как и в нашем примере. Однако кодируются они одинаково, и об этом не нужно забывать. Теперь
о том, как надо работать с полученным вектором h. Если мы просто
попробуем посчитать среднее значение (функция mean()), то получим:
> mean(h)
[1] NA
И это «идеологически правильно», поскольку функция может поразному обрабатывать NA, и по умолчанию она просто сигнализирует о
том, что с данными что-то не так. Чтобы высчитать среднее от «непропущенной» части вектора, можно поступить одним из двух способов:

Выбросы и как их найти

61

> mean(h, na.rm=TRUE)
[1] 8.5
> mean(na.omit(h))
[1] 8.5
Первый способ разрешает функции mean() принимать пропущенные
данные, а второй делает из вектора h временный вектор без пропущенных данных (они просто выкидываются из вектора). Какой из способов
лучше, зависит от ситуации.
Часто возникает еще одна проблема — как сделать подстановку пропущенных данных, скажем, заменить все NA на среднюю по выборке.
Вот распространенное (но не очень хорошее) решение:
> h[is.na(h)] h
[1] 8.0 10.0 8.5 8.5 8.0 8.5 8.0
В левой части первого выражения осуществляется индексирование,
то есть выбор нужных значений h — таких, которые являются пропущенными (is.na()). После того как выражение выполнено, «старые»
значения исчезают навсегда, поэтому рекомендуем сначала сохранить
старый вектор, скажем, под другим названием:
> h.old h.old
[1] 8 10 NA NA

8 NA

8

Есть много других способов замены пропущенных значений, в том
числе и очень сложные, основанные на регрессионном, а также дискриминантном анализе. Некоторые из них реализованы в пакетах mice и
cat, существует даже пакет, предоставляющий графический интерфейс
для «борьбы» с пропущенными данными, MissingDataGUI.

3.6. Выбросы и как их найти
К сожалению, после набора данных возникают не только «пустые
ячейки». Очень часто встречаются просто ошибки. Чаще всего это опечатки, которые могут возникнуть при ручном наборе. Если данных
немного, то можно попытаться выявить такие ошибки вручную. Хуже,
если объем данных велик — скажем, более тысячи записей. В этом случае могут помочь методы обработки данных, прежде всего те, которые
рассчитаны на выявление выбросов (outliers). Самый простой из них —
нахождение минимума и максимума, а для номинальных данных — по-

62

Типы данных

строение таблицы частот. К сожалению, такие методы помогают лишь
отчасти. Легко найти опечатку в таблице данных роста человека, если
кто-то записал 17 см вместо 170 см. Однако ее практически невозможно
найти, если вместо 170 см написано 171 см. В этом случае остается надеяться лишь на статистическую природу данных — чем их больше, тем
менее заметны будут ошибки, и на так называемые робастные (устойчивые к выбросам) методы обработки, о которых мы еще поговорим
ниже.

3.7. Меняем данные: основные принципы
преобразования
Если в исследовании задействовано несколько разных типов данных — параметрические и непараметрические, номинальные и непрерывные, проценты и подсчеты и т. п., то самым правильным будет привести их к какому-то «общему знаменателю».
Иногда такое преобразование сделать легко. Даже номинальные данные можно преобразовать в непрерывные, если иметь достаточно информации. Скажем, пол (номинальные данные) можно преобразовать в
уровень мужского гормона тестостерона в крови (непрерывные); правда, для этого нужна дополнительная информация. Распространенный
вариант преобразования — обработка дискретных данных так, как будто они непрерывные. В целом это безопасно, но иногда приводит к
неприятным последствиям. Совершенно неприемлемый вариант — преобразование номинальных данных в шкальные. Если данные по своей
природе не упорядочены, то их искусственное упорядочение может радикально сказаться на результате.
Часто данные преобразуют для того, чтобы они больше походили
на параметрические. Если у распределения данных длинные «хвосты»,
если график распределения (как на рис. 12) лишь отчасти «колоколообразный», можно прибегнуть к логарифмированию. Это, наверное, самое
частое преобразование. В графических командах R есть даже специальный аргумент
..., log="ось",
где вместо слова ось надо подставить x или y, и тогда соответствующая ось графика отобразится в логарифмическом масштабе.
Вот самые распространенные методы преобразований с указаниями,
как их делать в R (мы предполагаем, что ваши данные находятся в
векторе data):
• Логарифмическое: log(data + 1). Если распределение скошено
вправо, может дать нормальное распределение. Может также де-

Меняем данные: основные принципы преобразования

63

лать более линейными зависимости между переменными и уравнивать дисперсии. «Боится» нулей в данных, поэтому рекомендуется прибавлять единицу.
• Квадратного корня: sqrt(data). Похоже по действию на логарифмическое. «Боится» отрицательных значений.
• Обратное: 1/(data + 1). Эффективно для стабилизации дисперсии. «Боится» нулей.
• Квадратное: data^2. Если распределение скошено влево, может
дать нормальное распределение. Линеаризует зависимости и выравнивает дисперсии.
• Логит: log(p/(1-p)). Чаще всего применяется к пропорциям. Линеаризует так называемую сигмовидную кривую. Кроме логитпреобразования, для пропорций часто используют и арксинус-преобразование, asin(sqrt(p))
При обработке многомерных данных очень важно, чтобы они были
одной размерности. Ни в коем случае нельзя одну колонку в таблице
записывать в миллиметрах, а другую — в сантиметрах.
В многомерной статистике широко применяется и нормализация
данных — приведение разных колонок к общему виду (например, к одному среднему значению). Вот как можно, например, нормализовать
два разномасштабных вектора:
>
>
>
>

a ma ma
[,1] [,2]
[1,]
1
2
[2,]
3
4
> str(ma)
int [1:2, 1:2] 1 3 2 4
> str(m)
int [1:4] 1 2 3 4

Матрицы, списки и таблицы данных

65

Как видно, структура (напомним, структуру любого объекта можно
посмотреть при помощи очень важной команды str()) объектов m и ma
не слишком различается, различается, по сути, лишь их вывод на экран
компьютера. Еще очевиднее единство между векторами и матрицами
прослеживается, если создать матрицу несколько иным способом:
> mb mb
[1] 1 2 3 4
> attr(mb, "dim") mb
[,1] [,2]
[1,]
1
3
[2,]
2
4
Выглядит как некий фокус. Однако все просто: мы присваиваем
вектору mb атрибут dim («dimensions», размерность) и устанавливаем
значение этого атрибута в c(2,2), то есть 2 строки и 2 столбца. Читателю предоставляется догадаться, почему матрица mb отличается от
матрицы ma (ответ см. в конце главы).
Мы указали лишь два способа создания матриц, в действительности
их гораздо больше. Очень популярно, например, «делать» матрицы из
векторов-колонок или строк при помощи команд cbind() или rbind().
Если результат нужно «повернуть» на 90 градусов (транспонировать),
используется команда t().
Наиболее распространены матрицы, имеющие два измерения, однако никто не препятствует сделать многомерную матрицу (массив):
>
>
>
,

m3 h[3]
[1] 8.5

Матрицы, списки и таблицы данных

67

Элементы матрицы выбираются так же, только используются несколько аргументов (для двумерных матриц это номер строки и номер столбца — именно в такой последовательности):
> ma[2, 1]
[1] 3
А вот элементы списка выбираются тремя различными методами.
Во-первых, можно использовать квадратные скобки:
> l[1]
[[1]]
[1] "R"
> str(l[1])
List of 1
$ : chr "R"
Здесь очень важно, что полученный объект тоже будет списком.
Во-вторых, можно использовать двойные квадратные скобки:
> l[[1]]
[1] "R"
> str(l[[1]])
chr "R"
В этом случае полученный объект будет того типа, какого он был
бы до объединения в список (поэтому первый объект будет текстовым
вектором, а вот пятый — списком).
И в-третьих, для индексирования можно использовать имена элементов списка. Но для этого сначала надо их создать:
> names(l) l$first
[1] "R"
> str(l$first)
chr "R"
Для выбора по имени употребляется знак доллара, а полученный
объект будет таким же, как при использовании двойной квадратной
скобки. На самом деле имена в R могут иметь и элементы вектора, и
строки и столбцы матрицы:

68

Типы данных

> names(w) w
Коля Женя Петя Саша Катя Вася Жора
69
68
93
87
59
82
72
> rownames(ma) colnames(ma) ma
b1 b2
a1 1 2
a2 3 4
Единственное условие — все имена должны быть разными. Однако знак доллара можно использовать только со списками. Элементы
вектора по имени можно отбирать так:
> w["Женя"]
Женя
68

3.8.3. Таблицы данных
И теперь о самом важном типе представления данных — таблицах
данных (data frame). Именно таблицы данных больше всего похожи на
электронные таблицы Excel и аналогов, и поэтому с ними работают чаще всего (особенно начинающие пользователи R). Таблицы данных —
это гибридный тип представления, одномерный список из векторов одинаковой длины. Таким образом, каждая таблица данных — это список
колонок, причем внутри одной колонки все данные должны быть одного
типа (а вот сами колонки могут быть разного типа). Проиллюстрируем
это на примере созданных ранее векторов:
> d d
weight height size
sex
Коля
69
174
L
male
Женя
68
162
S female
Петя
93
188
XL
male
Саша
87
192 XXL
male
Катя
59
165
S female
Вася
82
168
M
male
Жора
72
172.5 L
male

Матрицы, списки и таблицы данных

69

> str(d)
’data.frame’:
7 obs. of 4 variables:
$ weight: num 69 68 93 87 59 82 72
$ height: num 174 162 188 192 165 168 172.5
$ size : Ord.factor w/ 5 levels "S" d[,"weight"]
[1] 69 68 93 87

59 82 72
59 82 72
59 82 72
59 82 72

Очень часто бывает нужно отобрать несколько колонок. Это можно
сделать разными способами:
> d[,2:4]
height size
Коля
174
L
Женя
162
S
Петя
188
XL
Саша
192 XXL
Катя
165
S
Вася
168
M
Жора
172.5 L
> d[,-1]
height size
Коля
174
L
Женя
162
S
Петя
188
XL
Саша
192 XXL
Катя
165
S
Вася
168
M
Жора
172.5 L

sex
male
female
male
male
female
male
male
sex
male
female
male
male
female
male
male

70

Типы данных

К индексации имеет прямое отношение еще один тип данных R —
логические векторы. Как, например, отобрать из нашей таблицы только
данные, относящиеся к женщинам? Вот один из способов:
> d[d$sex=="female",]
weight height size
sex
Женя
68
162
S female
Катя
59
165
S female
Чтобы отобрать нужные строки, мы поместили перед запятой логическое выражение d$sex==female. Его значением является логический
вектор:
> d$sex=="female"
[1] FALSE TRUE FALSE FALSE

TRUE FALSE FALSE

Таким образом, после того как «отработала» селекция, в таблице
данных остались только те строки, которые соответствуют TRUE, то есть
строки 2 и 5. Знак «==», а также знаки «&», «|» и «!» используются
для замены соответственно «равен?», «и», «или» и «не».
Более сложным случаем отбора является сортировка таблиц данных. Для сортировки одного вектора достаточно применить команду
sort(), а вот если нужно, скажем, отсортировать наши данные сначала
по полу, а потом по росту, приходится применить операцию посложнее:
> d[order(d$sex, d$height), ]
weight height size
sex
Женя
68
162
S female
Катя
59
165
S female
Вася
82
168
M
male
Жора
72
172.5 L
male
Коля
69
174
L
male
Петя
93
188
XL
male
Саша
87
192 XXL
male
Команда order() создает не логический, а числовой вектор, который соответствует будущему порядку расположения строк. Подумайте,
как применить команду order() для того, чтобы отсортировать колонки получившейся матрицы по алфавиту (см. ответ в конце главы).
***
Заключая разговор о типах данных, следует отметить, что эти типы
вовсе не так резко отграничены друг от друга. Поэтому если вам трудно

Матрицы, списки и таблицы данных

71

с первого взгляда сказать, к какому именно типу относятся ваши данные, нужно вспомнить главный вопрос — насколько хорошо данные соотносятся с числовой прямой? Если соотношение хорошее, то данные,
скорее всего, интервальные и непрерывные, если плохое — шкальные
или даже номинальные. И во-вторых, не следует забывать, что весьма
часто удается найти способ преобразования данных в требуемый тип.
***
Ответ к задаче про матрицы. Создавая матрицу ma, мы задали
byrow=TRUE, то есть указали, что элементы вектора будут соединяться
в матрицу построчно. Если бы мы указали byrow=FALSE, то получили
бы точно такую же матрицу, как mb:
> ma ma
[,1] [,2]
[1,]
1
3
[2,]
2
4
Ответ к задаче про сортировку. Для того чтобы что-то сделать
с колонками, надо использовать квадратные скобки с запятой в центре,
а команды помещать справа от запятой. Логично использовать ту же
команду order():
> d.sorted d.sorted[, order(names(d.sorted))]

Женя
Катя
Вася
Жора
Коля
Петя
Саша

height
sex size weight
162 female
S
68
165 female
S
59
168
male
M
82
172.5 male
L
72
174
male
L
69
188
male
XL
93
192
male XXL
87

Заметьте, что нельзя просто написать order() после запятой, эта
команда должна выдать новый порядок колонок. Поэтому мы «скормили» ей имена этих колонок, которые для таблицы данных всегда можно
получить, вызвав команду names(). Кстати, вместо order() здесь подошла бы и команда sort(), потому что нам надо отсортировать всего
лишь один вектор.
Данные гадания на ромашке. Вот они (проценты от общего количества исходов):

72

Типы данных

> rev(sort(romashka))
плюнет к сердцу прижмет
24
17
не любит
любит
15
15

поцелует
16
к черту пошлет
13

(Мы использовали rev(), потому что sort() сортирует по возрастанию.)
Как видно, гадание было не очень-то удачное...

Глава 4
Великое в малом: одномерные данные
Теперь, наконец, можно обратиться к статистике. Начнем с самых
элементарных приемов анализа — вычисления общих характеристик
одной-единственной выборки.

4.1. Как оценивать общую тенденцию
У любой выборки есть две самые общие характеристики: центр
(центральная тенденция) и разброс (размах). В качестве центра чаще всего используются среднее и медиана, а в качестве разброса —
стандартное отклонение и квартили. Среднее отличается от медианы прежде всего тем, что оно хорошо работает в основном тогда, когда
распределение данных близко к нормальному (мы еще поговорим об
этом ниже). Медиана не так зависит от характеристик распределения,
как говорят статистики, она более робастна (устойчива). Понять разницу легче всего на таком примере. Возьмем опять наших гипотетических
сотрудников. Вот их зарплаты (в тыс. руб.):
> salary mean(salary); median(salary)
[1] 32.28571
[1] 21
Получается, что из-за высокой Катиной зарплаты среднее гораздо хуже отражает «типичную», центральную зарплату, чем медиана.
Отчего же так получается? Дело в том, что медиана вычисляется совершенно иначе, чем среднее.
Медиана — это значение, которое отсекает половину упорядоченной
выборки. Для того чтобы лучше это показать, вернемся к тем двум
векторам, на примере которых в предыдущей главе было показано, как
присваиваются ранги:

74

Великое в малом: одномерные данные

> a1 a2 median(a1)
[1] 6
> median(a2)
[1] 7
В векторе a1 всего двенадцать значений, то есть четное число. В этом
случае медиана — среднее между двумя центральными числами. У вектора a2 все проще, там одиннадцать значений, поэтому для медианы
просто берется середина.
Кроме медианы, для оценки свойств выборки очень полезны квартили, то есть те значения, которые отсекают соответственно 0%, 25%,
50%, 75% и 100% от всего распределения данных. Если вы читали предыдущий абзац внимательно, то, наверное, уже поняли, что медиана —
это просто третий квартиль (50%). Первый и пятый квартили — это
соответственно минимум и максимум, а второй и четвертый квартили используют для робастного вычисления разброса (см. ниже). Можно понятие «квартиль» расширить и ввести специальный термин для
значения, отсекающего любой процент упорядоченного распределения
(не обязательно по четвертям),— это называется «квантиль». Квантили используются, например, при анализе данных на нормальность (см.
ниже).
Для характеристики разброса часто используют и параметрическую
величину — стандартное отклонение. Широко известно «правило трех
сигм», которое утверждает, что если средние значения двух выборок
различаются больше чем на тройное стандартное отклонение, то эти
выборки разные, то есть взяты из разных генеральных совокупностей.
Это правило очень удобно, но, к сожалению, подразумевает, что обе
выборки должны подчиняться нормальному распределению. Для вычисления стандартного отклонения в R предусмотрена функция sd().
Кроме среднего и медианы, есть еще одна центральная характеристика распределения, так называемая мода, самое часто встречающееся
в выборке значение. Мода применяется редко и в основном для номинальных данных. Вот как посчитать ее в R (мы использовали для
подсчета переменную sex из предыдущей главы):
> sex t.sex mode mode
male

Как оценивать общую тенденцию

75

5
Таким образом, мода нашей выборки — male.
Часто стоит задача посчитать среднее (или медиану) для целой таблицы данных. Есть несколько облегчающих жизнь приемов. Покажем
их на примере встроенных данных trees:
> attach(trees) # Первый способ
> mean(Girth)
[1] 13.24839
> mean(Height)
[1] 76
> mean(Volume/Height)
[1] 0.3890012
> detach(trees)
> with(trees, mean(Volume/Height)) # Второй способ
[1] 0.3890012
> lapply(trees, mean) # Третий способ
$Girth
[1] 13.24839
$Height
[1] 76
$Volume
[1] 30.17097
Первый способ (при помощи attach()) позволяет присоединить колонки таблицы данных к списку текущих переменных. После этого к
переменным можно обращаться по именам, не упоминая имени таблицы. Важно не забыть сделать в конце detach(), потому что велика
опасность запутаться в том, что вы присоединили, а что — нет. Если
присоединенные переменные были как-то модифицированы, на самой
таблице это не скажется.
Второй способ, в сущности, аналогичен первому, только присоединение происходит внутри круглых скобок функции with(). Третий способ
использует тот факт, что таблицы данных — это списки из колонок. Для
строк такой прием не сработает, надо будет запустить apply(). (Если
вам пришел в голову четвертый способ, то напоминаем, что циклические конструкции типа for в R без необходимости не приветствуются).
Стандартное отклонение, дисперсия (его квадрат) и так называемый
межквартильный разброс вызываются аналогично среднему:
> sd(salary); var(salary); IQR(salary)
[1] 31.15934

76

Великое в малом: одномерные данные

[1] 970.9048
[1] 6
Последнее выражение, дистанция между вторым и четвертым квартилями IQR (или межквартильный разброс), робастен и лучше подходит для примера с зарплатой, чем стандартное отклонение.
Применим эти функции к встроенным данным trees:
> attach(trees)
> mean(Height)
[1] 76
> median(Height)
[1] 76
> sd(Height)
[1] 6.371813
> IQR(Height)
[1] 8
> detach(trees)
Видно, что для деревьев эти характеристики значительно ближе
друг к другу. Разумно предположить, что распределение высоты деревьев близко к нормальному. Мы проверим это ниже.
В наших данных по зарплате — всего 7 цифр. А как понять, есть
ли какие-то «выдающиеся» цифры, типа Катиной зарплаты, в данных
большого, «тысячного» размера? Для этого есть графические функции. Самая простая — так называемый «ящик-с-усами», или боксплот.
Для начала добавим к нашим данным еще тысячу гипотетических работников с зарплатой, случайно взятой из межквартильного разброса
исходных данных (рис. 9):
>
+
>
>

new.1000 hist(salary2, breaks=20, main="")
В нашем случае hist() по умолчанию разбивает переменную на 10
интервалов, но их количество можно указать вручную, как в предложенном примере. Численным аналогом гистограммы является функция
cut(). При помощи этой функции можно выяснить, сколько данных какого типа у нас имеется:
> table(cut(salary2, 20))
(10.9,15.5]
(15.5,20]
(20,24.6] (24.6,29.1] (29.1,33.7]

79

0

100

200

300

400

Как оценивать общую тенденцию

20

40

60

80

100

Рис. 11. Гистограмма зарплат 1007 гипотетических сотрудников
76
391
295
244
0
(33.7,38.3]
0
(38.3,42.8] (42.8,47.4] (47.4,51.9] (51.9,56.5] (56.5,61.1]
0
0
0
0
0
(61.1,65.6]
0
(65.6,70.2] (70.2,74.7] (74.7,79.3] (79.3,83.9] (83.9,88.4]
0
0
0
0
0
(88.4,93]
(93,97.5] (97.5,102]
0
0
1
Есть еще две графические функции, «идеологически близкие» к гистограмме. Во-первых, это stem() — псевдографическая (текстовая) гистограмма:
> stem(salary, scale=2)
The decimal point is 1 digit(s) to the right of the |

80

Великое в малом: одномерные данные

1
2
3
4
5
6
7
8
9
10

| 19
| 1157
|
|
|
|
|
|
|
| 2

Это очень просто — значения данных изображаются не точками, а
цифрами, соответствующими самим этим значениям. Таким образом,
видно, что в интервале от 10 до 20 есть две зарплаты (11 и 19), в интервале от 20 до 30 — четыре и т. д.
Другая функция тоже близка к гистограмме, но требует гораздо
более изощренных вычислений. Это график плотности распределения
(рис. 12):
> plot(density(salary2, adjust=2), main="")
> rug(salary2)
(Мы использовали «добавляющую» графическую функцию rug(),
чтобы выделить места с наиболее высокой плотностью значений.)
По сути, перед нами сглаживание гистограммы — попытка превратить ее в непрерывную гладкую функцию. Насколько гладкой она будет, зависит от параметра adjust (по умолчанию он равен единице).
Результат сглаживания называют еще графиком распределения.
Кроме боксплотов и различных графиков «семейства» гистограмм,
в R много и других одномерных графиков. График-«улей», например,
отражает не только плотность распределения значений выборки, но и
то, как расположены сами эти значения (точки). Для того чтобы построить график-улей, потребуется загрузить (а возможно, еще и установить сначала) пакет beeswarm. После этого можно поглядеть на сам
«улей» (рис. 13):
> library("beeswarm")
> beeswarm(trees)
> boxplot(trees, add=TRUE)
Мы здесь не просто построили график-улей, но еще и добавили туда боксплот, чтобы стали видны квартили и медиана. Для этого нам
понадобился аргумент add=TRUE.
Ну и, наконец, самая главная функция, summary():

81

0.00

0.02

0.04

0.06

0.08

Как оценивать общую тенденцию

20

40

60

80

100

Рис. 12. Плотность распределения зарплат 1007 гипотетических сотрудников
> lapply(list(salary, salary2), summary)
[[1]]
Min. 1st Qu. Median
Mean 3rd Qu.
11.00
20.00
21.00
32.29
26.00
Max.
102.00
[[2]]
Min. 1st Qu.
11.00
18.00
Max.
102.00

Median
21.00

Mean 3rd Qu.
21.09
24.00

Фактически она возвращает те же самые данные, что и fivenum()
с добавлением среднего значения (Mean). Заметьте, кстати, что у обеих
«зарплат» медианы одинаковы, тогда как средние существенно отличаются. Это еще один пример неустойчивости средних значений — ведь с

82

20

40

60

80

Великое в малом: одномерные данные

Girth

Height

Volume

Рис. 13. График-«улей» с наложенными боксплотами для трех характеристик деревьев
добавлением случайно взятых «зарплат» вид распределения не должен
был существенно поменяться.
Функция summary() — общая, и по законам объект-ориентированного подхода она возвращает разные значения для объектов разного типа.
Вы только что увидели, она работает для числовых векторов. Для списков она работает немного иначе. Вывод может быть, например, таким
(на примере встроенных данных attenu о 23 землетрясениях в Калифорнии):
> summary(attenu)
event
Min.
: 1.00
1st Qu.: 9.00
Median :18.00
Mean
:14.74
3rd Qu.:20.00

mag
Min.
:5.000
1st Qu.:5.300
Median :6.100
Mean
:6.084
3rd Qu.:6.600

station
117
: 5
1028
: 4
113
: 4
112
: 3
135
: 3

dist
Min.
: 0.50
1st Qu.: 11.32
Median : 23.40
Mean
: 45.60
3rd Qu.: 47.55

83

Ошибочные данные

Max.

:23.00

Max.

:7.700

(Other):147
NA’s
: 16

Max.

:370.00

accel
Min.
:0.00300
1st Qu.:0.04425
Median :0.11300
Mean
:0.15422
3rd Qu.:0.21925
Max.
:0.81000
Переменная station (номер станции наблюдений) — фактор, и к
тому же с пропущенными данными, поэтому отображается иначе.
Перед тем как завершить рассказ об основных характеристиках выборки, надо упомянуть еще об одной характеристике разброса. Для
сравнения изменчивости признаков (особенно таких, которые измерены в разных единицах измерения) часто применяют безразмерную величину — коэффициент вариации. Это просто отношение стандартного
отклонения к среднему, взятое в процентах. Вот так можно сравнить
коэффициент вариации для разных признаков деревьев (встроенные
данные trees):
> 100*sapply(trees, sd)/colMeans(trees)
Girth
Height
Volume
23.686948 8.383964 54.482331
Здесь мы для быстроты применили sapply() — вариант lapply() с
упрощенным выводом, и colMeans(), которая просто вычисляет среднее для каждой колонки. Сразу отметим, что подобных функций в
R несколько. Например, широко используются функции colSums() и
rowSums(), которые выдают итоговые суммы соответственно по колонкам и по строкам (главная функция электронных таблиц!). Есть, разумеется, еще и rowMeans().

4.2. Ошибочные данные
Способность функции summary() указывать пропущенные данные,
максимумы и минимумы служит очень хорошим подспорьем на самом
раннем этапе анализа данных — проверке качества. Предположим, у
нас есть данные, набранные с ошибками, и они находятся в директории
data внутри текущей директории:
> dir("data")
[1] "errors.txt" ...

84

Великое в малом: одномерные данные

> err str(err)
’data.frame’: 7 obs. of 3 variables:
$ AGE
: Factor w/ 6 levels "12","22","23",..: 3 4 3 5 1 6 2
$ NAME : Factor w/ 6 levels "","John","Kate",..: 2 3 1 4 5 6 2
$ HEIGHT: num 172 163 161 16.1 132 155 183
> summary(err)
AGE
NAME
HEIGHT
12:1
:1
Min.
: 16.1
22:1
John :2
1st Qu.:143.5
23:2
Kate :1
Median :161.0
24:1
Lucy :1
Mean
:140.3
56:1
Penny:1
3rd Qu.:167.5
a :1
Sasha:1
Max.
:183.0
Обработка начинается с проверки наличия нужного файла. Кроме
команды summary(), здесь использована также очень полезная команда
str(). Как видно, переменная AGE (возраст) почему-то стала фактором,
и summary() показывает, почему: в одну из ячеек закралась буква a.
Кроме того, одно из имен пустое, скорее всего, потому, что в ячейку
забыли поставить NA. Наконец, минимальный рост — 16.1 см! Такого не
бывает обычно даже у новорожденных, так что можно с уверенностью
утверждать, что наборщик просто случайно поставил точку.

4.3. Одномерные статистические тесты
Закончив разбираться с описательными статистиками, перейдем к
простейшим статистическим тестам (подробнее тесты рассмотрены в
следующей главе). Начнем с так называемых «одномерных», которые
позволяют проверять утверждения относительно того, как распределены исходные данные.
Предположим, мы знаем, что средняя зарплата в нашем первом примере — около 32 тыс. руб. Проверим теперь, насколько эта цифра достоверна:
> t.test(salary, mu=mean(salary))
One Sample t-test
data: salary
t = 0, df = 6, p-value = 1
alternative hypothesis: true mean is not equal to 32.28571
95 percent confidence interval:
3.468127 61.103302

Одномерные статистические тесты

85

sample estimates:
mean of x
32.28571
Это вариант теста Стьюдента для одномерных данных. Статистические тесты (в том числе и этот) пытаются высчитать так называемую тестовую статистику, в данном случае статистику Стьюдента (tстатистику). Затем на основании этой статистики рассчитывается «pвеличина» (p-value), отражающая вероятность ошибки первого рода.
А ошибкой первого рода (ее еще называют «ложной тревогой»),в свою
очередь, называется ситуация, когда мы принимаем так называемую
альтернативную гипотезу, в то время как на самом деле верна нулевая
(гипотеза «по умолчанию»). Наконец, вычисленная p-величина используется для сравнения с заранее заданным порогом (уровнем) значимости. Если p-величина ниже порога, нулевая гипотеза отвергается, если
выше — принимается. Подробнее о статистических гипотезах можно
прочесть в следующей главе.
В нашем случае нулевая гипотеза состоит в том, что истинное среднее (то есть среднее генеральной совокупности) равно вычисленному
нами среднему (то есть 32.28571).
Перейдем к анализу вывода функции. Статистика Стьюдента при
шести степенях свободы (df=6, поскольку у нас всего 7 значений) дает
единичное p-значение, то есть 100%. Какой бы распространенный порог мы не приняли (0.1%, 1% или 5%), это значение все равно больше.
Следовательно, мы принимаем нулевую гипотезу. Поскольку альтернативная гипотеза в нашем случае — это то, что «настоящее» среднее
(среднее исходной выборки) не равно вычисленному среднему, то получается, что «на самом деле» эти цифры статистически не отличаются. Кроме всего этого, функция выдает еще и доверительный интервал
(confidence interval), в котором, по ее «мнению», может находиться настоящее среднее. Здесь он очень широк — от трех с половиной тысяч
до 61 тысячи рублей.
Непараметрический (то есть не связанный предположениями о распределении) аналог этого теста тоже существует. Это так называемый
ранговый тест Уилкоксона:
> wilcox.test(salary2, mu=median(salary2), conf.int=TRUE)
Wilcoxon signed rank test with continuity correction
data: salary2
V = 221949, p-value = 0.8321
alternative hypothesis: true location is not equal to 21
95 percent confidence interval:
20.99999 21.00007

86

Великое в малом: одномерные данные

sample estimates:
(pseudo)median
21.00004
Эта функция и выводит практически то же самое, что и t.test()
выше. Обратите, однако, внимание, что этот тест связан не со средним,
а с медианой. Вычисляется (если задать conf.int=TRUE) и доверитель0
ный интервал. Здесь он значительно уже, потому что медиана намного
устойчивее среднего значения.
Понять, соответствует ли распределение данных нормальному (или
хотя бы близко ли оно к нормальному), очень и очень важно. Например, все параметрические статистические методы основаны на предположении о том, что данные имеют нормальное распределение. Поэтому в R реализовано несколько разных техник, отвечающих на вопрос
о нормальности данных. Во-первых, это статистические тесты. Самый
простой из них — тест Шапиро-Уилкса (попробуйте провести этот тест
самостоятельно):
> shapiro.test(salary)
> shapiro.test(salary2)
Но что же он показывает? Здесь функция выводит гораздо меньше, чем в предыдущих случаях. Более того, даже встроенная справка
не содержит объяснений того, какая здесь, например, альтернативная
гипотеза. Разумеется, можно обратиться к литературе, благо справка
дает ссылки на публикации. А можно просто поставить эксперимент:
> set.seed(1638)
> shapiro.test(rnorm(100))
Shapiro-Wilk normality test
data: rnorm(100)
W = 0.9934, p-value = 0.9094
rnorm() генерирует столько случайных чисел, распределенных по
нормальному закону, сколько указано в его аргументе. Это аналог функции sample(). Раз мы получили высокое p-значение, то это свидетельствует о том, что альтернативная гипотеза в данном случае: «распределение не соответствует нормальному». Кроме того, чтобы результаты
при вторичном воспроизведении были теми же, использована функция
set.seed(), регулирующая встроенный в R генератор случайных чисел
так, чтобы числа в следующей команде были созданы по одному и тому
же «закону».

87

Одномерные статистические тесты

Таким образом, распределение данных в salary и salary2 отличается от нормального.
Другой популярный способ проверить, насколько распределение похоже на нормальное,— графический. Вот как это делается (рис. 14):

20

40

60

80

100

> qqnorm(salary2, main="")
> qqline(salary2, col=2)

−3

−2

−1

0

1

2

3

Рис. 14. Графическая проверка нормальности распределения
Для каждого элемента вычисляется, какое место он занимает в сортированных данных (так называемый «выборочный квантиль») и какое
место он должен был бы занять, если распределение нормальное (теоретический квантиль). Прямая проводится через квартили. Если точки
лежат на прямой, то распределение нормальное. В нашем случае многие точки лежат достаточно далеко от красной прямой, а значит, не
похожи на распределенные нормально.
Для проверки нормальности можно использовать и более универсальный тест Колмогорова—Смирнова, который сравнивает любые два

88

Великое в малом: одномерные данные

распределения, поэтому для сравнения с нормальным распределением
ему надо прямо указать «pnorm», то есть так называемую накопленную
функцию нормального распределения (она встроена в R):
> ks.test(salary2, "pnorm")
One-sample Kolmogorov-Smirnov test
data: salary2
D = 1, p-value < 2.2e-16
alternative hypothesis: two-sided
Он выдает примерно то же, что и текст Шапиро-Уилкса.

4.4. Как создавать свои функции
Тест Шапиро-Уилкса всем хорош, но не векторизован, как и многие
другие тесты в R, поэтому применить его сразу к нескольким колонкам
таблицы данных не получится. Сделано это нарочно, для того чтобы
подчеркнуть нежелательность множественных парных сравнений (об
этом подробнее написано в главе про двумерные данные). Но в нашем
случае парных сравнений нет, а сэкономить время хочется. Можно, конечно, нажимая «стрелку вверх», аккуратно повторить тест для каждой колонки, но более правильный подход — создать пользовательскую
функцию. Вот пример такой функции:
>
+
+
+
+
+
+
+
+
+
+
+

normality
+
+
+
+

91

normality3
p, "NORMAL", "NOT NORMAL"))
}

> normality3(list(salary, salary2))
> normality3(log(trees+1))
Примеры тоже интересны. Во-первых, нашу третью функцию можно применять не только к таблицам данных, но и к «настоящим» спискам, с неравной длиной элементов. Во-вторых, простейшее логарифмическое преобразование сразу же изменило «нормальность» колонок.

4.5. Всегда ли точны проценты
Полезной характеристикой при исследовании данных является пропорция (доля). В статистике под пропорцией понимают отношение объектов с исследуемой особенностью к общему числу наблюдений. Поскольку доля — это часть целого, то отношение части к целому находится в пределах от 0 до 1. Для удобства восприятия доли ее умножают
на 100% и получают процент — число в пределах от 0% до 100%. Следует внимательно относиться к вычислению долей и не забывать про
исходные данные. В 1960-е годы в районном отделе по сельскому хозяйству было обнаружено, что падеж лошадей в одном сельском поселении
составил 50%. Пришлось составить компетентную комиссию для проверки причин такого ужасного явления. Прибыв на место, комиссия
обнаружила, что в данном поселении было всего две лошади, в том
числе и сдохшая недавно старая кляча, которая и послужила причиной
формирования и выезда на место комиссии!
Рассмотрим проблему, которая часто встречается при проведении
статистических исследований. Как узнать, отличается ли вычисленный
нами процент от «истинного» процента, то есть доли интересующих нас
объектов в генеральной совокупности?
Вот пример. В больнице есть группа из 476 пациентов, среди которых 356 курят. Мы знаем, что в среднем по больнице доля курящих
составляет 0.7 (70%). А вот в нашей группе она чуть побольше — примерно 75%. Для того чтобы проверить гипотезу о том, что доля курящих в рассматриваемой группе пациентов отличается от средней доли
по больнице, мы можем задействовать так называемый биномиальный
тест:
> binom.test(x=356, n=476, p=0.7, alternative="two.sided")

92

Великое в малом: одномерные данные

Exact binomial test
data: 356 and 476
number of successes = 356, number of trials = 476,
p-value = 0.02429
alternative hypothesis: true probability of success is not
equal to 0.7
95 percent confidence interval:
0.7063733 0.7863138
sample estimates:
probability of success
0.7478992
Поскольку p-значение значительно меньше 0.05 и альтернативная
гипотеза состоит в том, что доля курильщиков (она здесь довольно
издевательски называется «probability of success», «вероятность успеха») не равна 0.7, то мы можем отвергнуть нулевую гипотезу и принять альтернативную, то есть решить, что наши 74% отличаются от
средних по больнице 70% не случайно. В качестве опции мы использовали alternative="two.sided", но можно было поступить иначе —
проверить гипотезу о том, что доля курящих в рассматриваемой группе пациентов превышает среднюю долю курящих по больнице. Тогда
альтернативную гипотезу надо было бы записать как alt="greater".
Кроме биномиального, мы можем применить здесь и так называемый тест пропорций. Надо заметить, что он применяется шире, потому
что более универсален:
> prop.test(x=356, n=476, p=0.7, alternative="two.sided")
1-sample proportions test with continuity correction
data: 356 out of 476, null probability 0.7
X-squared = 4.9749, df = 1, p-value = 0.02572
alternative hypothesis: true p is not equal to 0.7
95 percent confidence interval:
0.7059174 0.7858054
sample estimates:
p
0.7478992
Как видим, результат практически такой же.
Тест пропорций можно проводить и с двумя выборками, для этого используется все та же функция prop.test() (для двухвыборочного

Всегда ли точны проценты

93

текста пропорций), а также mcnemar.test() (для теста Мак-Немара, который проводится тогда, когда выборки связаны друг с другом). Узнать,
как их использовать, можно, прочитав справку (и особенно примеры!)
по обеим функциям.
***
Ответ к задаче про функцию normality(). Чтобы узнать, откуда взять значения p-value, надо сначала вспомнить, что в R почти все,
что выводится на экран,— это результат «печати» различных списков
при помощи невидимой команды print(). А из списка легко «достать»
нужное либо по имени, либо по номеру (если, скажем, имени у элементов нет — так иногда бывает). Сначала выясним, из чего состоит
список-вывод функции shapiro.test():
> str(shapiro.test(rnorm(100)))
List of 4
$ statistic: Named num 0.992
..- attr(*, "names")= chr "W"
$ p.value : num 0.842
$ method
: chr "Shapiro-Wilk normality test"
$ data.name: chr "rnorm(100)"
- attr(*, "class")= chr "htest"
В этом списке из 4 элементов есть элемент под названием p.value,
что нам и требовалось. Проверим на всякий случай, то ли это:
> set.seed(1683)
> shapiro.test(rnorm(100))$p.value
[1] 0.8424077
Именно то, что надо. Осталось только вставить это название в нашу
функцию.
Ответ к задаче про выборы из предисловия.
Для того чтобы рассчитать p-value для нашей пропорции (48% проголосовавших за кандидата B), можно воспользоваться описанным выше prop.test():
> prop.test(0.48*262, 262)
1-sample proportions test with continuity correction
data: 0.48 * 262 out of 262, null probability 0.5
X-squared = 0.343, df = 1, p-value = 0.5581

94

Великое в малом: одномерные данные

alternative hypothesis: true p is not equal to 0.5
95 percent confidence interval:
0.4183606 0.5422355
sample estimates:
p
0.48
Получается, что отвергнуть нулевую гипотезу о равенстве пропорций мы не можем, слишком велико p-value (гораздо больше «положенных» 0.05 и даже «либеральных» 0.1). Процент же проголосовавших
может лежать в интервале от 42% до 54%! Итак, по результатам опроса
нельзя сказать, что кандидат А победил.
А вот так можно рассчитать количество человек, которых надо опросить, чтобы быть уверенным в том, что 48% и 52% и вправду отражают
реальную ситуацию (генеральную совокупность):
> power.prop.test(p1=0.48, p2=0.52, power=0.8)
Two-sample comparison of proportions power calculation
n
p1
p2
sig.level
power
alternative

=
=
=
=
=
=

2451.596
0.48
0.52
0.05
0.8
two.sided

NOTE: n is number in *each* group
Получается, что опросить надо было примерно 5 тысяч человек!
Мы использовали здесь так называемый power-тест, который часто
применяется для прогнозирования эксперимента. Мы задали значение
power = 0.8, потому что именно такое значение является общепринятым порогом значимости (оно соответствует p-value < 0.1). Чуть более подробно про мощность (power) и ее связь с другими характеристиками теста можно прочитать в следующей главе.

Глава 5
Анализ связей: двумерные данные
Здесь речь пойдет о том, как работать с двумя выборками. Если у
нас два набора цифр, то первое, что приходит в голову,— сравнить их.
Для этого нам потребуются статистические тесты.

5.1. Что такое статистический тест
Настала пора подробнее познакомиться с ядром статистики — тестами и гипотезами. В предыдущей главе мы уже коротко объясняли,
как строятся статистические гипотезы, но если анализ одной выборки
можно сделать, не вдаваясь подробно в их смысл, то в анализе связей
без понимания этих принципов не обойтись.

5.1.1. Статистические гипотезы
Из первой главы мы знаем, что статистическая выборка должна
быть репрезентативной (то есть адекватно характеризовать генеральную совокупность, или, как ее еще называют, популяцию). Но как же
мы можем знать, репрезентативна ли выборка, если мы не исследовали
всю генеральную совокупность? Этот логический тупик называют парадоксом выборки. Хотя мы и обеспечиваем репрезентативность выборки
соблюдением двух основных принципов ее создания (рандомизации и
повторности), неопределенность все же остается. Кроме того, если мы
принимаем вероятностную точку зрения на происхождение наших данных (которые получены путем случайного выбора), то все дальнейшие
суждения, основанные на этих данных, будут иметь вероятностный характер. Таким образом, мы никогда не сможем на основании нашей
выборки со стопроцентной уверенностью судить о свойствах генеральной совокупности! Мы можем лишь выдвигать гипотезы и вычислять
их вероятность.
Великие философы науки (например, Карл Поппер) постулировали,
что мы ничего не можем доказать, мы можем лишь что-нибудь опровергнуть. Действительно, если мы соберем 1000 фактов, подтверждающих какую-нибудь теорию, это не будет значить, что мы ее доказали.

96

Анализ связей: двумерные данные

Вполне возможно, что 1001-ый (или 1 000 001-ый) факт опровергнет эту
теорию. Поэтому в любом статистическом тесте выдвигаются две противоположные гипотезы. Одна — это то, что мы хотим доказать (но
не можем!),— называется альтернативной гипотезой (ее обозначают
H1 ). Другая — противоречащая альтернативной — называется нулевой
гипотезой (обозначается H0 ). Нулевая гипотеза всегда является предположением об отсутствии чего-либо (например, зависимости одной
переменной от другой или различия между двумя выборками). Мы не
можем доказать альтернативную гипотезу, а можем лишь опровергнуть нулевую гипотезу и принять альтернативную (чувствуете разницу?). Если же мы не можем опровергнуть нулевую гипотезу, то мы
вынуждены принять ее.

5.1.2. Статистические ошибки
Естественно, что когда мы делаем любые предположения (в нашем
случае — выдвигаем статистические гипотезы), мы можем ошибаться
(в нашем случае — делать статистические ошибки). Всего возможно
четыре исхода (табл. 5.1).
Популяция

Верна H0

Верна H1

Принимаем H0

Правильно!

Статистическая
ошибка
второго рода

Принимаем H1

Статистическая
ошибка
первого рода

Правильно!

Выборка

Таблица 5.1. Статистические ошибки
Если мы приняли для выборки H0 (нулевую гипотезу) и она верна для генеральной совокупности (популяции), то мы правы, и все в
порядке. Аналогично и для H1 (альтернативной гипотезы). Ясно, что
мы не можем знать, что в действительности верно для генеральной совокупности, и сейчас просто рассматриваем все логически возможные
варианты.
А вот если мы приняли для выборки альтернативную гипотезу, а
она оказалась неверна для генеральной совокупности, то мы совершили так называемую статистическую ошибку первого рода (нашли несуществующую закономерность). Вероятность того, что мы совершили эту ошибку (это и есть p-значение, «p-value»), всегда отображается

Есть ли различие, или Тестирование двух выборок

97

при проведении любых статистических тестов. Очевидно, что если вероятность этой ошибки достаточно высока, то мы должны отвергнуть
альтернативную гипотезу. Возникает естественный вопрос: какую вероятность считать достаточно высокой? Так же как и с размером выборки (см. первую главу), однозначного ответа на этот вопрос нет. Более
или менее общепринято, что пороговым значением надо считать 0.05
(то есть альтернативная гипотеза отвергается, если вероятность ошибки при ее принятии больше или равна 5%). В медицине ценой ошибки
нередко являются человеческие жизни, поэтому там пороговое значение часто принимают равным 0.01 или даже 0.001 (то есть решение о
существовании закономерности принимается, если вероятность ошибки
ничтожна).
Таким образом, решение о результатах статистических тестов принимается главным образом на основании вероятности статистической
ошибки первого рода. Степень уверенности исследователя в том, что заключение, сделанное на основании статистической выборки, будет справедливо и для генеральной совокупности, отражает статистическая достоверность. Допустим, если вероятность статистической ошибки первого рода равна 3%, то говорят, что найденная закономерность достоверна с вероятностью 97%. А если вероятность статистической ошибки
первого рода равна, например, 23%, то говорят, что достоверной закономерности не найдено.
В случае, если мы принимаем нулевую гипотезу для выборки, в то
время как для генеральной совокупности справедлива альтернативная
гипотеза, то мы совершаем статистическую ошибку второго рода (не замечаем существующей закономерности). Этот параметр характеризует
так называемую мощность (power) статистического теста. Чем меньше
вероятность статистической ошибки второго рода (то есть чем меньше
вероятность не заметить несуществующую закономерность), тем более
мощным является тест.
Для полной ясности перерисуем нашу таблицу для конкретного случая — исследования зависимости между двумя признаками (табл. 5.2).

5.2. Есть ли различие, или Тестирование двух
выборок
При ответе на этот вопрос нужно всегда помнить, что упомянутые
ниже тесты проверяют различия только по центральным значениям
(например, средним) и подразумевают, что разброс данных в выборках
примерно одинаков. Например, выборки с одинаковыми параметрами
средней тенденции и разными показателями разброса данных, c(1, 2,

98

Выборка

Анализ связей: двумерные данные
Популяция

Верна H0

Верна H1

Принимаем H0

Принимаем H1

Таблица 5.2. Статистические ошибки при исследовании зависимости
между двумя признаками. Мелкие точки отражают объекты генеральной совокупности (популяции), крупные точки — объекты из нашей
выборки
3, 4, 5, 6, 7, 8, 9) и c(5, 5, 5, 5, 5, 5, 5, 5, 5) различаться
не будут.
Как вы помните, для проведения статистического теста нужно выдвинуть две статистические гипотезы. Нулевая гипотеза здесь: «различий между (двумя) выборками нет» (то есть обе выборки взяты из одной генеральной совокупности). Альтернативная гипотеза: «различия
между (двумя) выборками есть».
Напоминаем, что ваши данные должны быть организованы в виде
двух векторов — отдельных или объединенных в лист или таблицу данных. Например, если нужно узнать, различается ли достоверно рост
мужчин и женщин, то в одном векторе должен быть указан рост мужчин, а в другом — рост женщин (каждая строчка — это один обследованный человек).
Если данные параметрические, то нужно провести параметрический
«t-тест» (или «тест Стьюдента»). Если при этом переменные, которые
мы хотим сравнить, были получены на разных объектах, мы будем использовать двухвыборочный t-тест для независимых переменных (two
sample t-test), который запускается при помощи команды t.test(). Например, если две сравниваемые выборки записаны в первом и втором
столбцах таблицы данных data, то команда t.test(data[,1], data[,2])

Есть ли различие, или Тестирование двух выборок

99

по умолчанию выдаст нам результаты двухвыборочного t-теста для
независимых переменных.
Если же пары сравниваемых характеристик были получены на одном объекте, то есть переменные зависимы (например, частота пульса
до и после физической нагрузки измерялась у одного и того же человека), надо использовать парный t-тест (paired t-test). Для этого в команде t.test() надо указать параметр paired=TRUE. В нашем примере, если данные зависимы, надо использовать команду вида t.test(data[,1],
data[,2], paired=TRUE).
Второй тест, тест для зависимых переменных, более мощный. Представьте себе, что мы измеряли пульс до нагрузки у одного человека,
а после нагрузки — у другого. Тогда было бы не ясно, как объяснить
полученную разницу: может быть, частота пульса увеличилась после
нагрузки, а может быть, этим двум людям вообще свойственна разная
частота пульса. В случае же «двойного» измерения пульса каждый человек как бы является своим собственным контролем, и разница между
сравниваемыми переменными (до и после нагрузки) обусловливается
только тем фактором, на основе которого они выделены (наличием нагрузки).
Если мы имеем дело с непараметрическими данными, то нужно
провести непараметрический двухвыборочный тест Вилкоксона, «Wilcoxon test» (он известен еще и как как тест Манна-Уитни, «MannWhitney test»). Для этого надо использовать команду wilcox.test().
В случае с зависимыми выборками, аналогично t-тесту, надо использовать параметр paired=TRUE.
Приведем несколько примеров. Для начала воспользуемся классическим набором данных, который использовался в оригинальной работе
Стьюдента (псевдоним ирландского математика Уильяма Сили Госсета). В упомянутой работе производилось сравнение влияния двух различных снотворных на увеличение продолжительности сна (рис. 15).
В R эти данные доступны под названием sleep. В столбце extra содержится среднее приращение продолжительности сна после начала
приема лекарства (по отношению к контрольной группе), а в столбце
group — код лекарства (первое или второе).
> plot(extra ~ group, data = sleep)
Здесь использована так называемая «формула модели»: в рассматриваемом случае extra ~ group означает, что group используется для
разбивки extra.
Влияние лекарства на каждого человека индивидуально, но среднее увеличение продолжительности сна можно считать вполне логичным показателем «силы» лекарства. Основываясь на этом предположе-

100

−1

0

1

2

3

4

5

Анализ связей: двумерные данные

1

2

Рис. 15. Среднее приращение продолжительности сна после начала приема разных лекарств в двух группах по отношению к контрольной
нии, попробуем проверить при помощи t-теста, значимо ли различие в
средних для этих двух выборок (соответствующих двум разным лекарствам):
> with(sleep, t.test(extra[group == 1], extra[group == 2],
+ var.equal = FALSE))
Welch Two Sample t-test
data: extra[group == 1] and extra[group == 2]
t = -1.8608, df = 17.776, p-value = 0.0794
alternative hypothesis: true difference in means is not
equal to 0
95 percent confidence interval:
-3.3654832 0.2054832
sample estimates:
mean of x mean of y

Есть ли различие, или Тестирование двух выборок

0.75

101

2.33

Параметр var.equal позволяет выбрать желаемый вариант критерия: оригинальный t-критерий Стьюдента в предположении, что разбросы данных одинаковы(var.equal = TRUE), или же t-тест в модификации Уэлча (Welch), свободный от этого предположения (var.equal =
FALSE).
Хотя формально мы не можем отвергнуть нулевую гипотезу (о равенстве средних), p-значение (0.0794) все же достаточно маленькое, чтобы попробовать другие методы для проверки гипотезы, увеличить количество наблюдений, еще раз убедиться в нормальности распределений и т. д. Может прийти в голову провести односторонний тест — ведь
он обычно чувствительнее. Так вот, этого делать нельзя! Нельзя потому, что большинство статистических тестов рассчитаны на то, что они
будут проводиться ad hoc, то есть без знания какой-либо дополнительной информации. Существуют и post hoc тесты (скажем, обсуждаемый
ниже «Tukey Honest Significant Differences test»), но их немного.
Для сравнения двух выборок существуют, разумеется, и непараметрические тесты. Один из них, тест знаков, настолько прост, что его нет
в R. Можно, однако, легко сделать его самому. Тест знаков вычисляет
разницу между всеми парами элементов двух одинаковых по размеру
выборок (то есть это парный тест). Затем можно отбросить все отрицательные значения и оставить только положительные. Если выборки
взяты из одной генеральной совокупности, то положительных разниц
будет примерно половина, и тогда уже знакомый нам биномиальный
тест не найдет отличий между 50% и пропорцией положительных разниц. Если выборки разные, то пропорция положительных разниц будет
значимо больше или меньше половины.
Задача. Придумайте, при помощи какого R-выражения сделать такой тест, и протестируйте две выборки, которые мы привели в начале
раздела.
Перейдем тем временем к более сложным непараметрическим тестам. Рассмотрим пример. Стандартный набор данных airquality содержит информацию о величине озона в воздухе города Нью-Йорка с
мая по сентябрь 1973 года. Концентрация озона представлена округленными средними значениями за день, поэтому анализировать ее «от
греха подальше» лучше непараметрическими методами. Впрочем, попробуйте выяснить самостоятельно (это задача!), насколько близки к
нормальным помесячные распределения значений концентраций озона
(см. ответ в конце главы).
Проверим, например, гипотезу о том, что распределение уровня озона в мае и в августе было одинаковым:
> wilcox.test(Ozone ~ Month, data = airquality,

102

Анализ связей: двумерные данные

+ subset = Month %in% c(5, 8))
Wilcoxon rank sum test with continuity correction
data: Ozone by Month
W = 127.5, p-value = 0.0001208
alternative hypothesis: true location shift is not equal to 0
Поскольку Month дискретен (это просто номер месяца), то значения
Ozone будут сгруппированы помесячно. Кроме того, мы использовали
параметр subset вместе с оператором %in%, который выбирает изо всех
месяцев май и август (пятый и восьмой месяцы).
Критерий отвергает гипотезу о согласии распределений уровня озона в мае и в августе с достаточно большой надежностью. В это достаточно легко поверить, потому что уровень озона в воздухе сильно
зависит от солнечной активности, температуры и ветра.
Различия между выборками хорошо иллюстрировать при помощи
графика «ящик-с-усами», например (рис. 16):
> boxplot(Ozone ~ Month, data = airquality,
+ subset = Month %in% c(5, 8))
(Обратите внимание на то, что здесь для boxplot() мы использовали ту же самую формулу модели.)
Часто считают, что если «ящики» перекрываются более чем на треть
своей высоты, то выборки достоверно не различаются.
Можно использовать t-тест и тест Вилкоксона и для одной выборки, если стоит задача сравнить ее с неким «эталоном». Поскольку выборка одна, то и соответствующие тесты называют одновыборочными.
Нулевую гипотезу в этом случае надо формулировать как равенство
выборочной средней или медианы (для t-теста и теста Вилкоксона, соответственно) заданному числу µ (см. предыдущую главу).
Задача. В файле данных otsenki.txt записаны оценки одних и тех
же учеников некоторого школьного класса за первую четверть (значение A1 во второй колонке) и за вторую четверть (A2), а также оценки
учеников другого класса за первую четверть (B1). Отличаются ли результаты класса A за первую и вторую четверть? Какой класс учился
в первой четверти лучше — A или B? Ответ см. в конце главы.
Задача. В супермаркете, среди прочих, работают два кассира. Для
того чтобы проанализировать эффективность их работы, несколько раз
в день в середине недели подсчитывали очереди к каждой из них. Данные записаны в файле kass.txt. Какой кассир работает лучше?

103

0

50

100

150

Есть ли соответствие, или Анализ таблиц

5

8

Рис. 16. Распределение озона в воздухе в мае и июне

5.3. Есть ли соответствие, или Анализ таблиц
А как сравнить между собой две выборки из номинальных (категориальных) данных? Для этого часто используют таблицы сопряженности (contigency tables). Построить таблицу сопряженности можно с
помощью функции table():
> with(airquality, table(cut(Temp, quantile(Temp)), Month))
Month
5 6 7 8 9
(56,72] 24 3 0 1 10
(72,79] 5 15 2 9 10
(79,85] 1 7 19 7 5
(85,97] 0 5 10 14 5
Строки этой таблицы — четыре интервала температур (по Фаренгейту), а столбцы — месяц года. На пересечении каждой строки и столбца

104

Анализ связей: двумерные данные

находится число, которое показывает, сколько значений в данном месяце попадает в данный интервал температур.
Если факторов больше двух, то R будет строить многомерную таблицу и выводить ее в виде серии двумерных таблиц, что не всегда удобно.
Можно, однако, построить «плоскую» таблицу сопряженности, когда
все факторы, кроме одного, объединяются в один «многомерный» фактор, чьи градации используются при построении таблицы. Построить
такую таблицу можно с помощью функции ftable():
> ftable(Titanic, row.vars = 1:3)
Survived
Class Sex
1st
Male
Female
2nd

Male
Female

3rd

Male
Female

Crew

Male
Female

Age
Child
Adult
Child
Adult
Child
Adult
Child
Adult
Child
Adult
Child
Adult
Child
Adult
Child
Adult

No Yes
0
5
118 57
0
1
4 140
0 11
154 14
0 13
13 80
35 13
387 75
17 14
89 76
0
0
670 192
0
0
3 20

Параметр row.vars позволяет указать номера переменных в наборе данных, которые следует объединить в один-единственный фактор,
градации которого и будут индексировать строки таблицы сопряженности. Параметр col.vars проделывает то же самое, но для столбцов
таблицы.
Функцию table можно использовать и для других целей. Самое
простое — это подсчет частот. Например, можно считать пропуски:
>
+
>
>

d titanic
Survived
Class
No Yes
1st 122 203
2nd 167 118
3rd 528 178
Crew 673 212
> mosaicplot(titanic, col = c("red", "green"), main = "",
+ cex.axis=1)
При помощи функции chisq.test() можно статистически проверить гипотезу о независимости двух факторов. К данным будет применен тест хи-квадрат (Chi-squared test). Например, проверим гипотезу о
независимости цвета глаз и волос (встроенные данные HairEyeColor):
> x chisq.test(x)
Pearson’s Chi-squared test
data: x
X-squared = 138.2898, df = 9, p-value < 2.2e-16
(Того же эффекта можно добиться, если функции summary() передать таблицу сопряженности в качестве аргумента.)
Набор данных HairEyeColor — это многомерная таблица сопряженности. Для суммирования частот по всем «измерениям», кроме двух,
использовалась функция margin.table. Таким образом, в результате
была получена двумерная таблица сопряженности. Тест хи-квадрат в
качестве нулевой гипотезы принимает независимость факторов, так что
в нашем примере (поскольку мы отвергаем нулевую гипотезу) следует
принять, что факторы обнаруживают соответствие.
Чтобы графически изобразить эти соответствия, можно воспользоваться функцией assocplot() (рис. 18):
> x assocplot(x)

106

Анализ связей: двумерные данные
2nd

3rd

Crew

Yes

Survived

No

1st

Class

Рис. 17. Доля выживших на «Титанике» пассажиров и членов команды
в зависимости от класса их билета: мозаичный график
На графике видны отклонения ожидаемых частот от наблюдаемых
величин. Высота прямоугольника показывает абсолютную величину этого отклонения, а положение — знак отклонения. Отчетливо видно, что
для людей со светлыми волосами характерен голубой цвет глаз и совсем
не характерен карий цвет, а для обладателей черных волос ситуация обратная.
Чтобы закрепить изученное о таблицах сопряженности, рассмотрим
еще один интересный пример. Однажды большая компания статистиковэпидемиологов собралась на банкет. На следующее утро после банкета,
в воскресенье, многие встали с симптомами пищевого отравления, а в
понедельник в институте много сотрудников отсутствовало по болезни.
Поскольку это были статистики, и не просто статистики, а эпидемиологи, то они решили как следует вспомнить, кто что ел на банкете, и
тем самым выяснить причину отравления. Получившиеся данные имеют следующий формат:
> tox head(tox)
ILL CHEESE CRABDIP CRISPS BREAD CHICKEN RICE CAESAR TOMATO
1
1
1
1
1
2
1
1
1
1
2
2
1
1
1
2
1
2
2
2
3
1
2
2
1
2
1
2
1
2
4
1
1
2
1
1
1
2
1
2
5
1
1
1
1
2
1
1
1
1
6
1
1
1
1
1
1
2
1
1
ICECREAM CAKE JUICE WINE COFFEE
1
1
1
1
1
1
2
1
1
1
1
2
3
1
1
2
1
2
4
1
1
2
1
2
5
2
1
1
1
1
6
2
1
1
2
2

108

Анализ связей: двумерные данные

Первая переменная (ILL) показывает, отравился участник банкета
(1) или нет (2), остальные переменные соответствуют разным блюдам.
Простой взгляд на данные ничего не даст, ведь на банкете было 45
человек и 13 блюд! Значит, надо попробовать статистические методы.
Так как данные номинальные, то можно попробовать проанализировать
таблицы сопряженности1 :
> for (m in 2:ncol(tox))
+ {
+ tmp assocplot(table(ILL=tox$ILL, CAESAR=tox$CAESAR))
Практически такая же картина и по тем, кто ел помидоры. Виновник найден! Почти. Ведь вряд ли испорченными были сразу оба блюда.
Надо теперь попробовать разобраться, что же было главной причиной.
Для этого вам придется заглянуть ниже, туда, где мы знакомимся с
логистической регрессией.
1

На предупреждения можно не обращать внимания. Полученные значения лучше
дополнительно обработать с помощью p.adjust().

109

2

CAESAR

1

Есть ли соответствие, или Анализ таблиц

1

2
ILL

Рис. 19. Соответствие между отравившимися и теми, кто ели цезарский
салат
***
Кроме методов работы с таблицами сопряженности (таких, например, как тест хи-квадрат), номинальные и шкальные данные можно
обрабатывать различными, более специализированными методами. Например, для сравнения результатов экспертных оценок популярны так
называемые тесты согласия (concordance). Среди этих тестов широко
распространен тест Коэна (Cohen), который вычисляет так называемую каппу Коэна (Cohen’s kappa) — меру согласия, изменяющуюся от
0 до 1, и вдобавок вычисляет p-значение для нулевой гипотезы (о том,
что каппа равна 0). Приведем такой пример: в 2003 году две группы
независимо (и примерно в одно и то же время года) обследовали один
и тот же остров Белого моря. Целью было составить список всех видов
растений, встреченных на острове. Таким образом, данные были бинарными (0 — вида на острове нет, 1 — вид на острове есть). Результаты
обследований записаны в файл pokorm03.dat:

110

Анализ связей: двумерные данные

> pok library(psych)
> cohen.kappa(pok)
...
Cohen Kappa and Weighted Kappa correlation coefficients
and confidence boundaries
lower estimate upper
unweighted kappa 0.55
0.67
0.8
weighted kappa
0.55
0.67
0.8
Number of subjects = 193
(Мы загрузили пакет psych, потому что именно в нем находится
нужная нам функция cohen.kappa().)
Каппа довольно высока, нижний предел 95% доверительного интервала выше 0.5. Это значит, что результаты исследования можно считать
согласными друг с другом.
Задача. В файле данных prorostki.txt находятся результаты эксперимента по проращиванию семян васильков, зараженных различными грибами (колонка CID, CID=0 — это контроль, то есть незараженные
семена). Всего исследовали по 20 семян на каждый гриб, тестировали
три гриба, итого с контролем — 80 семян. Отличается ли прорастание
семян, зараженных грибами, от контроля? Ответ см. в конце главы.

5.4. Есть ли взаимосвязь, или Анализ
корреляций
Мерой линейной взаимосвязи между переменными является коэффициент корреляции Пирсона (обозначается латинской буквой r ). Значения коэффициента корреляции могут изменяться по модулю от нуля до единицы. Нулевой коэффициент корреляции говорит о том, что
значения одной переменной не связаны со значениями другой переменной, а коэффициент корреляции, равный единице (или минус единице),
свидетельствует о четкой линейной связи между переменными. Положительный коэффициент корреляции говорит о положительной взаимосвязи (чем больше, тем больше), отрицательный — об отрицательной
(чем больше, тем меньше).
Степень взаимосвязи между переменными отражает коэффициент
детерминации: это коэффициент корреляции, возведенный в квадрат.
Эта величина показывает, какая доля изменений значений одной переменной сопряжена с изменением значений другой переменной. Например, если коэффициент корреляции увеличится вдвое (0.8), то степень
взаимосвязи между переменными возрастет в четыре раза (0.82 = 0.64).

Есть ли взаимосвязь, или Анализ корреляций

111

Повторим еще раз, что коэффициент корреляции характеризует меру линейной связи между переменными. Две переменные могут быть
очень четко взаимосвязаны, но если эта связь не линейная, а, допустим, параболическая, то коэффициент корреляции будет близок к нулю. Примером такой связи может служить связь между степенью возбужденности человека и качеством решения им математических задач.
Очень слабо возбужденный человек (скажем, засыпающий) и очень
сильно возбужденный (во время футбольного матча) будет решать задачи гораздо хуже, чем умеренно возбужденный человек. Поэтому, перед тем как оценить взаимосвязь численно (вычислить коэффициент
корреляции), нужно посмотреть на ее графическое выражение. Лучше всего здесь использовать диаграмму рассеяния, или коррелограмму
(scatterplot) — именно она вызывается в R командой plot() с двумя
аргументами-векторами.
Очень важно также, что всюду в этом разделе речь идет о наличии
и силе взаимосвязи между переменными, а не о характере этой взаимосвязи. Если мы нашли достоверную корреляцию между переменными
А и Б, то это может значить, что:
• А зависит от Б;
• Б зависит от А;
• А и Б зависят друг от друга;
• А и Б зависят от какой-то третьей переменной В, а между собой
не имеют ничего общего.
Например, хорошо известно, что объем продаж мороженого и число пожаров четко коррелируют. Странно было бы предположить, что
поедание мороженого располагает людей к небрежному обращению с
огнем или что созерцание пожаров возбуждает тягу к мороженому. Все
гораздо проще — оба этих параметра зависят от температуры воздуха!
Для вычисления коэффициента корреляции в R используется функция cor():
> cor(5:15, 7:17)
[1] 1
> cor(5:15, c(7:16, 23))
[1] 0.9375093
В простейшем случае ей передаются два аргумента (векторы одинаковой длины). Кроме того, можно вызвать ее с одним аргументом, если
это — матрица или таблица данных. В этом последнем случае функ-

112

Анализ связей: двумерные данные

ция cor() вычисляет так называемую корреляционную матрицу, составленную из коэффициентов корреляций между столбцами матрицы
или набора данных, взятых попарно, например:
> cor(trees)
Girth
Height
Volume
Girth 1.0000000 0.5192801 0.9671194
Height 0.5192801 1.0000000 0.5982497
Volume 0.9671194 0.5982497 1.0000000
Если все данные присутствуют, то все просто, но что делать, когда
есть пропущенные наблюдения? Для этого в команде cor есть параметр use. По умолчанию он равен all.obs, что при наличии хотя бы
одного пропущенного наблюдения приводит к сообщению об ошибке.
Если use приравнять к значению complete.obs, то из данных автоматически удаляются все наблюдения с пропусками. Может оказаться
так, что пропуски раскиданы по исходному набору данных достаточно
хаотично и их много, так что после построчного удаления от матрицы
фактически ничего не остается. В таком случае поможет попарное удаление пропусков, то есть удаляются строчки с пропусками не из всей
матрицы сразу, а только лишь из двух столбцов непосредственно перед
вычислением коэффициента корреляции. Для этого опцию use следует
приравнять к значению pairwise.complete.obs (надо иметь в виду, что
в в этом коэффициенты корреляции вычисляются по разному количеству наблюдений и сравнивать их друг с другом может быть опасно).
Если данные непараметрические, нужно использовать ранговый коэффициент корреляции Спирмена (Spearman) ρ. Он менее подвержен
влиянию случайных «выбросов» в данных, чем коэффициент Пирсона.
Для подсчета ρ достаточно приравнять параметр method к значению
spearman:
> x cor(x, log(x), method="spearman")
[1] 1
Если корреляционная матрица большая, то читать ее довольно трудно. Поэтому существует несколько способов визуального представления
таких матриц. Можно, например, использовать функцию symnum(), которая выведет матрицу в текстовом виде, но с заменой чисел на буквы
в зависимости от того, какому диапазону принадлежало значение:
> symnum(cor(longley))
GNP. GNP U A P Y E
GNP.deflator 1

Есть ли взаимосвязь, или Анализ корреляций

GNP
B
Unemployed
,
Armed.Forces .
Population
B
Year
B
Employed
B
attr(,"legend")
[1] 0 ‘ ’ 0.3 ‘.’

1
,
.
B
B
B

113

1
1
, . 1
, . B 1
. . B B 1

0.6 ‘,’ 0.8 ‘+’ 0.9 ‘*’ 0.95 ‘B’ 1

Эта функция имеет большое количество разнообразных настроек,
но по умолчанию они все выставлены в значения, оптимальные для
отображения корреляционных матриц.
Второй способ — это графическое представление корреляционных
коэффициентов. Идея проста: нужно разбить область от −1 до +1 на
отдельные диапазоны, назначить каждому свой цвет, а затем все это
отобразить. Для этого можно воспользоваться функциями image() и
axis() (рис. 20):
>
>
+
#
>
>
+

cor.l
>
>
>
>

library(ellipse)
cor.l b0 b1 x1
+
>
>

1.50

1.55

1.60

1.65

1.70

1.75

1.80

Рост (м)

Рис. 22. Зависимость между ростом и весом
Для просмотра результатов линейной аппроксимации можно использовать функцию
> summary(lm.women)
Call:
lm(formula = weight ~ height, data = women.metr)

118

Анализ связей: двумерные данные

Residuals:
Min
1Q Median
-0.7862 -0.5141 -0.1739

3Q
0.3364

Max
1.4137

Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) -39.697
2.693 -14.74 1.71e-09 ***
height
61.610
1.628
37.85 1.09e-14 ***
--Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
Residual standard error: 0.6917 on 13 degrees of freedom
Multiple R-squared: 0.991,Adjusted R-squared: 0.9903
F-statistic: 1433 on 1 and 13 DF, p-value: 1.091e-14
on 1 and 13 DF, p-value: 1.091e-14
Отсюда следует, что:
1. Получена линейная регрессионная модель вида
Веc(мод) = -39.697 + 61.61 * Рост,
то есть увеличение роста на 10 см соответствует увеличению веса
примерно на 6 кг.
2. Наибольшее положительное отклонение истинного значения отклика от модельного составляет 1.4137 кг, наибольшее отрицательное −0.7862 кг.
3. Почти половина остатков находится в пределах от первой квартили (1Q = −0.5141 кг) до третьей (3Q = 0.3364 кг).
4. Все коэффициенты значимы на уровне p-value < 0.001; это показывают *** в строке Coefficients) и сами значения p-value
Pr(>|t|): 1.71e-09 для b0 («(Intercept)») и 1.09e-14 («height»)
для b1 .
5. Среднеквадратичное отклонение (Adjusted R-squared) для данной модели составляет R2 = 0.9903. Его значение близко к 1, что
говорит о высокой значимости.
6. Значимость среднеквадратичного отклонения подтверждает и высокое значение F-статистики, равное 1433, и общий уровень значимости (определяемый по этой статистике): p-value: 1.091e-14,
что много меньше 0.001.

Какая связь, или Регрессионный анализ

119

7. Если на основе этого анализа будет составлен отчет, то надо будет
указать еще и значения степеней свободы df: 1 и 13 (по колонкам
и по строкам данных соответственно).
На самом деле это еще далеко не конец, а только начало долгого пути анализа и подгонки модели. В этом случае, например, параболическая модель (где используется не рост, а квадрат роста), будет
еще лучше вписываться в данные (как выражаются, будет «лучше объяснять» вес). Можно попробовать и другие модели, ведь о линейных,
обобщенных линейных и нелинейных моделях написаны горы книг! Но
нам сейчас было важно сделать первый шаг.
Время от времени требуется не просто получить регрессию, а сравнить несколько регрессий, основанных на одних и тех же данных. Вот
пример. Известно, что вметодике анализа выборов большую роль играет соответствие между процентом избирателей, проголосовавших за
данного кандидата, и процентом явки на данном избирательном участке. Эти соответствия для разных кандидатов могут выглядеть по-разному, что говорит о различии их электоратов. Данные из файла vybory.txt
содержат результаты голосования за трех кандидатов по более чем
сотне избирательных участков. Посмотрим, различается ли для разных
кандидатов зависимость их результатов от явки избирателей. Сначала
загрузим и проверим данные:
> vybory str(vybory)
’data.frame’: 153 obs. of 7 variables:
$ UCHASTOK: int 1 2 3 4 5 6 7 8 9 10 ...
$ IZBIR
: int 329786 144483 709903 696114 ...
$ NEDEJSTV : int 2623 859 5656 4392 3837 4715 ...
$ DEJSTV: int 198354 97863 664195 619629 ...
$ KAND.1 : int 24565 7884 30491 54999 36880 ...
$ KAND.2 : int 11786 6364 11152 11519 10002 ...
$ KAND.3 : int 142627 68573 599105 525814 ...
> head(vybory)
UCHASTOK IZBIR NEDEJSTV DEJSTV KAND.1 KAND.2 KAND.3
1
1 329786
2623 198354 24565 11786 142627
2
2 144483
859 97863
7884
6364 68573
3
3 709903
5656 664195 30491 11152 599105
4
4 696114
4392 619629 54999 11519 525814
5
5 717095
3837 653133 36880 10002 559653
6
6 787593
4715 655486 72166 25204 485669
Теперь присоединим таблицу данных, для того чтобы с ее колонками
можно было работать как с независимыми переменными:

120

Анализ связей: двумерные данные

> attach(vybory)
Вычислим доли проголосовавших за каждого кандидата и явку:
> DOLJA JAVKA cor(JAVKA, DOLJA)
KAND.1
KAND.2
KAND.3
[1,] -0.3898262 -0.41416 0.9721124
Корреляция есть, хотя и невысокая для первых двух кандидатов.
Похоже, что голосование по третьему кандидату серьезно отличалось.
Проверим это:
>
>
>
>

lm.1 |t|)
(Intercept) -0.43006
0.01702 -25.27

>
>
>
>

plot(KAND.3/IZBIR ~ JAVKA, xlim=c(0,1), ylim=c(0,1),
xlab="Явка", ylab="Доля проголосовавших за кандидата")
points(KAND.1/IZBIR ~ JAVKA, pch=2)
points(KAND.2/IZBIR ~ JAVKA, pch=3)
abline(lm.3)
abline(lm.2, lty=2)
abline(lm.1, lty=3)

122

Анализ связей: двумерные данные

0.2

0.4

0.6

0.8

Кандидат 1
Кандидат 2
Кандидат 3

0.0

Доля проголосовавших за кандидата

1.0

> legend("topleft", lty=c(3,2,1),
+ legend=c("Кандидат 1","Кандидат 2","Кандидат 3"))
> detach(vybory)

0.0

0.2

0.4

0.6

0.8

1.0

Явка

Рис. 23. Зависимость результатов трех кандидатов от явки на выборы
Итак, третий кандидат имеет электорат, который значительно отличается по своему поведению от электоратов двух первых кандидатов.
Разумеется, хочется проверить это статистически, а не только визуально. Для того чтобы сравнивать регрессии, существует специальный
метод, анализ ковариаций (ANCOVA). Подробное рассмотрение этого
метода выходит за рамки настоящей книги, но мы сейчас покажем, как
можно применить анализ ковариаций для данных о выборах:
> vybory2 names(vybory2) str(vybory2)
’data.frame’: 459 obs. of 3 variables:
$ javka: num 0.609 0.683 0.944 0.896 0.916 ...

Какая связь, или Регрессионный анализ

123

$ dolja: num 0.0745 0.0546 0.043 0.079 0.0514 ...
$ kand : Factor w/ 3 levels "KAND.1","KAND.2",..: 1 1 1 ...
> head(vybory2, 3)
javka
dolja
kand
1 0.6094164 0.07448770 KAND.1
2 0.6832776 0.05456697 KAND.1
3 0.9435810 0.04295094 KAND.1
Мы создали и проверили новую таблицу данных. В ней две интересующие нас переменные (доля проголосовавших за каждого кандидата
и явка) расположены теперь в один столбец, а третий столбец — это
фактор с именами кандидатов. Чтобы получить такую таблицу, мы использовали функцию stack().
> ancova.v summary(ancova.v)
Call:
lm(formula = dolja ~ javka * kand, data = vybory2)
Residuals:
Min
1Q
Median
-0.116483 -0.015470 -0.000699

3Q
0.014825

Max
0.102810

Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept)
0.117115
0.011973
9.781 < 2e-16 ***
javka
-0.070726
0.018266 -3.872 0.000124 ***
kandKAND.2
-0.023627
0.016933 -1.395 0.163591
kandKAND.3
-0.547179
0.016933 -32.315 < 2e-16 ***
javka:kandKAND.2 0.004129
0.025832
0.160 0.873074
javka:kandKAND.3 1.393336
0.025832 53.938 < 2e-16 ***
--Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
Residual standard error: 0.02591 on 453 degrees of freedom
Multiple R-squared: 0.9824,Adjusted R-squared: 0.9822
F-statistic: 5057 on 5 and 453 DF, p-value: < 2.2e-16
Анализ ковариаций использует формулу модели «отклик ~ воздействие * фактор», где звездочка (*) обозначает, что надо проверить одновременно отклик на воздействие, отклик на фактор и взаимодействие
между откликом и фактором. (Напомним, что линейная регрессия ис-

124

Анализ связей: двумерные данные

пользует более простую формулу, «отклик ~ воздействие»). В нашем
случае откликом была доля проголосовавших, воздействием — явка, а
фактором — имя кандидата. Больше всего нас интересовало, правда ли,
что имеется сильное взаимодействие между явкой и третьим кандидатом. Полученные результаты (см. строку javka:kandKAND.3) не оставляют в этом сомнений, взаимодействие значимо.
***
Довольно сложно анализировать данные, если зависимость нелинейная. Здесь на помощь могут прийти методы визуализации. Более
того, часто их бывает достаточно, чтобы сделать выводы, пусть и предварительные. Рассмотрим такой пример. По берегу Черного моря, от
Новороссийска до Сочи, растут первоцветы, или примулы. Самая замечательная их черта — это то, что окраска цветков постепенно меняется
при продвижении от Новороссийска на юго-запад, вдоль берега: сначала
большинство цветков белые или желтые, а ближе к Дагомысу появляется все больше и больше красных, малиновых, фиолетовых и розовых
тонов. В файле данных primula.txt записаны результаты десятилетнего исследования береговых популяций первоцветов. Попробуем выяснить, как именно меняется окраска цветков по мере продвижения на
юго-запад (рис. 24):
>
+
>
+
+
>
>
>
>
>
>

prp.coast l head(l)

126

1
2
3
4
5
6

Анализ связей: двумерные данные

V1 V2
14 F
29 F
6 F
25 S
18 S
4 F

Как узнать, влияет ли стаж на успех? Таблицы сопряженности тут
подходят мало, ведь стаж (V1) — интервальная переменная. Линейная
регрессия не подходит, потому что для нее требуется не только интервальное воздействие (в нашем случае стаж), но еще и интервальный
отклик. У нас же отклик — бинарный. Оказывается, существует выход. Для этого надо в качестве отклика исследовать не успех/неуспех,
а вероятность успеха, которая, как всякая вероятность, непрерывно
изменяется в интервале от 0 до 1. Мы не будем здесь вдаваться в математические подробности, а просто покажем, как это работает для данных о программистах:
> l.logit summary(l.logit)
Call:
glm(formula = V2 ~ V1, family = binomial, data = l)
Deviance Residuals:
Min
1Q
Median
-1.9987 -0.4584 -0.2245

3Q
0.4837

Max
1.5005

Coefficients:
Estimate Std. Error z value Pr(>|z|)
(Intercept) -4.9638
2.4597 -2.018
0.0436 *
V1
0.2350
0.1163
2.021
0.0432 *
--Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
(Dispersion parameter for binomial family taken to be 1)
Null deviance: 18.249
Residual deviance: 10.301
AIC: 14.301

on 13
on 12

degrees of freedom
degrees of freedom

Number of Fisher Scoring iterations: 5

Вероятность успеха, или Логистическая регрессия

127

Итак, оба параметра логистической регрессии значимы, и p-значения
меньше пороговых. Этого достаточно, чтобы сказать: опыт программиста значимо влияет на успешное решение задачи.
Попробуйте теперь решить такую задачу. В файле pokaz.txt находятся результаты эксперимента с показом предметов на доли секунды,
описанного в первой главе. Первая колонка содержит номер испытуемого (их было десять), вторая — номер испытания (пять на человека,
испытания проводились последовательно), а третья — результат в бинарной форме: 0 — не угадал(а), 1 — угадал(а). Выясните, есть ли связь
между номером испытания и результатом.
В выдаче функции summary.glm() стоит обратить внимание на предпоследнюю строчку, AIC (Akaike’s Information Criterion) — информационный критерий Акаике. Это критерий, который позволяет сравнивать
модели между собой (несколько подробнее про AIC рассказано в главе
про временные ряды). Чем меньше AIC, тем лучше модель. Покажем
это на примере. Вернемся к нашим данным про «виновника» отравления. Помидоры или салат? Вот так можно попробовать решить проблему:
>
+
>
>

tox.logit tox.logit3$aic
[1] 53.11957
Что же, похоже, что настоящим виновником отравления является
салат, ведь модель с салатом обладает наименьшим AIC. Почему же

128

Анализ связей: двумерные данные

тест хи-квадрат указал нам еще и на помидоры? Дело, скорее всего, в
том, что между салатом и помидорами существовало взаимодействие
(interaction). Говоря человеческим языком, те, кто брали салат, обычно
брали к нему и помидоры.

5.7. Если выборок больше двух
А что если теперь мы захотим узнать, есть ли различия между
тремя выборками? Первое, что приходит в голову (предположим, что
это параметрические данные),— это провести серию тестов Стьюдента:
между первой и второй выборками, между первой и третьей и, наконец, между второй и третьей — всего три теста. К сожалению, число необходимых тестов Стьюдента будет расти чрезвычайно быстро с
увеличением числа интересующих нас выборок. Например, для сравнения попарно шести выборок нам понадобится провести уже 15 тестов!
А представляете, как обидно будет провести все эти 15 тестов только для того, чтобы узнать, что все выборки не различаются между
собой! Но главная проблема заключена не в сбережении труда исследователя (все-таки обычно нам нужно сравнить не больше 3–4 выборок).
Дело в том, что при повторном проведении статистических тестов, основанных на вероятностных понятиях, на одной и той же выборке вероятность обнаружить достоверную закономерность по ошибке возрастает. Допустим, мы считаем различия достоверными при p-value
< 0.05, при этом мы будем ошибаться (находить различия там, где их
нет) в 4 случаях из 100 (в 1 случае из 25). Понятно, что если мы проведем 25 статистических тестов на одной и той же выборке, то, скорее
всего, однажды мы найдем различия просто по ошибке. Аналогичные
рассуждения могут быть применены и к экстремальным видам спорта.
Например, вероятность того, что парашют не раскроется при прыжке,
довольно мала (допустим, 1/100), и странно бы было ожидать, что парашют не раскроется как раз тогда, когда человек прыгает впервые.
При этом любой десантник, имеющий опыт нескольких сотен прыжков, может рассказать несколько захватывающих историй о том, как
ему пришлось использовать запасной парашют.
Поэтому для сравнения трех и более выборок используется специальный метод — однофакторный дисперсионный анализ (ANOVA, от
английского «ANalysis Of VAriance»). Нулевая гипотеза здесь будет
утверждать, что выборки не различаются между собой, а альтернативная гипотеза — что хотя бы одна пара выборок различается между
собой. Обратите внимание на формулировку альтернативной гипотезы!
Результаты этого теста будут одинаковыми и в случае, если различается
только одна пара выборок, и в случае, если различаются все выборки.

Если выборок больше двух

129

Чтобы узнать, какие именно выборки отличаются, надо будет проводить специальные тесты (о них рассказано ниже).
Данные для ANOVA лучше организовать следующим образом: задать две переменные, и в одной из них указать все значения всех сравниваемых выборок (например, рост брюнетов, блондинов и шатенов), а
во второй — коды выборок, к которым принадлежат значения первой
переменной (например, будем ставить напротив значения роста брюнета «br», напротив роста блондина — «bl» и напротив роста шатена —
«sh»). Тогда мы сможем задействовать уже знакомую нам логическую
регрессию (при этом отклик будет количественный, а вот воздействие —
качественное). Сам анализ проводится при помощи двойной функции
anova(lm()) (рис. 25):
> set.seed(1683)
> VES.BR VES.BL VES.SH ROST.BR ROST.BL ROST.SH data boxplot(data$ROST ~ data$CVET)
> anova(lm(data$ROST ~ data$CVET))
Analysis of Variance Table
Response: data$ROST
Df Sum Sq Mean Sq F value
Pr(>F)
data$CVET 2 2185.2 1092.58 81.487 < 2.2e-16 ***
Residuals 87 1166.5
13.41
--Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
Надо теперь объяснить, что именно мы сделали. В R дисперсионный анализ проводится при помощи той же самой функции линейной
модели lm(), что и регрессия, но формула модели здесь другая: отклик
~ фактор, где фактором является индикатор группы (в нашем случае
c("br", "bl", "sh")). Такой анализ сначала строит три модели. Каждую модель можно условно представить в виде вертикальной линии, по
которой распределены точки; вертикальной потому, что все точки соответствуют одному значению индикатора группы (скажем, bl). Потом
функция anova() сравнивает эти модели и определяет, есть ли между

130

155

160

165

170

175

180

Анализ связей: двумерные данные

bl

br

sh

Рис. 25. Различается ли вес между тремя выборками людей с разным
цветом волос? (Данные получены искусственно)
ними какие-нибудь значимые отличия. Мы создали наши данные искусственно, как и данные о зарплате в главе про одномерные данные.
Ну и, естественно, получили ровно то, что заложили,— надо принять
альтернативную гипотезу о том, что хотя бы одна выборка отличается
от прочих. Глядя на боксплот, можно предположить, что это — блондины. Проверим предположение при помощи множественного t-теста
(pairwise t-test):
> pairwise.t.test(data$ROST, data$CVET)
Pairwise comparisons using t tests with pooled SD
data: data$ROST and data$CVET
bl
br
br < 2e-16 sh 1.1e-11 1.2e-05
P value adjustment method: holm

Если выборок больше двух

131

И вправду, из полученной таблички видно, что блондины значимо
(оба p-значения много меньше 0.05) отличаются от остальных групп.
Есть и еще один похожий тест. Это «Tukey Honest Significant Differences
test» (у него есть и специальный график, см. рис. 26):
> rost.cvet (rost.cvet.hsd plot(rost.cvet.hsd)
Результаты обоих тестов, разумеется, похожи.
Важно помнить, что ANOVA — параметрический метод, то есть наши данные должны иметь нормальное (или близкое к нормальному)
распределение. Кроме того, стандартные отклонения выборок должны
быть, по крайней мере, похожи. Последнее условие можно обойти при
помощи функции onеway.test(), которая не предполагает по умолчанию равенства разбросов. Но если есть сомнения в параметричности исходных данных, то лучше всего использовать непараметрический аналог ANOVA, тест Краскала-Уоллиса (Kruskal-Wallis test). Можно попробовать его на наших искусственных данных:
> kruskal.test(data$ROST ~ data$CVET)
Kruskal-Wallis rank sum test
data: data$ROST by data$CVET
Kruskal-Wallis chi-squared = 63.9153, df = 2,
p-value = 1.321e-14
Результат аналогичен «классическому» ANOVA, а p-значение чуть
больше, как и положено для непараметрического теста.
Задача про вес. Попробуйте самостоятельно выяснить, отличаются ли наши группы еще и по весу? Если да, то отличаются ли от
остальных по-прежнему блондины?

132

sh−br

sh−bl

br−bl

Анализ связей: двумерные данные

−5

0

5

10

Рис. 26. Результаты post hoc теста Тьюки. Обе разницы между средними
для роста блондинов и двух остальных групп лежат внутри доверительного интервала (справа от пунктирной линии)
Задача про зависимость роста и веса. А зависит ли вес от роста
в наших искусственных данных? Если да, то какая эта зависимость?
***
Ответ к задаче про тест знаков. Достаточно написать:
>
>
>
>
>

a normality3(ozone.month[ozone.month$Month==6,][1])

134

Анализ связей: двумерные данные

$Ozone
[1] "NORMAL"
и т. д.
Мы использовали здесь две пары квадратных скобок, для того чтобы данные не превратились в вектор, а остались таблицей данных, ведь
наша функция normality3() «заточена» именно под таблицы данных.
Если же эти две пары квадратных скобок вам не нравятся, можно поступить еще проще (хотя и муторнее) и вернуться к оригинальному
shapiro.test():
> shapiro.test(ozone.month[ozone.month$Month==5,"Ozone"])
Shapiro-Wilk normality test
data: ozone.month[ozone.month$Month == 5, "Ozone"]
W = 0.714, p-value = 8.294e-06
(Загляните в предыдущую главу, чтобы посмотреть, какая в этом
тесте альтернативная гипотеза.)
Как видите, в R одну и ту же задачу всегда можно решить множеством способов. И это прекрасно!
Ответ к задаче про оценки двух классов. Попробуем проанализировать данные при помощи статистических тестов:
> otsenki klassy normality3(klassy)
$A1
[1] "NOT NORMAL"
$A2
[1] "NOT NORMAL"
$B1
[1] "NOT NORMAL"
Увы, параметрические методы неприменимы. Надо работать непараметрическими методами:

Если выборок больше двух

135

> lapply(klassy, function(.x) median(.x, na.rm=TRUE))
$A1
[1] 4
$A2
[1] 4
$B1
[1] 5
Мы применили анонимную функцию, для того чтобы избавиться от
пропущенных значений (болевшие ученики?). Похоже, что в классе A
результаты первой и второй четвертей похожи, а вот класс B учился в
первой четверти лучше A. Проверим это:
> wilcox.test(klassy$A1, klassy$A2, paired=TRUE)
Wilcoxon signed rank test with continuity correction
data: klassy$A1 and klassy$A2
V = 15.5, p-value = 0.8605
alternative hypothesis: true location shift is not equal to 0
Warning messages:
...
> wilcox.test(klassy$B1, klassy$A1, alt="greater")
Wilcoxon rank sum test with continuity correction
data: klassy$B1 and klassy$A1
W = 306, p-value = 0.02382
alternative hypothesis: true location shift is greater than 0
Warning message:
In wilcox.test.default(klassy$B1, klassy$A1, alt = "greater") :
cannot compute exact p-value with ties
Для четвертей мы применили парный тест: ведь оценки получали
одни и те же ученики. А для сравнения разных классов использован односторонний тест, поскольку такие тесты обычно чувствительнее двусторонних и поскольку здесь как раз и можем проверить, учится ли
класс B лучше A, а не просто отличаются ли их результаты.
Итак, результаты класса A за первую и вторую четверти достоверно
не отличаются, при этом класс B учился в первую четверть статистически значимо лучше класса A.

136

Анализ связей: двумерные данные

Ответ к задаче про кассиров. Попробуем сначала выяснить,
можно ли использовать параметрические методы:
> kass head(kass)
KASS.1 KASS.2
1
3
12
2
12
12
3
13
9
4
5
6
5
4
2
6
11
9
> normality3(kass)
$KASS.1
[1] "NORMAL"
$KASS.2
[1] "NORMAL"
Отлично! Как насчет средних?
> (kass.m with(kass, t.test(KASS.1, KASS.2))
Welch Two Sample t-test
data: KASS.1 and KASS.2
t = 0.4358, df = 39.923, p-value = 0.6653
alternative hypothesis:
true difference in means is not equal to 0
95 percent confidence interval:
-2.078962 3.221819
sample estimates:
mean of x mean of y
8.380952 7.809524
Увы, достоверной разницы нет. Попробуем отобразить это графически (рис. 27) и проверить, выполняется ли «правило трех сигм»:

Если выборок больше двух

137

10
0

5

Очередь

15

20

> (kass.sd library(gplots)
> barplot2(kass.m, plot.ci=TRUE,
+ ci.l=kass.m, ci.u=(kass.m + (3 * kass.sd)),
+ names.arg=c("Первый кассир","Второй кассир"),
+ ylab="Очередь")

Первый кассир

Второй кассир

Рис. 27. Столбчатые диаграммы с разбросами в три стандартных отклонения отображают изменчивость очередей к двум кассирам в супермаркете
Столбчатые диаграммы с разбросами — это распространенный, хотя
и не самый эффективный, способ изображения данных, ящики-с-усами
гораздо лучше. Для того чтобы нарисовать разброс на столбике, мы
загрузили пакет gplots и вызвали оттуда функцию barplot2(). Кроме
того, мы использовали прием с внешними скобками, чтобы результат
функции sapply() шел и на экран, и в объект kass.sd.

138

Анализ связей: двумерные данные

«Правило трех сигм» нас не подвело — разбросы шириной в три
стандартных отклонения сильно перекрываются. Значит, выборки достоверно не отличаются.
Ответ к задаче про проращивание зараженных грибом семян. Загрузим данные, посмотрим на их структуру и применим тест
хи-квадрат:
> pr head(pr)
CID GERM.14
1 63
1
2 63
1
3 63
1
4 63
1
5 63
1
6 63
1
> chisq.test(table(pr[pr$CID %in% c(0,105),]))
Pearson’s Chi-squared test with Yates’ continuity correction
data: table(pr[pr$CID %in% c(0, 105), ])
X-squared = 8.0251, df = 1, p-value = 0.004613
> chisq.test(table(pr[pr$CID %in% c(0,80),]))
Pearson’s Chi-squared test with Yates’ continuity correction
data: table(pr[pr$CID %in% c(0, 80), ])
X-squared = 22.7273, df = 1, p-value = 1.867e-06
> chisq.test(table(pr[pr$CID %in% c(0,63),]))
Pearson’s Chi-squared test with Yates’ continuity correction
data: table(pr[pr$CID %in% c(0, 63), ])
X-squared = 0.2778, df = 1, p-value = 0.5982
Warning message:
In chisq.test(table(pr[pr$CID %in% c(0, 63), ])) :
Chi-squared approximation may be incorrect
Как видим, эффекты двух грибов (CID105 и CID80) отличаются от
контроля, а третьего (CID63) — нет.

Если выборок больше двух

139

В ответе использован оператор %in%, который позволяет выбрать из
того, что слева, то, что есть справа. Это сэкономило нам немного времени. Можно было написать и table(pr[pr$CID==0 & pr$CID==63, ]),
где & означает логическое «и». Есть, разумеется, и другие способы сделать то же самое.
Нужно заметить, что поскольку мы три раза повторили сравнение
с одной и той же группой (контролем), нужно пороговое p-значение
уменьшить в три раза (это называется «поправка Бонферрони для множественных сравнений»). Кстати, для подобных поправок в R есть специальная функция, p.adjust(). В нашем случае первые два значения
все равно останутся значимыми.
Ответ к задаче про вес. Да и нет:
> anova(lm(data$VES ~ data$CVET))
Analysis of Variance Table
Response: data$VES
Df Sum Sq Mean Sq F value
Pr(>F)
data$CVET 2 1043.5 521.73 31.598 4.837e-11 ***
Residuals 87 1436.5
16.51
--Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
> pairwise.t.test(data$VES, data$CVET)
Pairwise comparisons using t tests with pooled SD
data:

data$VES and data$CVET

bl
br
br 1.5e-10 sh 0.15
7.5e-08
P value adjustment method: holm
Отличия между группами есть, но отличаются по весу как раз брюнеты, а не блондины.
Ответ к задаче о показе предметов. Поступим так же, как и в
примере про программистов. Сначала загрузим данные и присоединим
их, чтобы было легче работать с переменными:
> pokaz attach(pokaz)
Затем проверим модель:

140

Анализ связей: двумерные данные

> pokaz.logit summary(pokaz.logit)
Call:
glm(formula = V3 ~ V2, family = binomial)
Deviance Residuals:
Min
1Q
Median
-2.4029 -0.8701
0.4299

3Q
0.7825

Max
1.5197

Coefficients:
Estimate Std. Error z value Pr(>|z|)
(Intercept) -1.6776
0.7923 -2.117 0.03423 *
V2
0.9015
0.2922
3.085 0.00203 **
--Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
(Dispersion parameter for binomial family taken to be 1)
Null deviance: 62.687
Residual deviance: 49.738
AIC: 53.738

on 49
on 48

degrees of freedom
degrees of freedom

Number of Fisher Scoring iterations: 4
(Вызывая переменные, мы учли тот факт, что R присваивает колонкам данных без заголовков имена V1, V2, V3 и т. д.)
Модель значима! Так что наша идея, высказанная в первой главе (о
том, что человек обучается в ходе попыток), имеет под собой определенные основания. А вот так можно построить график для этого эксперимента (рис. 28):
>
>
+
>
>
>

popytki ves.rost summary(ves.rost)
Call:
lm(formula = data$VES ~ data$ROST)
Residuals:
Min
1Q
-10.2435 -3.4745

Median
-0.5642

3Q
2.9358

Max
12.3203

Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 10.69580
13.37252
0.800
0.426
data$ROST
0.39742
0.08132
4.887 4.57e-06 ***
--Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
Residual standard error: 4.708 on 88 degrees of freedom
Multiple R-squared: 0.2135,Adjusted R-squared: 0.2045
F-statistic: 23.88 on 1 and 88 DF, p-value: 4.565e-06
> plot(data$VES ~ data$ROST)
> abline(ves.rost)
Вывод о том, что зависимость слабая, сделан на основании низкого R2 и незначимого первого коэффициента. Кроме того, очень много
остатков (больше 70% длины интервала между минимумом и максимумом) находится снаружи от первого и третьего квартилей. Да и график
показывает серьезный разброс.

143

70

75

80

85

90

Если выборок больше двух

155

160

165

170

175

Рис. 29. Слабая зависимость

180

Глава 6
Анализ структуры: data mining
Фразу «data mining» можно все чаще увидеть в Интернете и на
обложках книг по анализу данных. Говорят даже, что эпоха статистики
одной-двух переменных закончилась, и наступило новое время — время
анализа больших и сверхбольших массивов данных.
На самом деле под методами «data mining» подразумеваются любые методы, как визуальные, так и аналитические, позволяющие «нащупать» структуру в данных, особенно в данных большого размера.
Данные для такого анализа используются, как правило, многомерные,
то есть такие, которые можно представить в виде таблицы из нескольких колонок-переменных. Поэтому более традиционное название для
этих методов — «многомерный анализ», или «многомерная статистика».
Но «data mining» звучит, конечно, серьезнее. Кроме многомерности и
большого размера (сотни, а то и тысячи строк и столбцов), используемые данные отличаются еще и тем, что переменные в них могут быть
совершенно разных типов (интервальные, шкальные, номинальные).
Грубо говоря, многомерные методы делятся на методы визуализации и методы классификации с обучением. В первом случае результат
можно анализировать в основном зрительно, а во втором — возможна
статистическая проверка результатов. Разумеется, граница между этими группами нерезкая, но для удобства мы станем рассматривать их
именно в этом порядке.

6.1. Рисуем многомерные данные
Самое простое, что можно сделать с многомерными данными,— это
построить график. Разумеется, для того чтобы построить график нескольких переменных, надо свести все разнообразие к двум или, в крайнем
случае, к трем измерениям. Это называют «сокращением размерности».
Но иногда можно обойтись и без сокращения размерности. Например,
если переменных три.

Рисуем многомерные данные

145

6.1.1. Диаграммы рассеяния
Если все три переменные — непрерывные, то поможет пакет RGL,
который позволяет создавать настоящие трехмерные графики.
Для примера всюду, где возможно, мы будем использовать встроенные в R данные iris. Эти данные, заимствованные из работы знаменитого математика (и биолога) Р. Фишера, описывают разнообразие
нескольких признаков трех видов ирисов. Соответственно, в них 5 переменных (колонок), причем последняя — это название вида.
Вот как можно изобразить 4 из 5 колонок при помощи RGL (рис. 30):
> library(rgl)
> plot3d(iris$Sepal.Length, iris$Sepal.Width, iris$Petal.Length,
+ col=as.numeric(iris$Species), size=3)
2.0

2.5

3.0

3.5

iris$Petal.Length

4.0

6

5
iris$Sepal.Width
4

3

2
iris$Sepal.Length
1

5

6

7

Рис. 30. Пример трехмерного графика RGL
Размер появившегося окна и проекцию можно (и нужно) менять при
помощи мышки.
Сразу видно, что один из видов (Iris setosa) хорошо отличается
от двух других по признаку длины лепестков (Petal.Length). Кста-

146

Анализ структуры: data mining

ти, в том, что мы изобразили именно 4 признака, не было оговорки,
ведь вид ириса — тоже признак, закодированный в данном случае цветом. Можно обойтись и без RGL, тогда вам может потребоваться пакет
scatterplot3d, содержащий одноименную функцию. Но, вообще говоря, трехмерными графиками лучше не злоупотреблять — очень они не
раскрывают, а затемняют суть явления. Правда, в случае RGL это компенсируется возможностью свободно менять «точку обзора».
Другой способ визуализации многомерных данных — это построение
составных графиков. Здесь у R колоссальные возможности, предоставляемые пакетом lattice, который предназначен для так называемой
панельной (Trellis) графики. Вот как можно изобразить четыре признака ирисов (рис. 31):
> library(lattice)
> xyplot(Sepal.Length ~ Petal.Length + Petal.Width | Species,
+ data=iris, auto.key=TRUE)

Petal.Length
Petal.Width
virginica

8

7

6

Sepal.Length

5

setosa

8

versicolor

7

6

5

0

2

4

6

Petal.Length + Petal.Width

Рис. 31. Пример Trellis-графика

147

Рисуем многомерные данные

Получилась картинка зависимости длины чашелистиков как от длины, так и от ширины лепестков для каждого из трех видов.
Можно обойтись и без lattice. Несколько подобных графиков доступны и в базовой поставке R. Например, coplot() очень похож на
xyplot() (рис. 32):
> coplot(dolja ~ javka | kand, data=vybory2)

Given : kand
KAND.3
KAND.2
KAND.1

0.7

0.8

0.9

1.0
0.0 0.2 0.4 0.6 0.8 1.0

0.6

0.0 0.2 0.4 0.6 0.8 1.0

dolja

0.5

0.5

0.6

0.7

0.8

0.9

1.0

javka

Рис. 32. Данные о выборах из раздела о регрессионном анализе, представленные с помощью функции coplot()
Мы визуализировали данные о выборах из раздела про регрессионный анализ. Эти данные тоже можно назвать многомерными, ведь
каждый кандидат, по сути,— это отдельная переменная.
Матричный график рисуется при помощи функции pairs() (рис. 33):
> pairs(iris[1:4], pch=21, bg=c("red", "green3", "blue")
+ [unclass(iris$Species)])

148

Анализ структуры: data mining

3.0

4.0

0.5

1.5

2.5

6.5

7.5

2.0

4.0

4.5

5.5

Sepal.Length

1 2 3 4 5 6 7

2.0

3.0

Sepal.Width

1.5

2.5

Petal.Length

0.5

Petal.Width

4.5

5.5

6.5

7.5

1 2 3 4 5 6 7

Рис. 33. Матричный график
Мы получили зависимость значения каждого признака от каждого
признака, причем заодно и «покрасили» точки в цвета видов (для этого пришлось «деклассировать» фактор iris$Species, превратив его в
текстовый вектор). Таким образом, нам удалось отобразить сразу пять
переменных.

6.1.2. Пиктограммы
Для визуализации многомерных данных можно использовать разнообразные графики-пиктограммы, где каждый элемент представляет один объект наблюдений, а его параметры характеризуют значения
признаков объекта.
Один из классических графиков-пиктограмм — звезды (рис. 34):
> stars(mtcars[1:9,1:7], cex=1.2)
Здесь каждая пиктограмма — это один тип автомобиля, а длины лучей соответствуют значениям разных характеристик этих машин. Легко

149

Рисуем многомерные данные

Mazda RX4

Hornet 4 Drive

Duster 360

Mazda RX4 Wag

Hornet Sportabout

Merc 240D

Datsun 710

Valiant

Merc 230

Рис. 34. Звезды изображают разные марки автомобилей
видеть, например, обе «Мазды» схожи с друг с другом больше, чем с
остальными марками.
А вот более экзотический график-пиктограмма, так называемые «лица Чернова». Идея графика состоит в том, что лица люди различают
очень хорошо (рис. 35):
> library(TeachingDemos)
> faces(mtcars[1:9,1:7])
Здесь каждое лицо соответствует одному исследуемому объекту (типу автомобиля), а черты лица характеризуют значения признаков объекта. Сходство «Мазд» хорошо заметно и на этом графике.
«Идеологически» к пиктограммам близок график особого типа —
график параллельных координат. Его можно построить при помощи
функции parcoord() из пакета MASS. Чтобы понять, что он делает, лучше всего сразу посмотреть на результат на рис. 36 (для графика использованы данные об измерениях растений):

150

Анализ структуры: data mining

Mazda RX4

Mazda RX4 Wag

Datsun 710

Hornet 4 Drive

Hornet Sportabout

Valiant

Duster 360

Merc 240D

Merc 230

Рис. 35. Лица Чернова изображают разные марки автомобилей
>
>
>
+

measurements iris.pca plot(iris.pca, main="")
Это служебный график, так называемый «screeplot» (в буквальном
переводе «график осыпи»), показывающий относительные вклады каждой компоненты в общий разброс данных. Хорошо видно, что компонент четыре, как и признаков, но, в отличие от первоначальных признаков, наибольший вклад вносят первые две компоненты. Вместо графика
можно получить то же самое в текстовом виде, написав:
> summary(iris.pca)
Importance of components:
Comp.1
Comp.2
Comp.3
Comp.4
Standard deviation
1.7026571 0.9528572 0.38180950 0.143445939
Proportion of Variance 0.7296245 0.2285076 0.03668922 0.005178709
Cumulative Proportion 0.7296245 0.9581321 0.99482129 1.000000000
Отметьте, что первые две компоненты вместе объясняют почти 96%
разброса (как говорят, их вклады, «proportions of variance» = 73% и
23% соответственно).

153

0.0

0.5

1.0

1.5

2.0

2.5

Тени многомерных облаков: анализ главных компонент

Comp.1

Comp.2

Comp.3

Comp.4

Рис. 37. График, отражающий вклады каждой компоненты в разброс
данных
Перейдем к собственно визуализации (рис. 38):
>
>
>
+

iris.p biplot(iris.pca)

154

Анализ структуры: data mining

aa

s

a

2

s
s

0

1

ss s
ss ss
ss
s s ss
s
s sss
ssss
s
s ss s
s
ss
s s sss
s ss
s
s
s
s

−1

PC2

s

v
−2

v
v

a aa aa
a aa
v v v
aa
v v v va a aaaa a
av
a a
a
v
a
aa a
v
aaa v a
v vv v
aa
v vv vvv vaa
v v vv
aa
a
av
vv
a
vvv
a a a
v
v
vv v
a
vv
v
vv a
va
vv

a

s
v
−3

−2

−1

0

1

2

3

PC1

Рис. 38. Разнообразие ирисов на графике первых двух главных компонент
Парный график дает возможность понять, насколько силен вклад
каждого из четырех исходных признаков в первые две компоненты. В
данном случае хорошо видно, что признаки длины и ширины лепестков (в отличие от признаков длины и ширины чашелистиков) вносят
гораздо больший вклад в первую компоненту, которая, собственно, и
«различает» виды. К сожалению, графиком, выдаваемым при помощи
biplot(), довольно трудно «управлять». Поэтому часто предпочтительнее функция loadings():
> loadings(iris.pca)
Loadings:
Comp.1 Comp.2 Comp.3 Comp.4
Sepal.Length 0.521 -0.377 0.720 0.261
Sepal.Width -0.269 -0.923 -0.244 -0.124
Petal.Length 0.580
-0.142 -0.801
Petal.Width
0.565
-0.634 0.524

155

Тени многомерных облаков: анализ главных компонент
−10

−5

0

−0.2

10

132
118
110

0

5

137
145
125
149
126
Sepal.Length
121
144 106
136
865751 101
111
142
116
140
53
141
52
103
87 130
66
113108 123
71 138
146
105
148
131
117
76 78
Petal.Width
128
59
92
119 Petal.Length
139
150
104
62
75
77
133
966798
6455 129
89
85 79134
74 127
112
659772
115
124
56 122
100
84
102
143 109
83
68
95 135
73147
93
6091
80
114
70
90 88
99 82
81
107 69
54
63 120
58
94

−5

−0.1

0.0

0.1

17619
45
20
47
11
49
22
37
38528
18
1 44
23 41
32
21
29
40
27
8
25 24
712
50
36
330
48
35
10
43
431
226
46
13
1439
9

10

−10

0.2

16
Sepal.Width
34
33 15

5

42
61
−0.2

−0.1

0.0

0.1

0.2

Рис. 39. Парный график показывает вклад каждого признака в первые
две компоненты

SS loadings
Proportion Var
Cumulative Var

Comp.1 Comp.2 Comp.3 Comp.4
1.00
1.00
1.00
1.00
0.25
0.25
0.25
0.25
0.25
0.50
0.75
1.00

Собственно, показывает она то же самое, что и biplot(), но подробнее: в первой части вывода каждая ячейка таблицы соответствует
вкладу признака в определенный компонент. Чем ближе это значение
по модулю к единице, тем больше вклад.
Пакеты ade4 и vegan реализуют множество вариаций анализа главных компонент, но самое главное — содержат гораздо больше возможностей для визуализации. Вот как можно проанализировать те же данные
в пакете ade4 (рис. 40):
> library(ade4)
> iris.dudi s.class(iris.dudi$li, iris[,5])
d=1

versicolor

setosa

virginica

Рис. 40. Разнообразие ирисов на графике первых двух главных компонент (пакет ade4)
Не правда ли, на получившемся графике различия видны яснее?
Кроме того, можно проверить качество разрешения между классами (в
данном случае видами ирисов):
> iris.between randtest(iris.between)
Monte-Carlo test
Call: randtest.between(xtest = iris.between)
Observation: 0.7224358
Based on 999 replicates
Simulated p-value: 0.001
Alternative hypothesis: greater
...

Классификация без обучения, или Кластерный анализ

157

Классы (виды ирисов) различаются хорошо. Об этом говорит основанное на 999 повторениях (со слегка различающимися параметрами)
анализа значение Observation. Если бы это значение было меньше 0.5
(то есть 50%), то нам пришлось бы говорить о нечетких различиях.
Как видно, использованный метод уже ближе к «настоящей статистике», нежели к типичной визуализации данных.

6.3. Классификация без обучения, или
Кластерный анализ
Другим способом снижения размерности является классификация
без обучения (упорядочение, или ординация), проводимая на основании
заранее вычисленных значений сходства между всеми парами объектов
(строк). В результате этой процедуры получается квадратная матрица
расстояний, диагональ которой обычно составлена нулями (ведь расстояние между объектом и им же самим равно нулю). За десятилетия
развития этой области статистики придуманы сотни коэффициентов
сходства, из которых наиболее употребительными являются эвклидово
и квартальное (манхеттеновское), применимые в основном к непрерывным переменным (объяснение см. на рис. 41). Коэффициент корреляции
тоже может быть мерой сходства. Балльные и бинарные переменные в
общем случае требуют других коэффициентов, но в пакете cluster реализована функция daisy(), способная распознавать тип переменной и
применять соответствующие коэффициенты, а в пакете vegan реализовано множество дополнительных коэффициентов сходства.

Рис. 41. Эвклидово (1) и манхеттеновское (2) расстояния
Вот как можно построить матрицу сходства (лучше ее все-таки называть матрицей различий, поскольку в ее ячейках стоят именно расстояния) для наших ирисов:
> library(cluster)
> iris.dist
>
>
+

iris.c
>
>
>
+
>
+

library(KernSmooth)
est
>
>
>
+

iriss eq eq.k table(eq.k$cluster, eq$SPECIES)
arvense fluviatile
1
37
5
2
1
41

Классификация без обучения, или Кластерный анализ

163

Получилось! Ошибка классификации довольно низкая. Интересно
будет потом посмотреть, на каких именно растениях метод ошибся и
почему. Отметьте, кстати, что функция kmeans() (как и следующие
две) берет на входе не матрицу расстояний, а оригинальные данные.
Очень интересны так называемые нечеткие методы кластеризации,
основанные на идее того, что каждый объект может принадлежать к
нескольким кластерам сразу — но с разной «силой». Вот как реализуется такой метод в пакете cluster (рис. 46):
> iris.f plot(iris.f, which=1, main="")
> head(data.frame(sp=iris[,5], iris.f$membership))

1
2
3
4
5
6

sp
setosa
setosa
setosa
setosa
setosa
setosa

X1
0.9142273
0.8594576
0.8700857
0.8426296
0.9044503
0.7680227

X2
0.03603116
0.05854637
0.05463714
0.06555926
0.04025288
0.09717445

X3
0.04974153
0.08199602
0.07527719
0.09181118
0.05529687
0.13480286

Подобный график мы уже неоднократно видели, здесь нет ничего
принципиально нового. А вот текстовый вывод интереснее. Для каждой
строчки указан «membership» — показатель «силы» связи, с которой
данный элемент «притягивается» к каждому из трех кластеров. Как
видно, шестая особь, несмотря на то, что почти наверняка принадлежит
к первому кластеру, тяготеет и к третьему. Недостатком этого метода
является необходимость заранее указывать количество получающихся
кластеров. Подобный метод реализован и в пакете e1071 — функция
называется cmeans(), но в этом случае вместо количества кластеров
можно указать предполагаемые центры, вокруг которых будут группироваться элементы.
***
Как вы уже поняли, методы классификации без обучения могут работать не только с интервальными и шкальными, но и с номинальными
данными. Для этого надо лишь закодировать номинальные признаки в
виде нулей и единиц. Дальше умные функции типа daisy() сами выберут коэффициент сходства, а можно и указать его вручную (например,
так: dist(..., method="binary"). А есть ли многомерные методы, которые могут работать с таблицами сопряженности? Оказывается, такие
не просто существуют, но широко используются в разных областях, например в экологии.

164

−3

−2

−1

0

1

2

3

Анализ структуры: data mining

−3

−2

−1

0

1

2

3

Рис. 46. Результат кластеризации функцией fanny()
Анализ связей (correspondence analysis) — один из них. Как и любой
многомерный метод, он позволяет визуализировать структуру данных,
причем работает он через составление таблиц сопряженности. Простой
вариант анализа связей реализован в пакете MASS функцией corresp()
(рис. 47):
>
>
>
+
>
+
>

library(MASS)
caith.ru
>
>
>
>

library(MASS)
iris.train

library(tree)
iris.tree
>
>
+
>
>

library(randomForest)
set.seed(17)
iris.rf iris.urf MDSplot(iris.urf, iris[,5])
Великое множество методов «data mining», разумеется, нельзя охватить в одной главе. Однако нельзя не упомянуть еще об одном современном методе классификации с обучением, основанном на идее вычисления параметров гиперплоскости, разделяющей различные группы в
многомерном пространстве признаков, «Support Vector Machines».

или Дискриминантный анализ

171

−0.4

−0.2

0.0

0.2

0.4

Классификация с обучением,

−0.4

−0.2

0.0

0.2

Рис. 50. Визуализация данных при помощи «Random Forest»
>
>
>
>

library(e1071)
iris.svm library(vegan)
> pivo.d plot(hclust(pivo.d, "ward.D"), main="", xlab="", sub="")

Рис. 52. Иерархическая классификация сортов пива
Получились две большие группы, одна с «Жигулевским» и «Балтикой», другая с «Клинским» и «Старым мельником».

174

Анализ структуры: data mining

Ответ к задаче про два вида растений. Попробуем построить
дерево классификации (рис. 53):
> eq eq.tree plot(eq.tree); text(eq.tree)
N.REB < 15.5
|

DL.R < 631.5
arvense

fluviatile
arvense

Рис. 53. Дерево классификации показывает, что два вида хвощей различаются по признаку N.REB (это количество ребер стебля)

Глава 7
Узнаем будущее: анализ временных
рядов
В этой главе мы рассмотрим только самые основные принципы ра0
боты с временными рядами. За более подробными сведениями рекомендуем обратиться к специальной литературе.

7.1. Что такое временные ряды
Во многих областях деятельности людей замеры показателей проводятся не один раз, а повторяются через некоторые интервалы времени.
Иногда этот интервал равен многим годам, как при переписи населения
страны, иногда — дням, часам, минутам и даже секундам, но интервал
между измерениями во временном ряду есть всегда. Его называют интервалом выборки (sampling interval). А образующийся в результате
0
выборки ряд данных называют временным рядом (time series).
В любом временном ряду можно выделить две компоненты:
1) неслучайную (детерминированную) компоненту;
2) случайную компоненту.
Неслучайная компонента обычно наиболее интересна, так как она
дает возможность проверить гипотезы о производящем временной ряд
явлении. Математическая модель неслучайной компоненты может быть
использована для прогноза поведения временного ряда в будущем.

7.2. Тренд и период колебаний
Если явление, результатом которого является изучаемый ряд, зависит от времени года (или времени суток, или дня недели, или иного фиксированного периода календаря), то из неслучайной компоненты
может быть выделена еще одна компонента — сезонные колебания явления. Ее следует отличать от циклической компоненты, не привязанной
к какому-либо естественному календарному циклу.

176

Узнаем будущее: анализ временных рядов

Под трендом (тенденцией) понимают неслучайную и непериодическую компоненту ряда. Первый вопрос, с которым сталкивается исследователь, анализирующий временной ряд,— существует ли в нем тренд?
0
При наличии во временном ряде тренда и периодической компоненты значения любого последующего значения ряда зависят от предыдущих. Силу и знак этой связи можно измерить уже знакомым вам
инструментом — коэффициентом корреляции. Корреляционная зависимость между последовательными значениями временного ряда называется автокорреляцией.
Коэффициент автокорреляции первого порядка определяет зависи0
мость между соседними значениями ряда tn и tn−1 , больших порядков —
между более отдаленными значениями. Лаг (сдвиг) автокорреляции —
это количество периодов временного ряда, между которыми определяется коэффициент автокорреляции. Последовательность коэффициентов автокорреляции первого, второго и других порядков называется
автокорреляционной функцией временного ряда.
Анализ автокорреляционной функции позволяет найти лаг, при котором автокорреляция наиболее высокая, а следовательно, связь между
текущим и предыдущими уровнями временного ряда наиболее тесная.
Если значимым оказался только первый коэффициент автокорреляции
(коэффициент автокорреляции первого порядка), временной ряд, скорее всего, содержит только тенденцию (тренд). Если значимым оказался коэффициент автокорреляции, соответствующий лагу n, то ряд
содержит циклические колебания с периодичностью в n моментов времени. Если ни один из коэффициентов автокорреляции не является значимым, то можно сказать, что либо ряд не содержит тенденции (тренда)
и циклических колебаний, либо ряд содержит нелинейную тенденцию,
которую линейный коэффициент корреляции выявить не способен.
Взаимная корреляция (кросс-корреляция) отражает, есть ли связь
между рядами. В этом случае расчет происходит так же, как и для автокорреляции, только коэффициент корреляции рассчитывается между
основным рядом и рядом, связь с которым основного ряда нужно определить. Лаг (сдвиг) при этом может быть иотрицательной величиной,
поскольку цель расчета взаимной корреляции — выяснение того, какой
из двух рядов «ведущий».

7.3. Построение временного ряда
0

Анализ временного ряда часто строится вокруг объяснения выявленного тренда и циклических колебаний значений ряда в рамках некоторой статистической модели.

Построение временного ряда

177

Найденная модель позволяет: прогнозировать будущие значения ряда (forecasting), генерировать искусственный временной ряд, все статистические характеристики которого эквивалентны исходному (simulation), и заполнять пробелы в исходном временном ряду наиболее вероятными значениями.
Нужно отличать экстраполяцию временного ряда (прогноз будущих значений ряда) от интерполяции (заполнение пробелов между
имеющимися данными ряда). Не всегда модели ряда, пригодные для
интерполяции, можно использовать для прогноза. Например, полином
0
(степенное уравнение с коэффициентами) очень хорошо сглаживает исходный ряд значений и позволяет получить оценку показателя, который
описывает данный ряд в промежутках между значениями ряда. Но если мы попытаемся продлить полином за стартовое значение ряда, то
получим совершенно случайный результат. Вместе с тем обычный линейный тренд, хотя и не столь изощренно следует изгибам внутри ряда,
дает устойчивый прогноз развития ряда в будущем.
Разные участки ряда могут описывать различные статистические
модели, в этом случае говорят, что ряд нестационарный. Нестационарный временной ряд во многих случаях удается превратить в стационарный путем преобразования данных.
В базовые возможности R входят средства для представления и ана0
0
лиза временных рядов. Основным типом временных данных является
«ts», который представляет собой временной ряд, состоящий из значе0
ний, разделенных одинаковыми интервалами времени. Временные ряды
могут быть образованы и неравномерно отстоящими друг от друга значениями. В этом случае следует воспользоваться специальными типами
данных — zoo и its, которые становятся доступными после загрузки
пакетов с теми же именами.
Часто необходимо обрабатывать календарные даты. По умолчанию
read.table() считывает все нечисловые даты (например, «12/15/04»
или «2004-12-15») как текстовые строки или факторы. Поэтому после
загрузки таких данных при помощи read.table() нужно обязательно применить функцию as.Date(). Она «понимает» описание шаблона
даты и преобразует строки символов в тип данных Date. В последних
версиях R она работает и с факторами:
> dates.df str(dates.df$dates)
chr [1:5] "2011-01-01" "2011-01-02" "2011-01-03" ...
> dates.1 str(dates.1)
Date[1:5], format: "2011-01-01" "2011-01-02" "2011-01-03"

178

Узнаем будущее: анализ временных рядов

"2011-01-04" ...
А вот как создаются временные ряды типа ts:
> ts(1:10,
# ряд данных
+ frequency = 4,
# поквартально
+ start = c(1959, 2)) # начинаем во втором квартале 1959 года
Qtr1 Qtr2 Qtr3 Qtr4
1959
1
2
3
1960
4
5
6
7
1961
8
9
10
Можно конвертировать сразу матрицу, тогда каждая колонка мат0
рицы станет отдельным временным рядом:
#
>
+
+

матрица данных из трех столбцов
z plot(z,
+ plot.type="single", # поместить все ряды на одном графике
+ lty=1:3)
# типы линий временных рядов
0

Методы для анализа временных рядов и их моделирования включают ARIMA-модели, реализованные в функциях arima(), AR() и VAR(),
структурные модели в StructTS(), функции автокорреляции и частной
автокорреляции в acf() и pacf(), классическую декомпозицию временного ряда в decompose(), STL-декомпозицию в stl(), скользящее
среднее и авторегрессивный фильтр в filter().
Покажем на примере, как применить некоторые из этих функций.
В текстовом файле leaf2-4.txt в директории data записаны результаты длившихся трое суток непрерывных наблюдений над хищным растением росянкой. Листья этого растения постоянно открываются и закрываются «в надежде» поймать и затем переварить мелкое насекомое.
Файл содержит результаты наблюдений над четвертым листом второго
растения, поэтому он так называется. Состояние листа отмечали каж-

179

−2

−1

0

1

2

Построение временного ряда

1962

1964

1966

1968

0

Рис. 54. График трех временных рядов с общими осями времени
дые 40 минут, всего в сутки делали 36 наблюдений. Попробуем сделать
временной ряд из колонки FORM, в которой закодированы изменения
формы пластинки листа (1 — практически плоская, 2 — вогнутая); это
шкальные данные, поскольку можно представить себе форму = 1.5.
Сначала посмотрим, как устроен файл данных, при помощи команды
file.show("data/leaf2-4.txt"):
K.UVL;FORM;ZAGN;OTOGN;SVEZH;POLUPER;OSTATKI
2;1;2;2;1;0;1
1;1;2;1;0;1;1
1;1;2;1;1;0;1
2;2;3;1;1;0;0
1;2;3;1;1;0;0
...
Теперь можно загружать его:
> leaf str(leaf)
’data.frame’: 80
$ K.UVL : int
$ FORM
: int
$ ZAGN
: int
$ OTOGN : int
$ SVEZH : int
$ POLUPER: int
$ OSTATKI: int
> summary(leaf)
K.UVL
Min.
:1.000
1st Qu.:1.000
Median :1.000
Mean
:1.325
3rd Qu.:2.000
Max.
:2.000
OTOGN
Min.
:0.00
1st Qu.:0.00
Median :1.00
Mean
:0.75
3rd Qu.:1.00
Max.
:2.00

obs. of
2 1 1 2
1 1 1 2
2 2 2 3
2 1 1 1
1 0 1 1
0 1 0 0
1 1 1 0

7 variables:
1 1 1 1 1 1 ...
2 2 2 2 2 2 ...
3 3 2 2 2 2 ...
1 1 1 1 1 1 ...
1 1 1 1 1 1 ...
0 0 0 0 0 0 ...
0 0 1 1 1 1 ...

FORM
Min.
:1.0
1st Qu.:1.0
Median :2.0
Mean
:1.7
3rd Qu.:2.0
Max.
:2.0
SVEZH
Min.
:0.0000
1st Qu.:0.0000
Median :0.0000
Mean
:0.1625
3rd Qu.:0.0000
Max.
:1.0000

ZAGN
Min.
:1.0
1st Qu.:2.0
Median :2.0
Mean
:2.5
3rd Qu.:3.0
Max.
:4.0
POLUPER
Min.
:0.000
1st Qu.:1.000
Median :1.000
Mean
:0.925
3rd Qu.:1.000
Max.
:2.000

OSTATKI
Min.
:0.0000
1st Qu.:0.0000
Median :1.0000
Mean
:0.6125
3rd Qu.:1.0000
Max.
:2.0000
Все загрузилось правильно, видимых ошибок и выбросов нет. Теперь
преобразуем колонку FORM во временной ряд:
> forma str(forma)
Time-Series [1:80] from 1 to 3.19: 1 1 1 2 2 2 2 2 2 2 ...
Все правильно, наблюдения велись чуть больше трех (3.19) суток.
Ну вот, а теперь попробуем понять, насколько наши данные периодичны
и есть ли в них тренд (рис. 55):
> (acf(forma, main=""))
Autocorrelations of series ‘forma’, by lag

0.4
−0.2

0.0

0.2

ACF

0.6

0.8

1.0

0.0000 0.0278 0.0556 0.0833 0.1111 0.1389 0.1667 0.1944 0.2222
1.000 0.614 0.287 0.079 0.074 0.068 -0.038 -0.085 -0.132
0.2500 0.2778
-0.137 -0.024
0.3056 0.3333 0.3611 0.3889 0.4167 0.4444 0.4722 0.5000 0.5278
0.030 0.025 -0.082 -0.087 0.027 0.021 -0.043 -0.090 -0.137

0.0

0.1

0.2

0.3

0.4

0.5

Lag

Рис. 55. График автокорреляций для состояния формы листа росянки

182

Узнаем будущее: анализ временных рядов

Эта команда («auto-correlation function», ACF) выводит коэффициенты автокорреляции и рисует график автокорреляции, на котором в
нашем случае можно увидеть, что значимой периодичности нет — все
пики лежат внутри обозначенного пунктиром доверительного интервала, за исключением самых первых пиков, которые соответствуют автокорреляции без лага или с очень маленьким лагом. По сути, это показывает, что в пределах 0.05 суток (у нас период наблюдений — сутки),
то есть около 1 часа, следующее состояние листа будет таким же, как
0
и текущее, а вот на больших интервалах таких предсказаний сделать
нельзя. То, что волнообразный график пиков как бы затухает, говорит
о том, что в наших данных возможен тренд. Проверим это (рис. 56):

0.0
2.0−0.4 −0.2
1.8
0.2
−0.2
−0.6

remainder

0.6

1.7

trend

1.9

seasonal

0.2

> plot(stl(forma, s.window="periodic")$time.series, main="")

1.0

1.5

2.0

2.5

3.0

Time

Рис. 56. График сезонной декомпозиции для состояния формы листа
росянки. Возможный тренд изображен на среднем графике
Действительно, наблюдается тенденция к уменьшению значения формы с течением времени. Мы выяснили это при помощи функции stl()

183

Прогноз

(названа по имени метода, STL — «Seasonal Decomposition of Time Series by Loess»), которая вычленяет из временного ряда три компоненты:
сезонную (в данном случае суточную), тренд и случайную, при помощи
сглаживания данных методом LOESS.
Задача. Попробуйте понять, имеет ли другой признак того же листа
(K.UVL, коэффициент увлажнения, отражающий степень «мокрости»
листа) такую же периодичность и тренд.

7.4. Прогноз
0

Научившись базовым манипуляциям с временными рядами, мы можем попробовать решить задачу построения модели временного ряда.
Построенная модель позволит нам проследить развитие наблюдаемого
процесса в будущем. Кроме того, мы сможем рассмотреть применение
0
более сложных функций анализа временных рядов, а заодно познакомить читателя с общими принципами сравнительного анализа статистических моделей.
Наш пример будет заключаться в прогнозе числа абонентов интернетпровайдера. Исходные данные состоят из:
1) данных о подключениях за декабрь 2004 года;
2) помесячных данных о подключениях в 2005–2008 годах.
> polzovateli cum.polzovateli
>
>
>

oldpar
>
>
>
>
>
>

model01
model02
model03
model04
model05
model06
model07



model08
>
>

model2120 round(-predict(model2123, n.ahead=12, se.fit = TRUE)$se +
+ predict(model2123, n.ahead=12, se.fit = TRUE)$pred)
Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
2009 3134 3283 3437 3628 3817 4031 4253 4490 4728 4969 5213 5463
Что же, теперь можно заняться обещанным в предисловии «предсказанием курса доллара на следующую неделю». Итак, задача: в файле dollar.txt содержатся значения курса доллара Центрального Банка с 1 июля по 9 августа 2011 года, всего за 11 недель. Попробуйте
предсказать курс доллара на две недели вперед. Чтобы проверить эффективность предсказания, возьмите для модели данные по 26 июля, а
предскажите последние две недели.
***
Ответ к задаче про лист росянки. Применим тот же подход,
который мы использовали для признака формы листа:
> uvl str(uvl)
Time-Series [1:80] from 1 to 3.19: 2 1 1 2 1 1 1 1 1 1 ...
> acf(uvl)
> plot(stl(uvl, s.window="periodic")$time.series)
(Графики мы предлагаем вам построить самостоятельно.)
Видно, что на этот раз присутствует некая периодичность, с интервалом около 0.2 суток (примерно 5 часов). А вот выраженного тренда
нет.
Ответ к задаче про цикл. Вот как можно его сделать:
>
+
+
+
+

for (m in 1:14)
{
assign(paste("model0",m,sep=""), arima(cum.polzovateli,
order=c(0,0,m)))
}

Прогноз

189

Функция assign() заменяет стрелочку ( dollar1 dollar for (m in 1:7)
+ {
+ mm for (m in 0:5)
+ {
+ mm for (m in 0:5)
+ {
+ mm
>
+
>

plot(dollar, xlim=c(1,11), ylim=c(27.3,28.5))
lines(predict(model005, n.ahead=14, se.fit = TRUE)$pred,
lty=2)
lines(ts(dollar1[56:70], start=9, frequency=7))

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

191

27.4

27.6

27.8

28.0

28.2

28.4

Прогноз

2

4

6

8

10

Рис. 60. Предсказание курса доллара (предсказанные значения обозначены пунктирной линией). По оси x отложены недели, прошедшие с 1
июня 2011 года

Глава 8
Статистическая разведка
Вот и окончилось наше изложение основных методов статистики
и их использования в R. Теперь настало время применить знания на
практике. Но перед тем, как сделать этот шаг, неплохо представить полученные знания в «концентрированном» виде. Итак, как нужно анализировать данные?

8.1. Первичная обработка данных
Вначале надо выяснить несколько вопросов:
1. Какой тип у данных, каким способом (способами) они представлены?
2. Однородны ли данные — в каких единицах измерены показатели?
3. Параметрические или непараметрические у нас данные — можно
ли предположить нормальное распределение данных?
4. Нужна ли «чистка» данных — есть ли пропущенные данные, выбросы, опечатки?
Если в таблице есть, кроме цифр, буквы, то нужно тщательно проследить, нет ли опечаток. Иногда опечатки бывают почти невидимыми,
скажем, русская буква «с» и английская «c» по виду неразличимы, а
вот программа обработки данных посчитает их разными. К счастью,
базовые команды read.table() и summary() позволяют «выловить» подавляющее большинство подобных проблем. Кстати, если при попытке
загрузки данных в R возникает ошибка, не спешите винить программу:
0
большая часть таких ошибок — это результат неправильного оформления и/или ввода данных.

8.2. Окончательная обработка данных
Для того чтобы помочь читателю разобраться в многочисленных
способах анализа, мы составили следующую таблицу (табл. 8.1). Она

Отчет

193

устроена очень просто: надо ответить всего лишь на три вопроса, и способ обработки найдется в соответствующей ячейке. Главный вопрос —
это тип данных (см. соответствующую главу). Если данные количественные, то подойдет верхняя половина таблицы, если качественные или
порядковые — нижняя половина. Затем надо понять, можно ли посчитать ваши данные распределенными нормально, то есть параметрическими, или по крайней мере без опаски закрыть глаза на некоторое
уклонение от нормальности. И наконец, нужно понять, зависят ли раз0
0
ные колонки данных друг от друга, то есть стоит или не стоит применять парные методы сравнения (см. главу о двумерных данных).
Найдя в таблице нужный метод, загляните в указатель и перейдите
на страницу с более подробным описанием функции. А можно просто
ввести команду help().

8.3. Отчет
Любое полноценное исследование оканчивается отчетом — презентацией, статьей или публикацией в Интернете. В R есть множество автоматизированных средств подготовки отчетов.
Таблицы, созданные в R, можно сохранить в форматах LATEX или
HTML при помощи пакета xtable. Естественно, хочется пойти дальше
и сохранять в каком-нибудь из этих форматов вообще всю R-сессию.
Для HTML такое возможно, если использовать пакет R2HTML:
>
>
>
>
>
>
>

library(R2HTML)
dir.create("example")
HTMLStart("example")
2+2
plot(1:20)
HTMLplot()
HTMLStop()

В рабочей директории будет создана поддиректория example, и туда
будут записаны HTML-файлы, содержащие полный отчет о текущей
сессии, в том числе и созданный график.
Можно пойти и еще дальше. Что, если создать файл, который будет
содержать код R, перемешанный с текстовыми комментариями, и потом
«скормить» этот файл R, так чтобы фрагменты кода заменились на
результат их исполнения? Идея эта называется «literate programming»
(грамотное программирование) и принадлежит Дональду Кнуту, создателю TEX. В случае R такая система используется для автоматической
генерации отчетов — особенности, которая делает R поистине незаме-

194

Статистическая разведка

Зависимые

t.test(...,
paired =
TRUE)

Независимые

Зависимые

wilcox.
test()

wilcox.test
(...,
paired =
TRUE)

cor.test(..., method="pe")

t.test()

Две
группы:
связи

cor.test(..., method="sp")

Независимые

summary()

Параметрические
Непараметрические
Непараметрические

Номинальные
или шкальные

Количественные

Данные

ОдДве
на
группы:
групразличия
па

Три и
более
групп:
связи

Три и
более
групп:
общая
картина

oneway.
test(),
paiwise.
t.test(),
anova(),
lm()

lda(),
manova()





kruskal.
test()

pca(),
tree(),
cor(),
hclust(),
isoMDS(),
cmdscale()





Независимые

chisq.
test(),
prop.
test(),
binom.
test()

glm(...,
"binomial")



cor(),
dist(),
hclust(),
isoMDS(),
corresp()

Зависимые

mcnemar.
test()







Таблица 8.1. Варианты статистического анализа и соответствующие
функции R

Отчет

195

нимым. Для создания подобного отчета надо вначале набрать простой
LATEX-файл и назвать его, например, test-Sweave.Rnw:
\documentclass[a4paper,12pt]{article}
\usepackage[T2A]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage[english,russian]{babel}
\usepackage[noae]{Sweave}
\begin{document} % Тут начинается отчет
\textsf{R} как калькулятор:
=
1 + 1
1 + pi
sin(pi/2)
@
Картинка:
=
plot(1:20)
@
\end{document}
Затем этот файл необходимо обработать в R:
> Sweave("test-Sweave.Rnw")
Writing to file test-Sweave.tex
Processing code chunks ...
1 : echo print term verbatim
2 : echo term verbatim eps pdf
You can now run LaTeX on ’test-Sweave.tex’
При этом создается готовый LATEX-файл test-Sweave.tex. И наконец, при помощи LATEX и dvips или pdfLATEX можно получить результирующий PDF-файл, который изображен на рис. 61.
Такой отчет можно расширять, шлифовать, изменять исходные данные, и при этом усилия по оформлению сводятся к минимуму.

196

Статистическая разведка

R как калькулятор:
> 1 + 1
[1] 2
> 1 + pi
[1] 4.141593
> sin(pi/2)
[1] 1
Картинка:

1:20

5

10

15

20

> plot(1:20)

5

10

15

20

Index

1

Рис. 61. Пример отчета, полученного с помощью команды Sweave()

Отчет

197

Есть и другие системы генерации отчетов, например пакет brew, который позволяет создавать автоматические отчеты в текстовой форме
(разумеется, без графиков), и пакет odfWeave, который может работать
с ODF (формат OpenOffice.org).
0
И все-таки полностью полагаться на автоматику никогда не стоит.
Обязательно проверьте, например, полученные графики — достаточен
ли размер графических файлов, нет ли проблем с цветами. И не забудьте запомнить в файл историю ваших команд. Это очень поможет,
если придется опять вернуться к обработке этих данных. А если вы
захотите сослаться в своем отчете на тот замечательный инструмент,
который помог вам обработать данные, не забудьте вставить в список
литературы то, что выводит строка
> citation()
Удачного вам анализа данных!

Приложение А
Пример работы в R
Это приложение написано для тех, кто не хочет читать длинные
тексты, а хочет как можно скорее научиться обрабатывать данные в
R. Для этого надо «просто» последовательно выполнить приведенные
ниже команды. Желательно не копировать их откуда-либо, а набрать с
клавиатуры: таким способом запомнить и, стало быть, научиться будет
гораздо проще. Хорошо, если про каждую выполненную команду вы
прочтете соответствующий раздел справки (напоминаем, это делается
командой help(команда)). Мы не стали показывать здесь то, что выводит R, и не приводим получающиеся графики: все это вам нужно
будет получить и проверить самостоятельно.
Все команды будут относиться к файлу данных о воображаемых
жуках, состоящему из четырех столбцов (признаков): пол жука (POL:
самки = 0 и самцы = 1), цвет жука: (CVET: красный=1, синий=2, зеленый=3), вес жука в граммах (VES) и длина жука в миллиметрах (ROST):
POL
0
1
0
1
0
1
1
0
1
1

CVET VES
1
10.68
1
10.02
2
10.18
1
8.01
3
10.23
3
9.7
2
9.73
3
11.22
1
9.19
2
11.45

ROST
9.43
10.66
10.41
9
8.98
9.71
9.09
9.23
8.97
10.34

Начинаем...
Создаем на жестком диске рабочую директорию, создаем в ней директорию data; копируем в последнюю файл данных в текстовом формате с расширением *.txt и разделителем-табуляцией (делается из файла Excel или другой подобной прграммы командой меню Save as... /
Сохранить как...). Пусть ваш файл называется zhuki.txt.

Пример работы в R

199

Внимание! Проверьте, чтобы десятичный разделитель был «.», то
есть точкой, а не запятой: половина — это 0.5, а не 0,5! Если десятичный разделитель — запятая, то можно заменить его на точку в любом
текстовом редакторе, например в Блокноте/Notepad.
Открываем программу R. Указываем директорию, где находится
ваш файл данных, при помощи меню: Файл -> Изменить папку -> выбираем директорию, или печатаем команду setwd(...), в аргументе которой должен стоять полный путь к вашей директории (для указания
пути надо использовать прямые слэши — «/».
Для проверки местоположения печатаем команду
> dir("data")
...и нажимаем Enter (эту клавишу нужно нажимать каждый раз
после ввода команды).
Эта команда должна вывести, среди прочего, имя вашего файла
(zhuki.txt).
Читаем файл данных (создаем в памяти программы объект под названием data, который представляет собой копию вашего файла данных). В строке ввода набираем:
> data data.2
+
+
+

data.3 data
Внимание! Внутри R вносить изменения в данные не очень удобно.
Разумно вносить их в текстовый файл данных (открыв его, например,
в Excel), а потом заново читать его в R.
Посмотрим на структуру файла данных. Сколько объектов (obs. =
observations), сколько признаков (variables), как названы признаки и в
каком порядке они следуют в таблице:
> str(data)
Отметьте для себя, что POL и CVET загружены как числа, в то время
как на самом деле это номинальные данные.
Создадим в памяти еще один объект с данными, куда отберем данные только для самок (POL = 0):
> data.f data.m.big 10,]
Кстати, эту команду проще не вводить заново, а получить путем
редактирования предыдущей команды (обычное дело в R). Для вызова
предыдущей команды воспользуйтесь «стрелкой вверх» на клавиатуре.
Использованные знаки «==» и «&» — это логические выражения «таких, что» и «и». Именно они служат критериями отбора. Кроме того,
для отбора данных обязательно нужны квадратные скобки, а если данные табличные (как у нас), то внутри квадратных скобок обязательна
запятая, разделяющая выражения для строк и столбцов.
Добавим еще один признак к нашему файлу: удельный вес жука
(отношение веса жука к его длине) — VES.R:
> data$VES.R write.table(data, "data/zhuki_new.txt", quote=FALSE)

Пример работы в R

201

Охарактеризуем выборку...
Посмотрим сначала на основные характеристики каждого признака
(не имеет смысла для категориальных данных):
> summary(data)
Конечно, команду summary(), как и многие прочие, можно применять как к всему файлу данных, так и к любому отдельному признаку:
> summary(data$VES)
А можно вычислять эти характеристики по отдельности. Минимум
и максимум:
> min(data$VES)
> max(data$VES)
... медиана:
> median(data$VES)
... среднее арифметическое только для веса и для каждого из признаков:
> mean(data$VES)
и
> colMeans(data)
соответственно.
К сожалению, предыдущие команды не работают, если есть пропущенные значения. Для того чтобы посчитать среднее для каждого из
признаков, избавившись от пропущенных значений, надо ввести
> mean(data, na.rm=TRUE)
Кстати, строки с пропущенными значениями можно удалить из таблицы данных так:
> data.o sum(data$VES)

202

Пример работы в R

... или сумму всех значений одной строки (попробуем на примере
второй):
> sum(data[2,])
... или сумму значений всех признаков для каждой строки:
> apply(data, 1, sum)
Для номинальных признаков имеет смысл посмотреть, сколько раз
встречается в выборке каждое значение признака (заодно узнаем, какие
значения признак принимает):
> table(data$POL)
А теперь выразим частоту встречаемости значений признака не в
числе объектов, а в процентах, приняв за 100% общее число объектов:
> 100*table(data$POL)/length(data$POL)
И еще округлим значения процентов до целых чисел:
> round(100*table(data$POL)/length(data$POL), 0)
Одна из основных характеристик разброса данных вокруг среднего значения (наряду с абсолютным и межквартильным разбросом) —
стандартное отклонение (standard deviation). Вычислим его:
> sd(data$VES)
Вычислим напоследок и безразмерный коэффициент вариации (CV):
> 100*sd(data$VES)/mean(data$VES)
Можно вычислять характеристики любого признака отдельно для
самцов и для самок. Попробуем на примере среднего арифметического
для веса:
> tapply(data$VES, data$POL, mean)
Посмотрим, сколько жуков разного цвета среди самцов и самок:
> table(data$CVET, data$POL)

Пример работы в R

203

(Строки — разные цвета, столбцы — самцы и самки.)
А теперь то же самое, но не в штуках, а в процентах от общего числа
жуков:
> 100*table(data$CVET, data$POL)/sum(data$CVET, data$POL)
И наконец, вычислим средние значения веса жуков отдельно для
всех комбинаций цвета и пола (для красных самцов, красных самок,
зеленых самцов, зеленых самок...):
> tapply(data$VES, list(data$POL, data$CVET), mean)
Теперь порисуем диаграммы...
Проверим сначала, как распределены данные, нет ли выбросов. Для
этого построим гистограммы для каждого признака:
> hist(data$VES, breaks=20)
Детализацию гистограммы можно изменять, варьируя число интервалов (breaks).
Можно задать точную ширину интервалов значений признака на
гистограмме (зададим ширину в 20 единиц, а значения признака пусть
изменяются от 0 до 100):
> hist(data$VES, breaks=c(seq(0,100,20)))
Более точно проверить нормальность распределения признака можно при помощи двух команд:
> qqnorm(data$VES); qqline(data$VES)
Обе команды «делают» один график (поэтому мы написали их в
одну строчку через точку с запятой). Чем больше распределение точек
на этом графике отклоняется от прямой линии, тем дальше распределение данных от нормального. Кстати, для того чтобы открыть новое
графическое окно (новый график будет нарисован рядом со старым, а
не вместо него), можно использовать команду dev.new().
Построим диаграмму рассеяния, на которой объекты будут обозначены кружочками. Рост будет по оси абсцисс (горизонтальная ось),
вес — по оси ординат (вертикальная ось):
> plot(data$ROST, data$VES, type="p")
Можно изменять размер кружочков (параметр cex). Сравните:

204

Пример работы в R

> plot(data$ROST, data$VES, type="p", cex=0.5)
и
> plot(data$ROST, data$VES, type="p", cex=2)
Можно изменить вид значка, который обозначает объект (см. номера значков на рис. 62)). Похожую таблицу можно вызвать при помощи
команды example(points) и нажать несколько раз Enter, пока «демонстрация» не окончится.

0

5

10

15

20

25

1

6

11

16

21

*

2

7

12

17

22

.

3

8

13

18

23

+

4

9

14

19

24

*

+

Рис. 62. Номера значков, используемых в стандартных графиках R
Вот, например, обозначим объекты значком 2-го типа (пустым треугольником):
> plot(data$ROST, data$VES, type="p", pch=2)
Можно вместо значков обозначить объекты на диаграмме кодом пола (0/1):

Пример работы в R

205

> plot(data$ROST, data$VES, type="n")
> text(data$ROST, data$VES, labels=data$POL)
Обе команды здесь действуют на один и тот же график. Первая
печатает пустое поле, вторая добавляет туда значки.
Еще можно сделать так, чтобы разные цифры-обозначения имели и
разные цвета (мы добавили «+1», иначе бы значки для самок печатались нулевым, прозрачным цветом):
> plot(data$ROST, data$VES, type="n")
> text(data$ROST, data$VES, labels=data$POL, col=data$POL+1)
А можно самцов и самок обозначить разными значками:
> plot(data$ROST, data$VES, type="n")
> points(data$ROST, data$VES, pch=data$POL)
Другой, более сложный вариант — с использованием встроенных в
R шрифтов Hershey (в порядке исключения приводим его на рис. 63):
>
>
+
+

plot(data$ROST, data$VES, type="n", xlab="Рост", ylab="Вес")
text(data$ROST, data$VES,
labels=ifelse(data$POL, "\\MA", "\\VE"),
vfont=c("serif","plain"), cex=1.5)
... и еще разными цветами:

> plot(data$ROST, data$VES, type="n")
> text(data$ROST, data$VES, pch=data$POL, col=data$POL+1)
Наконец, в случае с разноцветными значками нужно добавить условные обозначения (легенду):
> legend(50, 100, c("male", "female"), pch=c(0,1), col=c(1,2))
Числами указано положение (координаты на графике) верхнего левого угла легенды: первая цифра (50) — абсцисса, вторая цифра (100) —
ордината.
Сохраняем график при помощи меню: Файл -> Сохранить как...
-> PNG -> graph.png или печатаем две команды:
> dev.copy(png, filename="graph.png")
> dev.off()

206

8.0

8.5

9.0

9.5

Вес

10.0

10.5

11.0

11.5

Пример работы в R

9.0

9.5

10.0

10.5

Рост

Рис. 63. Распределение самцов и самок жуков по росту и весу (для
символов использованы шрифты Hershey)
Не забудьте напечатать dev.off()!
Чтобы получить график без посторонних надписей, следует дать
команду plot(..., main="", xlab="", ylab=""), где многоточие обозначает все остальные возможные аргументы.
Сохранять график можно, и не выводя его на экран, сразу в графический файл, опять-таки с помощью двух дополнительных команд:
> png("data.png")
# [... тут печатаем команды для построения графика ...]
> dev.off()
Рисуем коррелограмму:
> plot(data[order(data$ROST), c("ROST", "VES")], type="o")
Здесь мы отсортировали жуков по росту, потому что иначе вместо
графика получилось бы множество пересекающихся линий.
А теперь нарисуем две линии на одном графике:

Пример работы в R

207

> plot(data[order(data$CVET), c("CVET", "VES")], type="o",
+ ylim=c(5, 15))
> lines(data[order(data$CVET), c("CVET", "ROST")], lty=3)
Аргумент ylim задает длину оси ординат (от 0 до 50). Аргумент lty
задает тип линии («3» — это пунктир).
Рисуем «ящик-с-усами», или, по-другому, боксплот (он покажет выбросы, минимум–максимум, квартильный разброс и медиану):
> boxplot(data$ROST)
... а теперь — для самцов и самок по отдельности:
> boxplot(data$ROST ~ factor(data$POL))
Статистические тесты
Достоверность различий для параметрических данных (тест Стьюдента), для зависимых переменных:
> t.test(data$VES, data$ROST, paired=TRUE)
... и для независимых переменных:
> t.test(data$VES, data$ROST, paired=FALSE)
... если нужно сравнить значения одного признака для двух групп:
> t.test(data$VES ~ data$POL)
Если p-value < 0.05, то различие между выборками достоверно.
В R по умолчанию не требуется проверять, одинаков ли разброс данных
относительно среднего.
Достоверность различий для непараметрических данных (тест Вилкоксона):
> wilcox.test(data$VES, data$ROST, paired=TRUE)
Достоверность различий между тремя и более выборками параметрических данных (вариант однофакторного дисперсионного анализа):
> oneway.test(data$VES ~ data$CVET)
Посмотрим, какие именно пары выборок достоверно различаются:
> pairwise.t.test(data$VES, data$CVET, p.adj="bonferroni")

208

Пример работы в R

А теперь проверим достоверность различий между несколькими выборками непараметрических данных:
> kruskal.test(data$VES ~ data$CVET)
Достоверность соответствия для категориальных данных (тест Пирсона, хи-квадрат):
> chisq.test(data$CVET, data$POL)
Достоверность различия пропорций (тест пропорций):
> prop.test(c(sum(data$POL)), c(length(data$POL)), 0.5)
Здесь мы проверили — правда ли, что доля самцов достоверно отличается от 50%?
Достоверность линейной связи между параметрическими данными
(корреляционный тест Пирсона):
> cor.test(data$VES, data$ROST, method="pearson")
... и между непараметрическими (корреляционный тест Спирмена):
> cor.test(data$VES, data$ROST, method="spearman")
Дисперсионный анализ при помощи линейной модели:
> anova(lm(data$ROST ~ data$POL))
Заканчиваем...
Сохраняем историю команд через меню или при помощи команды
> savehistory("zhuki.r")
Все введенные вами команды сохранятся в файл с расширением *.r,
который можно открыть в любом текстовом редакторе и исправлять,
дополнять или копировать в строку ввода программы в следующий раз.
Этот файл можно выполнить целиком (запустить), для этого используется команда source("zhuki.r"). Более того, можно запустить данный
файл, не заходя в R,— для этого в командной строке надо набрать
$ Rscript zhuki.r
Внимание! Всегда сохраняйте то, что вы делали в R!
Выходим из программы через меню или с помощью команды q("no").

Приложение Б
Графический интерфейс (GUI) для R
Прежде чем начать рассказ о GUI, хотелось бы сказать пару слов в
пользу консольного интерфейса. Анализ данных — это творческий процесс, и ничто не продвигает его лучше, чем неспешный ввод с клавиатуры. Естественно, этому занятию должен предшествовать интервал времени, целиком и полностью посвященный чтению документации, книг
и статей по тематике проблемы. Меню и кнопки отвлекают, создавая
иллюзию простоты творческого процесса, требуя нажать их немедленно и посмотреть, что получится. Как правило, не получается ничего, то
есть все равно приходится брать в руки книгу и думать.
Консольный интерфейс в R идеален. Он предоставляет пользователю историю команд, дополнение по Tab (то есть выдает при нажатии
на эту клавишу список возможных продолжений команд, объектов и
даже имен файлов), сохраняет информацию и объекты между сессиями (если пользователь этого захочет, естественно). Нужно поработать
на удаленном компьютере? С консолью нет никаких проблем. А если
воспользоваться программой screen, то можно не бояться разорванных
сессий и случайно закрытых терминалов.
К счастью, практически все графические оболочки для R учитывают это и, как правило, «расширяют» базовые возможности пакета
такими дополнениями, как интегрированный редактор кода с подсветкой, отладчик, редактор массивов данных и т. п.

Б.1. R Сommander
R Сommander, или Rcmdr (рис. 64),— кросс-платформенный графический интерфейс к R, написанный на Tcl/Tk. Его домашнюю страницу можно найти по адресу http://socserv.mcmaster.ca/jfox/Misc/
Rcmdr/.
Автор R Commander Джон Фокс (John Fox) признается, что в случае
программ для статистического анализа он не является фанатом интерфейса, состоящего из меню и диалогов. С его точки зрения, R Commander полезен в основном для образовательных целей при введении в R,
а также в отдельных, очень редких случаях — для быстрого анализа.

210

Графический интерфейс (GUI) для R

Рис. 64. Окно R Сommander
Одной из основополагающих целей, преследуемых при создании интерфейса R Commander, был мягкий перевод пользователя в консоль, где
можно автоматизировать свои действия более глобально.
Пакет распространяется под лицензией GPLv2, и поэтому доступен
во всех стандартных дистрибутивах Linux. Например, для установки в
Debian/Ubuntu достаточно выполнить команду:
$ sudo aptitude install r-cran-rcmdr
Пакет присутствует также и на CRAN, поэтому его установку можно
произвести и собственными силами R:
> install.packages("Rcmdr", dependencies=TRUE)
Обратите внимание на значение опции dependencies: R Commander
зависит от довольно большого числа пакетов. После установки в сессии
R следует выполнить команду
> library(Rcmdr)
При запуске R Commander открывается снабженное довольно «развесистым» меню окно, разделенное на Окно скриптов (Script Window), Окно вывода (Output Window), а также информационное окно Сообщения (Messages). Многие действия в R Commander можно
выполнять через меню, которое достаточно легко настраивается, например с помощью редактирования текстового файла Rcmdr-menus.txt.
В Окне скриптов можно вводить команды, то есть консоль никуда не

RStudio

211

делась. Графики появляются в отдельных окнах, как и в случае «чистого» R.
R Commander имеет русский перевод, который активизируется автоматически, если система русифицирована.
Основная проблема русифицированного R Commander (если вы работаете
в Linux) — это кириллические шрифты, которые выбраны по умолчанию.
Поэтому если вы предпочитаете русский интерфейс, то перед загрузкой R
Commander ему с помощью команды options следует передать примерно следующее:
> options(Rcmdr=list(default.font=
"-rfx-fixed-medium-r-normal-*-20*", suppress.X11.warnings=TRUE))
Растровый шрифт семейства rfx от Дмитрия Болховитянова (20 в конце
строки — это размер по умолчанию), установленный в качестве default.font,
находится обычно в составе пакета xfonts-bolkhov вашего дистрибутива Linux. Второй параметр, suppress.X11.warnings, подавляет надоедливые сообщения при создании новых графических окон.

Выйти из R Commander можно через меню Файл -> Выйти, при этом
можно одновременно закрыть и сессию R. Если же сессия R осталась
открытой, то повторный запуск R Commander выполняется с помощью
команды
> Commander()
Для вводного ознакомления с возможностями R Commander следует
прочитать текст Getting-Started-with-the-Rcmdr.pdf, получить доступ к которому можно через меню Помощь -> Введение в R Commander.
Если по какой-то причине было принято решение анализировать
данные исключительно с помощью R Commander, то можно сделать
так, чтобы при запуске R эта графическая оболочка загружалась автоматически. Для этого в файл пользовательских настроек ~/.Rprofile
достаточно добавить следующие строки:
old Options -> Pane Layout).

Рис. 65. Окно RStudio
В качестве отдельных преимуществ оболочки следует отметить редактор кода с автоматической подсветкой, автодополнением, простейшими преобразования кода (например, выделение нескольких строк в
отдельную функцию), поддержку редактирования не только R кода, но
и документов Sweave.
Авторы среды пошли немного дальше простого предоставления функций R из-под оболочки: в комплекте с RStudio поставляется пакет manipulate, позволяющий интерактивно менять параметры графиков внутри
среды (рис. 66). Так, например, следующий код
>
>
+
+
+
+

library(manipulate)
manipulate(plot(cars, xlim = c(0, x.max), type = type,
ann = label),
x.max = slider(10, 25, step=5, initial = 25),
type = picker("Points" = "p", "Line" = "l", "Step" = "s"),
label = checkbox(TRUE, "Draw Labels"))

RKWard

213

выведет окно настроек, позволяющее менять значение переменных
x.max, type и label и при этом сразу же видеть результат такого изменения на графике.

Б.3. RKWard
RKWard (http://rkward.sourceforge.net) — это довольно удобный, основанный на KDE интерфейс к R (рис. 67). Разработчики RKWard старались совместить мощь R с простотой использования, подобной предоставляемой коммерческими статистическими пакетами (такими, как SPSS или STATISTICA).
Для начинающих пользователей RKWard предоставляет широкие
возможности по выбору многих стандартных процедур статистического анализа по принципу «выдели и щелкни»: достаточно активировать
соответствующий пункт меню.
Для продвинутого пользователя RKWard предлагает удобный редактор кода с подсветкой, автоматической расстановкой отступов, автодополнением — теми вещами, без которых в настоящее время не обходится ни одна среда программирования. Привычная консоль R также
наличествует, она доступна в любое время на вкладке R Console.
Авторы RKWard взяли курс на как можно более полную интеграцию функций R в графическую среду: присутствует браузер текущего окружения (environment) и редактор данных. Есть менеджер
пакетов, умеющий не только устанавливать их, но и следить за обновлениями; обеспечивается прозрачная интеграция со справочной системой. Кроме того, RKWard умеет перехватывать создание графических
окон и добавлять к ним очень удобные функции типа сохранения содержимого в файл одного из стандартных форматов, поддерживаемых
R (PDF, EPS, JPEG, PNG).
Интерфейс RKWard чрезвычайно гибок: пользователь может расширять его за счет написания собственных модулей (кстати, все встроенные средства анализа, доступные сразу же после запуска из меню,—
это такие же модули, но только созданные авторами RKWard).

Б.4. Revolution-R
Ключевые особенности Revolution-R (его домашняя страница — http:
//www.revolutionanalytics.com) — это, разумеется, графический интерфейс (рис. 68), полная интеграция с Microsoft Visual Studio, наличие
полноценного пошагового отладчика и оптимизированных подпрограмм
математических функций BLAS и LAPACK, позволяющих автоматически
использовать преимущества многопроцессорных систем.

214

Графический интерфейс (GUI) для R

Рис. 66. Вот так в RStudio работает пакет manipulate

Revolution-R

215

Рис. 67. Окно RKWard
Как читатель уже понял, основная платформа, на которую «нацеливается» Revolution-R, — это Windows. Версия под Linux тоже существует, но пошаговый отладчик в комплекте отсутствует, что сразу же
снижает ценность пакета.
Revolution-R существует в двух редакциях: бесплатной Community
Edition и очень платной Enterprise Edition (цена на последнюю начинается с $1000 за рабочее место). Главное отличие бесплатной версии
заключается в отсутствии какой-либо интеграции с Visual Studio (а,
следовательно, и в отсутствии отладчика). Впрочем, оптимизированные математические подпрограммы доступны всюду, поэтому получать
преимущества от многопроцессорных вычислений можно совершенно
бесплатно.
Стоит отметить, что существует программа по предоставлению бесплатных лицензий (но без технической поддержки) на Enterprise редакцию при условии использования для научных исследований. Для
получения такой лицензии на веб-сайте программы следует отправить
запрос с использованием «академического» (например, университетского) адреса электронной почты.

216

Графический интерфейс (GUI) для R

Рис. 68. Окно Revolution-R

Б.5. JGR
Предпочитаете, чтобы графический интерфейс отрисовывался средствами Java (рис. 69)? Тогда JGR (Java GUI для R) — это то, что вы
искали. JGR (произносится как «ягуар») был впервые представлен публике в 2004 году, но развивается и поддерживается до сих пор. Пакет
JGR распространяется под открытой лицензией GPLv2, и его домашняя
страница доступна по адресу http://jgr.markushelbig.org/JGR.html.
JGR создавался для Mac OS X «со всеми вытекающими», но под
Windows и Linux он тоже работает. Для использования JGR под Linux
необходимо инсталлировать Sun Java Development Kit (JDK):
$ apt-get install sun-java6-jdk
$ sudo update-java-alternatives -s java-6-sun
$ sudo R CMD javareconf
После установки и настройки Java-окружения следует запустить сессию R и выполнить следующие действия:
> install.packages(’JGR’)
> library(JGR)
> JGR()
Starting JGR run script. ...
Готово — JGR запущен.

Rattle

217

Рис. 69. Окно JGR
В JGR есть встроенный текстовый редактор, в нем есть подсветка
синтаксиса и Tab-завершение команд. Есть и гипертекстовая помощь,
простенькая электронная таблица, возможность управлять объектами,
в том числе и с помощью мыши, имеется и графический интерфейс для
установки и загрузки R-пакетов. С учетом того, что этот GUI может
работать везде, где есть Java, на него стоит обратить внимание.

Б.6. Rattle
Rattle (http://rattle.togaware.com) — сокращение, обозначающее
«R Analytical Tool To Learn Easily» (легкая в освоении среда анализа
R). Программа активно развивается. Rattle — это среда для «разглядывания» данных человеком (рис. 70), она предназначена для интеллектуального анализа данных (data mining), иными словами, для выявления скрытых закономерностей или взаимосвязей между переменными
в больших массивах необработанных данных.
Для установки Rattle под Linux необходимо наличие пакетов ggobi
(программа визуализации данных) и libglade2-dev:
$ apt-get install ggobi libglade2-dev
После этого в консоли R следует выполнить команду
> install.packages("rattle", dependencies=TRUE)

218

Графический интерфейс (GUI) для R

Рис. 70. Стартовое окно Rattle
и откинуться на спинку кресла. Из-за большого количества зависимостей установка занимает много времени.
Запуск GUI производится как обычно:
> library(rattle)
> rattle()

Б.7. rpanel
Пакет rpanel — это простая и одновременно высокоуровневая надстройка над R, написанная в том числе на языке Tcl/Tk. Это не GUI
в строгом смысле слова, а скорее средство (toolkit) для того, чтобы
самостоятельно создать простые GUI, которые потом можно использовать для множества целей. Самое, наверное, важное преимущество
rpanel — вы можете сделать графическое приложение под конкретную
задачу всего за несколько минут!
Если пакет rpanel отсутствует в системе, то надо его скачать и установить с помощью команды install.packages("rpanel"). После этого
можно загрузить саму библиотеку:
> library(rpanel)

219

rpanel

Логика использования пакета довольно проста:
1. С помощью функции rp.control нужно создать объект panel и
объявить в нем нужные нам переменные.
2. Далее необходимо «населить» объект rpanel различными элементами графического интерфейса (ползунками, кнопками, картинками и т. д.) и связанными с ними переменными.
3. В самом конце требуется определить функцию, которая будет выполняться в ответ на взаимодействие пользователя с графическим
интерфейсом.
Разберем простейший пример, в котором функция выводит на экран
одно из двух: или гистограмму полученных данных, или «ящик-с-усами»
(боксплот) в зависимости от выбранного значения в элементе rp.radiogroup (рис. 71).
Объявим объект panel и данные, которые будем отображать:
> panel rp.radiogroup(panel,
+
plot.type,
+
c("histogram", "boxplot"),
+
title="Plot type",
+
action=hist.or.boxp)

#
#
#
#
#

сам объект
переменная
значения переключателя
название переключателя
пользовательская функция

В заключение объявим функцию, которая будет выполняться в ответ на взаимодействие с переключателем:
> hist.or.boxp v c(5.0i, -1.3+8.73i, 2.0)
Комплексные векторы могут принимать те же специальные значения, что и числовые.
Объекты типа logical могут принимать одно из трех значений: TRUE,
FALSE и NA. Кроме этого, существуют (восновном в целях обратной
совместимости) глобальные переменные T и F, имеющие значение TRUE
и FALSE соответственно.
Каждый элемент вектора типа character является, как следует из
названия типа, строкой. Каждая такая строка может иметь произвольную длину.
Как и в языке C, символ обратной косой черты является специальным и предназначен для ввода так называемых escape-последовательностей. В частности, обычный символ обратной косой черты (backslash)
вставляется при помощи последовательности «\\», символ табуляции —
при помощи «\t», а перехода на новую строку — при помощи «\n».
Строки заключаются в одинарные или двойные кавычки. В случае использования двойных кавычек любую двойную кавычку внутри строки
необходимо предварять символом «\». Аналогично следует поступать с
одинарными кавычками внутри строки, заключенной в одинарные кавычки. При выводе на экран всегда используются двойные кавычки.
Вместо вызова функции vector() с указанием типа элемента удобно
вызывать функции, создающие объект нужного типа сразу:
>
>
>
>

v1
v2
v3
v4

v[c("first", "third")]
first third
1
3

Операторы доступа к данным

229

В.2.4. Оператор [ с логическим аргументом
Типичным примером использования оператора [ с логическим вектором в качестве аргумента является конструкция вида
> v[v < 0],
извлекающая из вектора v только отрицательные элементы. Предполагается, что длина аргумента совпадает с длиной вектора, в противном случае аргумент повторяется столько раз, сколько необходимо для
достижения длины вектора. В результат попадают только те элементы
вектора, элемент вектора индексов для которых совпадает с TRUE. Индекс NA выбирает элемент NA (с «никаким» индексом). Таким образом,
длина результата совпадает с общим количеством индексов TRUE и NA:
> v even v[even]
[1] 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36
38 40 42 44 46 48 50
> three v[even & three]
[1] 6 12 18 24 30 36 42 48
Отметим, что в случае замены индексы NA допускаются, только если
длина вектора, стоящего справа от оператора присваивания, равна 1 (в
противном случае непонятно, следует ли использовать очередной элемент замещающего вектора или нет; при длине замещающего вектора
= 1 эта неоднозначность, очевидно, не влияет на результат):
v[c(TRUE, NA)] .Machine$double.eps
[1] 2.220446e-16
Оператор $ обладает одной полезной особенностью: имя элемента нет необходимости передавать полным, достаточно лишь префикса,
определяющего нужный элемент однозначно.
> .Machine$double.ep
[1] 2.220446e-16
В случае, если элемент не определяется однозначно, будет возвращено значение NULL:
> names(.Machine)[grep("double.e", names(.Machine))]
[1] "double.eps"
"double.exponent"
> .Machine$double.e
NULL
Следует, однако, предельно осторожно обращаться с неполными именами при выполнении замены: произойдет добавление нового значения
с коротким именем. Старое значение с полным именем останется неизменным:
> l l
$abc
[1] 1 2 3 4 5 6
> l$a
[1] 1 2 3 4 5 6
> l$a l
$abc
[1] 1 2 3 4 5 6
$a
[1] 1 2 3

7

8

9 10

7

8

9 10

7

8

9 10

В.2.6. Оператор [[
Вообще говоря, оператор $ может быть удален из языка без потери
какой-либо функциональности. Оператор [[ позволяет осуществлять
все те же операции и даже больше. Выражение типа .Machine$double.eps
полностью эквивалентно .Machine[["double.eps"]]. Первое, правда,
короче на 5 символов и выглядит как единое целое.

Операторы доступа к данным

231

Оператор $ не может быть использован, если элемент списка не имеет имени: доступ по индексу возможен только посредством оператора
[[. Кроме этого, оператор [[ следует применять в том случае, когда
интересующее нас имя элемента уже содержится в некоторой переменной:
> l cname l$cname
NULL
> l[[cname]]
[1] 1 2 3 4 5

6

7

8

9 10

Отметим также, что по умолчанию оператор [[ требует точного совпадения переданного названия элемента. Это поведение можно переопределить при помощи аргумента exact:
> l l$a
[1] 1 2 3 4 5 6 7
> l[["a"]]
NULL
> l[["a", exact=FALSE]]
[1] 1 2 3 4 5 6 7

8

9 10

8

9 10

Необходимо всегда иметь в виду важный факт: разобранный ранее
оператор [ работает не только с векторами, но и со списками. Отличие
заключается в том, что оператор [[ всегда возвращает элемент списка,
а [ — список :
> .Machine[[1]]
[1] 2.220446e-16
> .Machine[1]
$double.eps
[1] 2.220446e-16
Кроме этого, аргумент оператора [[ всегда имеет длину 1, а аргумент оператора [ может быть вектором с семантикой, описанной выше.

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

232

Основы программирования в R

жет принимать любую из форм, допустимых для оператора [, как было
описано выше (хотя векторы строк будут индексировать не имена элементов, а имена измерений):
> state.x77[1:2, c("Area", "Population")]
Area Population
Alabama 50708
3615
Alaska 566432
365
В случае если одно (или больше) измерение результата будет иметь
длину 1, то это измерение будет «схлопнуто»: так, ниже видно, что
> state.x77[1:2, "Area"]
Alabama Alaska
50708 566432
является вектором, а не матрицей. Если такое поведение нежелательно, то его можно переопределить заданием аргумента drop:
> state.x77[1:2, "Area", drop=FALSE]
Area
Alabama 50708
Alaska 566432
Так как матрицы являются обычными векторами, то их элементы
можно индексировать по порядку так же, как если бы измерения отсутствовали. Двумерные матрицы при этом индексируются по столбцам;
многомерные — так, что первое измерение меняется наиболее часто, а
последнее — наиболее редко:
> m m
[,1] [,2]
[1,]
1
4
[2,]
2
5
[3,]
3
6
> m[2:4]
[1] 2 3 4
Такая гибкость очень часто приводит к трудноуловимым ошибкам
в случае пропуска запятых при индексировании.
Объект типа data.frame является обычным списком из векторов,
поэтому доступ к нему при помощи оператора [ аналогичен доступу к
списку.

Функции и аргументы

233

В.2.8. Пустые индексы
Отдельные индексирующие векторы могут быть опущены. В таком
случае выбирается все измерение и нет необходимости дополнительно
узнавать его размер. Наиболее часто это используется при выборке отдельных строк или столбцов из матрицы:
> m m[c(2,3), ]
[,1] [,2]
[1,]
2
5
[2,]
3
6
> m[, 2]
[1] 4 5 6
Пустой индекс можно использовать и с векторами (осторожно!):
> v v[] v
[1] 0 0 0 0 0 0 0 0 0 0
Заменяет все элементы вектора v на 0, но при этом сохраняет все
атрибуты (например, имена отдельных элементов). В некоторых случаях это может быть более предпочтительно, чем, скажем, конструкция
вида
> v
>
+
+
+
+
+
+
+
+

Основы программирования в R

norm f f(arg1 = 7, aa = 1, a = 2, ac = 3, 4, 5, 6)
[1] 1 4 5 2 7 10
$ac
[1] 3
[[2]]
[1] 6
Назначение аргументов здесь происходит так:
1. Назначаются аргументы arg1 и aa, так как их имена совпадают
целиком.
2. Значение аргументу aa уже было присвоено на предыдущем шаге, поэтому ab может быть назначен по совпадению префикса a,
ставшего уникальным.
3. Вследствие совпадения имени для переданного аргумента ac происходит добавление аргумента к списку (. . . ). arg2 получает значение по умолчанию. После этого этапа именованных аргументов
не остается.
4. Аргументы bb и cc назначаются по порядку значениями 4 и 5
соответственно. Аргумент 6 добавляется к списку list(...).
При отсутствии оператора троеточия вызов функции оканчивается
ошибкой при попытке назначить аргументы на шагах 3 и 4.

236

Основы программирования в R

Функция args(fun) выводит список всех аргументов функции fun.
В заключение отметим еще одну важную особенность, отличающую
R от многих других языков программирования: аргументы функций вычисляются «лениво», то есть не в момент вызова функции, а в момент
использования (этот факт объясняется просто: язык R является интерпретируемым). В связи с этим надо соблюдать большую осторожность,
скажем, при передаче в качестве аргументов результатов вызовов функций со сторонними эффектами: порядок их вызова может отличаться
от ожидаемого.
С другой стороны, такое поведение позволяет в некоторых случаях
существенно упростить задание значений по умолчанию для аргументов:
f loc.N.POP head(cbind(species=loc.N.POP, measurements))
species N.POP DL.R DIA.ST N.REB N.ZUB DL.OSN.Z DL.TR.V DL.BAZ
1 arvense
1 424
2.3
13
12
2.0
5
3.0
2 arvense
1 339
2.0
11
12
1.0
4
2.5
3 arvense
1 321
2.5
15
14
2.0
5
2.3
4 arvense
1 509
3.0
14
14
1.5
5
2.2
5 arvense
1 462
2.5
12
13
1.1
4
2.1
6 arvense
1 350
1.8
9
9
1.1
4
2.0
...

240

Основы программирования в R

Здесь показано, как работать с двумя связанными таблицами и командой recode(). В одной таблице записаны местообитания (locations),
а в другой — измерения растений (measurements). Названия видов есть
только в первой таблице. Если мы хотим узнать, каким видам какие
признаки соответствуют (см. главу про многомерные данные), то надо слить первую и вторую таблицы. Можно использовать для этого
merge(), но recode() работает быстрее и эффективнее. Надо только
помнить о типе данных, чтобы факторы не превратились в цифры.
Ключом в этом случае является колонка N.POP (номер местообитания).

В.6. Правила переписывания. Векторизация
Встроенные операции языка R векторизованы, то есть выполняются покомпонентно. В таком случае достаточно быстро встает вопрос,
каким образом осуществляются операции в случае, если операнды имеют разную длину (например, при сложении вектора длины 2 и длины
4). За это отвечают так называемые правила переписывания (recycling
rules):
1. Длина результата совпадает с длиной операнда наибольшей длины.
2. Если длина операнда меньшей длины делит длину второго операнда, то такой операнд повторяется (переписывается) столько
раз, сколько нужно до достижения длины второго операнда. После этого операция производится покомпонентно над операндами
одинаковой длины.
3. Если длина операнда меньшей длины не является делителем длины второго операнда (то есть она не укладывается целое число
раз в длину большего операнда), то такой операнд повторяется
столько раз, сколько нужно для перекрытия длины второго операнда. Лишние элементы отбрасываются, производится операция
и выводится предупреждение.
Как следствие этих правил, операции типа сложения числа (то есть
вектора единичной длины) с вектором выполняются естественным образом:
> 2 + c(3, 5, 7, 11)
[1] 5 7 9 13
> c(1, 2) + c(3, 5, 7, 11)
[1] 4 7 8 13

Правила переписывания. Векторизация

241

> c(1, 2, 3) + c(3, 5, 7, 11)
[1] 4 7 10 12
Warning message:
In c(1, 2, 3) + c(3, 5, 7, 11) :
longer object length is not a multiple of shorter object
length
Большинство встроенных функций языка R так или иначе векторизованы, то есть выдают «естественный» результат при передаче в
качестве аргумента вектора. К этому необходимо стремиться при написании собственных функций, так как это, как правило, является ключевым фактором, влияющим на скорость выполнения программы. Разберем простой пример (написанный, очевидно, человеком, хорошо знакомым с языком типа C, но малознакомым с R):
>
>
>
+
+
+

p
+
+
+
+
+
+

v qqline(x)
> qqplot(x, y)
Это графики сравнения распределений. Первый вариант рисует численный вектор х в сравнении с ожидаемым нормальным распределением квантилей, а второй добавляет прямую линию к такому графику,
проводя ее через квартили. Третий вариант выводит квантили х по y
для сравнения их распределений.
> hist(x)
> hist(x, nclass=n)
> hist(x, breaks=b, ...)
выводят гистограммы вектора чисел х. Как правило, выбирается
разумное количество интервалов группировки, но можно их задать прямо аргументом nclass. Кроме того, точки разбиения можно указать
точно аргументом breaks. Если указан аргумент probability=TRUE,
столбцы вместо сумм представляют относительные частоты.
> dotchart(x, ...)
создает точечную диаграмму из данных, приведенных в х. В точечной диаграмме ось у дает метки данных из х, а ось х отображает их
величину. Это позволяет, например, легко отобрать все записи данных,
значения которых лежат в определенных диапазонах.
> image(x, y, z, ...)
> contour(x, y, z, ...)
> persp(x, y, z, ...)

Графические функции

267

рисуют графики трех переменных. График image() рисует сетку
прямоугольников, используя различные цвета для отображения величины z, график contour() рисует горизонтали, представляющие уровни
z, а график persp() рисует 3D-поверхность.

Г.15.4. Параметры функций высокого уровня
Есть ряд параметров, которые могут быть переданы высокоуровневым графическим функциям, например
..., add=TRUE
принуждает функцию действовать в качестве низкоуровневой графической функции, накладывая свой график на текущий (это работает
только для некоторых функций).
..., axes=FALSE
подавляет вывод осей, что бывает полезно для того, чтобы добавить
собственные оси с помощью функции axis(). По умолчанию axes=TRUE,
что означает отображение осей.
..., log="x"
..., log="y"
..., log="xy"
Приводит х, у или обе оси к логарифмическому масштабу. Это сработает для многих, но не всех видов графиков.
Аргумент
..., type
контролирует тип выводимого графика, в частности
..., type="p"
выводит отдельные точки (принято по умолчанию),
..., type="l"
рисует линии,
..., type="b"
выводит точки, соединенные линиями вместе,

268

Выдержки из документации R

..., type="o"
рисует точки, перекрытые линиями,
..., type="h"
рисует вертикальные линии от точек до нулевого уровня оси,
..., type="s"
..., type="S"
рисует ступенчатую функцию. В первом варианте точка наверху, а
во втором — внизу.
..., type="n"
не рисует ничего. Однако оси по-прежнему выводятся (это задано
по умолчанию), и система координат создается в соответствии с данными. Идеально подходит для создания участков последовательностью
низкоуровневых графических функций.
..., xlab="строка"
..., ylab="строка"
Подписи осей х и у.
..., main="строка"
Название всего графика большим шрифтом, расположено в верхней
части рисунка.
..., sub="строка"
Подзаголовок меньшим шрифтом, расположенный чуть ниже оси х.

Г.15.5. Низкоуровневые графические команды
Иногда высокоуровневые графические функции не дают именно такой график, какой вам нужен. Можно использовать низкоуровневые
графические команды, чтобы добавить дополнительную информацию
(например, точки, линии или текст) к текущему рисунку.
Некоторые из наиболее полезных низкоуровневых функций:
> points(x, y)
> lines(x, y)

Графические функции

269

добавляют точки или связывающие линии к текущему графику. Для
plot() аргументы типа type= также могут быть переданы в эти функции (по умолчанию "р" для points() и "l" для lines()).
> text(x, y, labels, ...)
добавляет текст в рисунок в точке, заданной х и у. Часто labels —
целочисленный или символьный вектор, в этом случае labels[i] выводится в точке (x[i], y[i]). Значение по умолчанию — 1:length(x).
Эта функция часто используется в таком сочетании:
> plot(x, y, type="n")
> text(x, y, names)
Графический параметр type="n" подавляет точки, но создает оси,
а функция text() поставляет специальные символы, которые заданы
именами в символьном векторе для каждой из точек.
>
>
>
>

abline(a, b)
abline(h=y)
abline(v=x)
abline(lm.obj)
добавляют в текущий рисунок линии наклона b, отсекающие отрезок

a.
«h=y» может быть использовано для задания y-координаты высоты
горизонтальной линии, проходящей через рисунок, а «v=x» аналогичным образом задает вертикальную линию. «lm.obj» может быть списком с компонентами длины 2 (например, результатом функции подгонки модели), которые используются в качестве отсекающего отрезка и
наклона (именно в таком порядке).
> polygon(x, y, ...)
строит многоугольник, определенный вершинами из (x, у), можно
дополнительно затенить его штриховкой или закрасить его, если графическое устройство позволяет закраску изображений.
> legend(x, y, legend, ...)
добавляет легенду к текущему графику в указанной позиции. Начертание символов, стиль линий, цвета и т. п. определяются метками
в символьном векторе описания. Должен быть задан по меньшей мере
один аргумент v (вектор той же длины, как описание) с соответствующими значениями единиц измерения, а именно:

270

Выдержки из документации R

> legend(..., fill=v)
так определяются цвета для заполнения прямоугольников,
> legend(..., col=v)
так — цвета, которыми рисуются точки или линии.
> legend(..., lty=v)
Стиль линии
> legend(..., lwd=v)
Ширина линии
> legend(..., pch=v)
Выводимые символы (символьный вектор)
> title(main, sub)
добавляет заголовок main в начало текущего рисунка и подзаголовок sub внизу.
> axis(side, ...)
добавляет оси к текущему рисунку со стороны, заданной первым
аргументом (от 1 до 4, считая по часовой стрелке снизу). Другие аргументы контролируют расположение оси внутри или рядом с рисунком
и позиции меток. Для добавления в дальнейшем пользовательских осей
полезно вызывать plot() с аргументом axes=FALSE.
Низкоуровневые графические функции, как правило, требуют некоторой позиционной информации (например, х- и у-координаты), чтобы
определить место для нового элемента рисунка. Координаты определяются в предыдущей высокоуровневой графической команде и выбираются на основе предоставленной информации.
Если нужны х и у, то достаточно задать один аргумент — список с
элементами по имени x и y. Матрица с двумя столбцами также подходит для ввода. Таким путем функции типа locator() (см. ниже) могут
использоваться для интерактивного определения позиции на рисунке.

Графические функции

271

Г.15.6. Математические формулы
В некоторых случаях бывает полезно добавить математические символы и формулы на график. Это можно сделать в R не символьной
строкой в text, mtext, axis или title, а описанием выражения. Например, следующий код рисует формулу для биномиальной функции
распределения:
> text(x, y, expression(paste(bgroup("(", atop(n, x), ")"),
+ p^x, q^{n-x})))
Более подробную информацию, включая полный перечень доступных возможностей, можно получить в R с помощью команд:
> help(plotmath)
> example(plotmath)
> demo(plotmath)

Г.15.7. Интерактивная графика
R также обеспечивает функции, которые позволяют пользователям
получать или добавлять информацию на график с помощью мыши.
Простейшей из них является функция locator().
> locator(n, type)
ожидает щелчка левой кнопкой мыши. Это продолжается до тех
пор, пока не будет выбрано n (по умолчанию 512) точек или нажата
любая другая кнопка мыши. Аргумент type применим для вывода отмеченных точек и действует так же, как на высокоуровневые графические команды; по умолчанию вывод отсутствует. locator() возвращает
координаты отдельных точек как список с двумя компонентами x и y.
locator() обычно вызывают без аргументов. Это особенно полезно
для интерактивного выбора позиции для графических элементов, таких как описания и метки, когда трудно рассчитать заранее, куда надо
пометить графический элемент. Например, чтобы поставить некоторый
информативный текст вблизи точки-выброса, может быть полезна команда
> text(locator(1), "Outlier", adj=0)
(Команда locator() будет проигнорирована, если текущее устройство, например postscript, не поддерживает интерактивного взаимодействия.)

272

Выдержки из документации R

> identify(x, y, labels)
позволяет пользователю выделить любую из точек с определенными
координатами х и у путем вывода поблизости соответствующего компонента labels (или индекс точки, если labels отсутствует). Возвращает
индексы выбранных точек, когда нажимают другую кнопку мыши.
Иногда мы хотим идентифицировать именно точки на графике, а
не их координаты. Например, мы можем пожелать, чтобы пользователь
выбрал некоторые интересующие наблюдения на графическом дисплее,
а затем обработать эти наблюдения разными способами. Задав ряд (x,
у) координат двумя численными векторами х и у, мы могли бы использовать функцию identify() следующим образом:
> plot(x, y)
> identify(x, y)
Функция identify() сама не рисует, она просто позволяет пользователю переместить курсор мыши и нажать левую кнопку мыши вблизи точки. Если возле указателя мыши есть точка, она будет помечена
своим индексом (то есть своей позицией в х/y векторах), выведенным
поблизости.
Можно также использовать строки (например, название случая) в
качестве подписи, используя аргумент labels для identify(), или вообще отключить выделение аргументом plot = FALSE. Когда работа с
графиком прекращается (см. выше), identify() возвращает индексы
выбранных точек; вы можете использовать эти индексы для извлечения отдельных точек из первоначальных векторов х и y.

Г.15.8. par()
При создании графики, в частности для выступлений или публикаций, R по умолчанию не всегда выводит именно то, что требуется.
Однако можно настроить практически каждый аспект вывода, используя графические параметры. R поддерживает список из большого числа
графических параметров, которые контролируют среди многих других
такие вещи, как стиль линии, цвет, расположение рисунка и текста.
Каждый графический параметр имеет имя (например, «col», который
контролирует цвет) и значение (например, номер цвета).
Отдельный список графических параметров поддерживается для
каждого активного устройства, и каждое устройство, когда оно инициализировано, по умолчанию имеет набор параметров. Графические
параметры можно задать двумя способами: либо постоянно, затронув
все графические функции, связанные с текущим устройством, либо временно, затрагивая только один вызов графической функции.

Графические функции

273

Функция par() используется для доступа к списку параметров и изменению графических параметров для текущего графического устройства.
> par()
без параметров возвращает список всех графических параметров и
их значения для текущего устройства.
> par(c("col", "lty"))
Если аргумент — символьный вектор, команда возвращает только
названные графические параметры.
> par(col=4, lty=2)
с именованными аргументами (или одним общим списком аргументов) устанавливает значения для соответствующих графических параметров и возвращает первоначальные значения параметров как список.
При настройке параметров графики функцией par() изменение значений параметров постоянно в том смысле, что все будущие вызовы графических функций (от текущего устройства) будут зависеть от нового
значения.
Заметим, что вызовы par() всегда влияют на значения графических
параметров глобально, даже когда par() вызывается внутри функции.
Это часто нежелательное поведение — как правило, мы хотим установить некоторые параметры графики для некоторых рисунков, а затем
восстановить исходные значения, чтобы не повлиять на пользовательскую сессию R. Вот как можно восстановить некоторые параметры, временно сохраняя результат par():
> oldpar par(oldpar)
Чтобы сохранить и восстановить вообще все настраиваемые графические параметры, используем
> oldpar par(oldpar)

274

Выдержки из документации R

Графические параметры часто могут быть переданы в графические
функции как именованные аргументы. Это действует так же, как передача аргументов в функции par(), за исключением того, что изменения
действуют только в течение срока действия вызова функции. Например:
> plot(x, y, pch="+")
производит коррелограмму, используя знак «+» и не изменяя символ печати по умолчанию для будущих графиков.

Г.15.9. Список графических параметров
В следующих подразделах детализируются многие из широко используемых графических параметров. В справочной документации сведения по функции par() представлены кратко, здесь дан несколько
более подробный вариант.
Графические параметры будут представлены в следующей форме:
..., имя = значение
(Заметим, что axes — это не графический параметр, а аргумент для
нескольких методов plot; см. документацию к xaxt и yaxt.)
Графики в R состоят из точек, линий, текста и полигонов (заполненных областей). Графические параметры существуют для того, чтобы контролировать, как эти графические элементы будут нарисованы,
а именно:
..., pch="+"
Символ, который будет использоваться для рисования точек. Умолчание различно для различных графических устройств, однако это, как
правило, кружок. Выводимые точки, как правило, немного выше или
ниже соответствующей позиции, за исключением ситуации, когда как
символ вывода используется ".".
..., pch=4
Когда pch задается как целое число между 0 и 25 включительно,
получается специальный символ. Чтобы увидеть, какие это символы,
используйте команду
> legend(locator(1), as.character(0:25), pch = 0:25)

Графические функции

275

Символы с 21 по 25 могут показаться дублирующими предыдущие
символы, но у них разные цвета: см. помощь по points() и приведенные
примеры.
Кроме того, pch может быть символом или числом в диапазоне
32:255, представляющем символ в текущем шрифте.
..., lty=2
Это — тип линии. Альтернативные стили линии не поддерживаются
всеми графическими устройствами (и различаются у разных устройств),
но тип 1 — это всегда сплошная линия, линия типа 0 всегда невидима,
линии типов 2 и более являются точечными или штриховыми, или сочетаниями обоих типов.
..., lwd=2
Ширина линии. Желаемая ширина линий, кратная «стандартной»
толщине линии. Влияет на линии осей, а также линий, выводимых командой lines() и другими. Не все устройства это поддерживают, многие имеют ограничения на ширину.
..., col=2
Это цвета, которые будут использоваться для точек, линий, текста,
заполненных областей и изображений. Номер из текущей палитры (см.
?palette), или название цвета.
...,
...,
...,
...,

col.axis=
col.lab=
col.main=
col.sub=

Это цвета, которые будут использоваться соответственно для аннотации осей, х и у меток, заголовка и подзаголовка.
..., font=2
Параметр, который определяет, какой шрифт использовать для всего текста на графике. Если возможно, устройство вывода организует это
так, что 1 соответствует простому тексту, 2 — выделенному, 3 — курсиву, 4 — выделенному курсиву, а 5 — символьному шрифту (включая
греческие буквы).

276

...,
...,
...,
...,

Выдержки из документации R

font.axis=
font.lab=
font.main=
font.sub=

Шрифты, которые будут использоваться соответственно для аннотации осей, х и у меток, заголовка и подзаголовка.
..., adj=-0.1
Юстировка текста относительно позиции вывода. 0 означает юстировать влево, 1 означает юстировать вправо и 0.5 — центрировать горизонтально относительно позиции вывода. Фактическое значение означает долю текста, которая появляется с левой стороны от позиции вывода.
Значение −0.1 оставляет между текстом и позицией вывода пробел в
10% от ширины текста.
..., cex=1.5
Масштаб символов. Значение — это желаемый размер символов текста (включая выводимые символы) по отношению к размеру шрифта
по умолчанию.
...,
...,
...,
...,

cex.axis=
cex.lab=
cex.main=
cex.sub=

Масштаб символов, который будет использоваться для аннотации
оси, х и у меток, заголовков и подзаголовков.
Оси и шкалы
Многие высокоуровневые рисунки R имеют оси, вы также можете самостоятельно конструировать оси низкоуровневыми графическими функциями axis(). Оси имеют три основных компонента: линию оси
(стиль линии, который контролируется графическим параметром lty),
шкалу (которая обозначает деление линии оси единицами измерения) и
метки шкалы (которые обозначают единицы измерения). Две последние
компоненты можно настроить следующими параметрами графики:
..., lab=c(5, 7, 12)
Первые два числа — это желаемое количество интервалов по х и у
осям соответственно. Третье — желаемая длина меток оси, в символах
(включая запятую).

Графические функции

277

..., las=1
Ориентация меток оси. 0 означает «всегда параллельно оси», 1 означает «всегда горизонтально» и 2 — «всегда перпендикулярно оси».
..., mgp=c(3, 1, 0)
Это — позиции компонентов оси. Первый компонент — это расстояние от метки оси до позиции оси, в текстовых строках. Второй компонент – это расстояние до меток шкалы, и окончательный компонент –
это расстояние от позиции оси до линии оси (обычно ноль). Положительные числа «уводят» за пределы региона рисования, отрицательные
числа — внутрь региона.
..., tck=0.01
Длина меток шкалы как доля размера региона рисования. Когда tck
невелик (менее 0.5), метки шкалы на осях х и у одинакового размера.
Значение 1 дает сетку линий. Отрицательные значения дают метки за
пределами региона рисования. Используйте tck = 0.01 и mgp = c(1,
-1.5, 0) для внутренних меток шкалы.
..., xaxs="r"
..., yaxs="i"
Стили для осей х и у соответственно. Со стилями "i" (внутренний)
и "r" (по умолчанию) метки шкалы всегда находятся внутри диапазона
данных, однако стиль "r" оставляет небольшое пространство на краях.
(S имеет и другие стили, которые не были реализованы в R).

Г.15.10. Края рисунка
Одиночный рисунок в R называется «изображение» и включает регион рисования, окруженный полями (возможно, содержащими метки
осей, заголовки и т. д.).
Графические параметры, контролирующие формат изображения:
..., mai=c(1, 0.5, 0.5, 0)
Определяет ширину соответственно нижнего, левого, верхнего и правого полей; измеряется в дюймах.
..., mar=c(4, 2, 2, 1)

278

Выдержки из документации R

Аналогично mai, за исключением того, что единица измерения — это
текстовые строки.
mar и mai эквивалентны в том смысле, что настройка одного вызывает изменение значения другого. Часто значения по умолчанию выбраны
для этого параметра слишком большими; правый край требуется редко, и верхнее поле не нужно, если нет названия. Нижнее и левое поля
должны быть достаточными для размещения оси и меток масштаба.
Кроме того, размер по умолчанию выбирается без учета размера
устройства вывода: например, использование устройства postscript()
с параметром height=4 приведет к рисунку, 50% которого составляют
поля, если используются mai или mar по умолчанию.
Когда выводятся составные изображения (см. ниже), поля уменьшаются, однако этого может быть недостаточно, когда на одной странице
много изображений.

Г.15.11. Составные изображения
R позволяет создать массив из n×m изображений на одной странице.
Каждое изображение имеет свои поля, а массив изображений может
быть окружен наружными полями.
Графические параметры, связанные с составными изображениями,—
это:
..., mfcol=c(3, 2)
..., mfrow=c(2, 4)
Они устанавливают размер массива составных изображений. Первый аргумент у них — это количество строк, а второй — число столбцов.
Единственная разница между ними в том, что установление mfcol выводит изображения по колонкам, а mfrow заполняет массив построчно.
Установка любого из этих показателей может сократить базовый
размер символов и текста (контролируется при помощи par("cex")) и
pointsize устройства вывода. В формате с двумя строками и столбцами базовый размер уменьшается на 0.83, если есть три или больше
строки или столбца, коэффициент уменьшения равен 0.66.
..., mfg=c(2, 2, 3, 2)
Это — позиция текущего изображения в составном окружении. Первые два номера — это строка и столбец текущего составного изображения; последние два числа — это строки и столбцы в составном массиве
изображений. Используйте этот параметр для перехода между изображениями в массиве. Для отображения разноразмерных изображений на
одной и той же странице можно даже использовать отличающиеся от
правильных значений значения для последних двух аргументов.

Графические функции

279

..., fig=c(4, 9, 1, 4)/10
Позиция текущего изображения на странице. Значения параметров — это позиции левого, правого, верхнего и нижнего краев соответственно, в процентах от размера страницы, измеряемого от нижнего
левого угла. Пример соответствует изображению в нижнем правом углу
страницы. Используйте этот параметр для произвольного позиционирования изображений на странице. Если вы хотите добавить изображение
к текущей странице, используйте new=TRUE.
..., oma=c(2, 0, 3, 0)
..., omi=c(0, 0, 0.8, 0)
Размер внешних полей. Как mar и mai, первое измеряется в текстовых строках, а второе — в дюймах, начиная с нижнего поля и по часовой
стрелке.
Внешнее поле особенно полезно для заметок на полях и т. д. Текст
может быть добавлен к внешнему полю функцией mtext() с аргументом
outer=TRUE. По умолчанию внешних полей не существует.
Более сложные составные изображения могут быть выведены функциями split.screen() и layout(), а также с использованием пакетов
grid и lattice.

Г.15.12. Устройства вывода
R может генерировать графику (разного качества) почти для любого
типа дисплея или устройства печати, но сначала он должен быть проинформирован, с каким типом устройства нужно взаимодействовать. Это
делается путем запуска «драйвера устройства вывода». Смысл драйвера устройства вывода состоит в преобразовании графических команд R
(«нарисовать линию», например) к такой форме, которую конкретное
устройство вывода может понять.
Драйвер устройства вывода запускают, вызывая функцию драйвера
устройства. Для каждого устройства графического вывода существует
единственная функция драйвера устройства (введите help(Devices))
для получения полного списка. Например, ввод команды postscript()
перенаправляет весь последующий графический вывод на принтер, преобразуя его в формат PostScript. Некоторые широко используемые драйверы устройств вывода:
X11() служит для использования с X11 — оконной системой UNIXподобных ОС;
windows() нужна для использования в Windows;

280

Выдержки из документации R

quartz() используется в Mac OS X;
postscript() работает для печати на принтерах PostScript, а также
создания графических файлов PostScript;
pdf() выводит PDF-файл, который может быть включен в другие PDFфайлы;
png() выводит растровый файл PNG;
jpeg() выводит растровый файл JPEG (этот формат лучше использовать только для рисунков-изображений).
Когда вы закончите работать с устройством вывода, убедитесь, что
остановили драйвер устройства вывода командой dev.off(). Это гарантирует, что устройство остановится правильным образом, например
в случае печатного устройства это гарантирует, что каждая страница
будет отправлена на принтер.

Г.15.13. Несколько устройств вывода одновременно
При профессиональном использования R зачастую бывает полезно
иметь несколько графических устройств вывода, используемых одновременно. Конечно, в любой отдельный момент времени только одно
графическое устройство может принимать графические команды: оно
обозначается как «текущее» устройство. Когда несколько устройств открыты одновременно, они образуют пронумерованную последовательность с именами, определяющими тип устройства вывода в каждой из
позиций.
Каждый новый вызов функции драйвера устройства вывода открывает новое графическое устройство, что расширяет на одну позицию
список устройств вывода. Это устройство становится текущим устройством вывода.
> dev.list()
Эта команда возвращает количество и наименование всех активных
устройств вывода. Устройство в позиции 1 списка всегда является nullустройством, которое вообще не принимает графических команд.
> dev.next()
> dev.prev()
возвращает номер и название, соответственно, последующего или
предыдущего графического устройства.

Пакеты

281

> dev.set(which=k)
может быть использована для изменения текущего графического
устройства на устройство в позиции k списка устройств вывода. Возвращает номер и метку устройства.
> dev.off(k)
закрывает графическое устройство в позиции k списка устройств вывода. Для некоторых устройств, таких как postscript, это будет либо
немедленно распечатывать файл, либо корректно завершать файл для
последующей печати, в зависимости от того, как было инициировано
устройство.
> dev.copy(device, ..., which=k)
> dev.print(device, ..., which=k)
делает копию устройства k. Здесь устройство — это функция драйвера устройства, например postscript (без скобок), в случае необходимости с дополнительными аргументами, назначенными посредством
«...».
dev.print() работает аналогично dev.copy(), но копируемое устройство немедленно закрывается.
> graphics.off()
закрывает все графические устройства, кроме null-устройства.

Г.16. Пакеты
Все функции и данные R хранятся в пакетах. Их содержимое становится доступным, лишь когда пакет загружается. Это делается как
для производительности, так и для помощи разработчикам, которые
предохранены от конфликтов имен.
Чтобы узнать, какие пакеты установлены в вашей инсталляции, наберите команду library() без аргументов. Для загрузки конкретного
пакета (например, пакета boot) используйте такую команду:
> library(boot)
Пользователи, подключенные к Интернету, могут использовать функции install.packages() и update.packages() для установки и обновления пакетов.
Чтобы узнать, какие пакеты уже загружены, используйте

282

Выдержки из документации R

> search()
для отображения списка. Некоторые пакеты могут быть загружены,
но не доступны в этом списке: они будут включены в список, выводимый
> loadedNamespaces()
Чтобы увидеть список всех доступных тем в системе помощи, касающихся установленных пакетов, используйте help.start(). Эта команда запускает HTML-систему помощи, затем надо перейти на список
пакетов в разделе «Ссылки».

Г.16.1. Стандартные и сторонние пакеты
Стандартные пакеты содержат базовые функции, которые позволяют R работать, а также наборы данных и стандартные статистические
и графические функции, которые описаны выше. Они должны быть
автоматически доступны в любой инсталляции R.
Существуют тысячи сторонних пакетов для R, написанных различными авторами. Некоторые из этих пакетов реализуют специализированные статистические методы, другие предоставляют доступ к данным или оборудованию, третьи призваны дополнять руководства. Некоторые из них (рекомендованные, «recommended») распространяются с
каждым бинарным дистрибутивом R. Большинство из них доступны
для загрузки с CRAN (http://CRAN.R-project.org и его зеркала) и
других хранилищ, например http://www.bioconductor.org.

Г.16.2. Пространство имен пакета
Пакеты должны иметь пространства имен. Пространство имен делает три вещи: оно позволяет автору пакета скрыть функции и данные,
предназначенные только для внутреннего использования, предотвращает функции пакета от поломки, когда пользователь (или автор другого
пакета) выбирает название, совпадающее с названием из пакета, а также предусматривает способ сослаться на объект в рамках конкретного
пакета.
Например, t() — это функция транспонирования в R, но пользователи могут определять свои собственные функции с именем «t». Пространство имен предохраняет переопределения пользователя и прерывает все функции, которые пытаются транспонировать матрицу.
Есть два оператора, которые работают с пространством имен. Двойное двоеточие — оператор «::» — выбирает определения из определенного пространства имен. В примере выше функция транспонирования
всегда будет доступна как base::t(), поскольку оно определяется в

Пакеты

283

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

Приложение Д
Краткий словарь языка R
Здесь мы собрали примерно пятьдесят самых-самых, на наш взгляд,
необходимых команд, операторов и обозначений. Список мы постарались сделать как можно короче, для того чтобы эти команды было
легче запомнить.
? Помощь