Читать книгу Основы статистической обработки педагогической информации - Денис Владимирович Соломатин - Страница 5

Глава 1. Первое знакомство
§2. Визуализация и преобразование данных

Оглавление

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

Во второй части книги изложены некоторые полезные инструменты, которые дают немедленную отдачу по следующим причинам:

1) Визуализация прекрасна для начала работы в R, потому что выигрыш очевиден, научитесь делать элегантные и информативные графики, которые помогут понять собранные данные. Погрузитесь в визуализацию изучая основное содержимое библиотеки ggplot2, и узнаете мощные методы превращения табличных данных в графики.

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

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

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

Среди последующих глав, сконцентрированных на изложении инструментов исследования, присутствует описание рабочих процессов. В соответствующем разделе разбираются основы рабочего процесса, автоматизация сценариями, на примере готовых решений иллюстрируются ведущие практики написания и организации R-кода. Это настроит на успех в долгосрочной перспективе, так как даёт инструменты для реализации конкретных проектов.

Как было показано во введении, простой график приносит больше информация для ума аналитика, чем любое другое представление данных. Покажем, как визуализировать данные с помощью ggplot2. В R имеется несколько систем для построения графиков, но ggplot2 является одним из самых элегантных и самых универсальных, так как ggplot2 реализует графический язык, схожий в системе описания и построения графиков. С ggplot2, многое делается быстрее, изучив одну систему команд можно применять её в самых неожиданных местах.

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

library(tidyverse)

Эта одна строка кода загружает ядро tidyverse, пакеты, которые будут использоваться практически при каждом анализе данных. После её выполнения в консоли показывается, какие функции из tidyverse конфликтуют с функциями в базе R (или из других пакетов, которые могли быть загружены). Если запустите этот код и получите сообщение «Ошибка в library(tidyverse) :нет пакета под названием ‘tidyverse’», то нужно будет сначала установить его, а затем снова запустить library() следующим образом:

install.packages("tidyverse")

library(tidyverse)

Достаточно однократно установить пакет, но необходимо подгружать его каждый раз, когда открываете новую рабочую сессию. Если потребуется в явном виде указать из какого пакета вызывается функция (или набор данных), то будем использовать специальную нотацию с двойным двоеточием, например, ggplot2::ggplot() сообщает явным образом, что мы используем функцию ggplot() из пакет ggplot2. Давайте разберем первый график из предыдущей главы, чтобы ответить на a вопрос: используют ли автомобили с большими двигателями больше топлива, чем автомобили с маленькими двигатели? Аналогично риторическому: лучше ли осваивают математику ученики в специализированных физико-математических классах, чем ученики обучающиеся в классах с минимальным количеством уроков математики? Вы, вероятно, уже знаете ответ, но попробуйте конкретизировать. Какова взаимосвязь между размером двигателя и топливная эффективность, либо взаимосвязь между количеством учебного времени, выделяемого на элементарную математику, и успехами страны в космической отрасли, как она выглядит: положительно? отрицательно? линейно? нелинейно?

Вы можете проверить свой ответ с помощью базы данных mpg хранящейся в ggplot2 (она же ggplot2::mpg). База данных представляет собой таблицу переменных (в столбцах) и наблюдаемых значений (в строках). База mpg содержит наблюдения, собранные американскими агентством по охране экологии на 38 моделях автомобилей.

Среди прочих переменных в базе mpg хранятся:

1. displ, – объем двигателя автомобиля, в литрах;

2. hwy, – топливная экономичность автомобиля на шоссе, в милях на галлон (mpg).

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

Чтобы визуализировать mpg, запускается следующий код, который отобразит displ на ось x и hwy на ось y:

ggplot (data = mpg) +

geom_point (mapping = aes (x = displ, y = hwy))

По графику становится очевидной отрицательная связь между размером двигателя (displ) и топливной экономичностью (hwy). Другими словами, автомобили с большими двигателями использует больше топлива, равно как и большее количество учебного времени приводит к заведомо лучшим результатам обучающихся. Может ли это подтвердить или опровергнуть гипотезу о топливной экономичности и размере двигателя?

С помощью ggplot2 можно начать построение графика с помощью функции ggplot(). ggplot() создает систему координат, которую можно наполнить слоями. Первый аргумент функции ggplot() это набор данных, используемый в диаграмме. Таким образом, ggplot(data=mpg) создает пустой график, но это не очень информативно. Вы завершите построение графика, добавив один или несколько слоёв в ggplot(). Функция geom_point() добавляет слой точек.

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

Каждая функция geom в ggplot2 принимает аргумент, который определяет, как переменные в наборе данных сопоставляются с визуальными свойствами. Аргумент mapping всегда сопряжен с функцией aes(), а аргументы x и y в функции aes () определяют, какие переменные нужно сопоставить осям x и y соответственно. ggplot2 ищет сопоставленные переменные в данных аргумента, например, в таблице mpg.

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

ggplot(data = <данные>) +

<функция geom>(mapping = aes(<функция geom ><соответствие>))

В дальнейшем разберем детально, как заполнить и расширить этот шаблон, чтобы построить графики различных типов. Начнем с компоненты <соответствие>.

Упражнения

1. Запустите ggplot(data=mpg). Что получится?

2. Сколько строк находится в mpg? Сколько там колонок?

3. Что описывает переменная drv? Прочитайте справку по mpg, введя в консоли ?mpg, чтобы выяснить это.

4. Сделайте диаграмму рассеяния hwy относительно cyl.

5. Что произойдет, если попытаться создать диаграмму рассеяния переменной class относительно drv? Почему такого графика нельзя построить?

С эстетической точки зрения, картина тем ценнее, чем больше она заставляет нас замечать то, чего мы никогда не ожидали увидеть. На анализируемом графике обнаруживается одна группа точек (выделенная оранжевым цветом для автомобилей класса 2-Seater, «двуместные») в которой кажется, что они выходят за пределы линейного тренда. Эти автомобили имеют более высокий пробег, чем можно было бы ожидать. Как объяснить подобное? Давайте предположим, что данные автомобили являются гибридами. Один из способов проверить эту гипотезу – посмотреть на значение переменной class для каждого автомобиля. Переменная class из набора данных mpg разделяет автомобили на такие группы, как compact, midsize и SUV. Если выделяющиеся точки являются гибридными автомобилями, их следует классифицировать как компактные автомобили или, возможно, малолитражные (имейте в виду, что эти исторические данные были собраны раньше, чем стали популярными гибридные грузовики и внедорожники).

Можно добавить третью переменную, например class, к двумерному графику исходя из эстетических соображения. Эстетика – это визуальное свойство объектов в вашей диаграмме. Эстетика включает в себя такие вещи, как размер, форма или цвет точек. Изобразим точки различными способами путем изменения перечисленных значений свойств их оформления. Так как мы уже используем это слово "значение" для описания числовых данных, то будем использовать слово "уровень" для описания художественных, нечисловых, эстетических свойств. Мы меняем уровни размера, формы и цвета точки, чтобы сделать точка маленькая, треугольная или синяя.

В результате, появляется возможность передать информацию о данных с помощью сопоставление эстетики на графике с переменными в наборе данных. Например, можно сопоставить цвета точек (color) с переменной класса автомобиля (class), чтобы выявить класс каждого автомобиля. Для этого, после x = displ, y = hwy в список аргументов функции aes() через запятую необходимо добавить color = class.

Чтобы отобразить настройки форматирования в переменную, сопоставляется имя настраиваемого параметра, например цвета (color), с именем переменной внутри aes(). ggplot2 автоматически присвоит уникальный цвет для каждого уникального значения переменной, а также добавит объяснение, какие уровни каким значениям соответствуют.

Цветом показано, что многие из необычных точек охватывают двухместные автомобили. Эти автомобили не похожи на гибриды, и выглядят, по сути, как спортивные автомобили. Спортивные автомобили имеют большие двигатели, такие как внедорожники или пикапы, но небольшие кузова, такие как средние и компактные автомобили, что улучшает их экономичность. В ретроспективе, эти автомобили вряд будут гибридами, так как у них есть большие двигатели.

В приведенном выше примере сопоставлен класс с цветом, но можно сопоставить класс с размером точки точно так же. В этом случае размер каждой точки будет демонстрировать классовую принадлежность. Достаточно лишь заменить color = class на size = class, но будет получено предупреждение от интерпретатора, так как сопоставление неупорядоченной переменной (class) с упорядоченной категорией размера (size) не самая лучшая идея.

#> Предупреждение: использование параметра size для дискретной переменной не рекомендуется.

Кроме того, можно сопоставить класс с уровнем прозрачности точек (alpha), либо с их формой (shape). Для этого достаточно заменить color = class на alpha = class, либо на shape = class соответственно. Но в последнем случае ggplot2 может использовать только до шести фигур одновременно, по умолчанию все остальные группы будут отключены.

Для каждой эстетики используется aes(), чтобы связать имя эстетического объекта с переменной для отображения. Функция aes() собирает вместе каждое из эстетических отображений, используемых слоем и передает их в аргумент отображения слоя. Синтаксис выделяет полезную информацию об осях x и y: расположение объектов x и y, точки сами по себе являются эстетикой, визуальными свойствами, которые можно отобразить к переменным для демонстрации данных. После того, как настроена эстетика, ggplot2 заботится обо всём остальном. Выбирается оптимальный масштаб для использования, строится легенда, которая объясняет условные обозначения. Для координатных осей x и y функция ggplot2 не создает легенду, но будут построены осевые линии с делениями и метками. Линия оси сама по себе выступает в качестве легенды, так как она объясняет связь между расположением и координатами точек.

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

geom_point(mapping = aes(x = displ, y = hwy), color = "green")

Здесь цвет не передает информацию о переменном, он только меняет внешний вид графика. Устанавливая параметры вручную, можно регулировать общий стиль диаграмм. В частности, форму точек можно задавать порядковыми номерами, например, 0, 15 и 22 – это квадраты, разница между ними заключается в том, что некоторые залиты сплошным цветом. Полые формы (0-14) имеют границу, определяемую значением параметра color; сплошные формы (15-18) заполнены цветом указанным в color; а заполненные формы (21-24) имеют границу, совпадающую с цветом заливки.

Упражнения

1. Как сделать цвет всех точек графика синим?

2. Какие переменные в базе mpg являются категориальными? Который переменные являются непрерывными? (Подсказка: найдите в документации описание типов полей таблицы mpg). Где найти эту информацию при открытии справки по mpg?

3. Сопоставьте непрерывную переменную с цветом, размером и формой. Как такие настройки эстетики поведут себя для категориальных в отличии от непрерывных переменных?

4. Что произойдет, если сопоставить одну и ту же переменную с несколькими эстетиками?

5. Что делает эстетика stroke? В каких случаях она применима? (Подсказка: в документации найдите описание функции geom_point, для этого в консоли можно ввести ?geom_point)

6. Что произойдет, если сопоставите эстетику с чем-то другим, не являющимся именем переменной, например color = displ < 5? Как и прежде предварительно нужно будет указать значения параметров x и y.

Когда начнете выполнять код R, возможны некоторые затруднения. Не волнуйтесь, это случается со всеми. Автор книги тоже писал код R, в течение многих лет, который работает не сразу. Начните с тщательного сравнения кода, который используете, с кодом из книги. R чрезвычайно придирчив. Убедитесь, что каждая открывающаяся скобка «(«соответствует закрывающейся «)», а каждая кавычка «"» имеет парную «"». Иногда запускаете код, но ничего не происходит. Проверьте содержимое левого нижнего угла консоли: если там «+», это означает, что R ничего не делает, просто набрали не полное выражение, и он ждет завершения ввода. В этом случае можно начать с начала, нажав клавишу ESC, чтобы прервать обработку текущей команды.

Частая ошибка при создании графиков ggplot2 это расположение «+» в неправильном месте, он должен находиться в конце строки, а не в начале. Убедитесь, что не сделали этого случайно. Если всё ещё в тупике, попробуйте обратиться к справочной информации. Для получения развернутой справки о любой функции R достаточно ввести команду ?имя_функции в консоли, или выделить имя интересующей функции и нажать клавишу F1 в RStudio. Не волнуйтесь, если справки не кажется, бывает полезным вместо этого перейти к примерам и найти код, который соответствует тому, что пытаетесь сделать. Если и это не помогает, то внимательно перечитайте сообщение об ошибке. Иногда ответ содержится именно там. Просто для начинающих пользователей R, ответ может находиться в сообщении об ошибке, но не приходит его понимания. Еще одним отличным инструментом является Yandex, попробуйте поискать сообщение об ошибке в интернете, так как возможно, у кого-то была аналогичная проблема, и её решение описали на специализированных онлайн-форумах.

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

Чтобы собрать свой график из нескольких фрагментов от одной одной переменной, используйте facet_wrap(). Первым аргументом функции facet_wrap() должна быть именем структуры данных в R, которое начинается с символа «~», за которым следует имя переменной. Переменная, передаваемая в функцию facet_wrap(), должна быть дискретной. Например, следующая команда:

ggplot(data = mpg) +

geom_point(mapping = aes(x = displ, y = hwy)) +

facet_wrap(~ class, nrow = 1)

разобьет общее изображение данных известной уже нам базы на фрагментарные части, расположив их одной строкой, так как указано nrow = 1:


Чтобы расположить группы данных фрагментами на сетке, можно использовать комбинацию из двух переменных, добавив функцию facet_grid() к вызову графопостроителя. Первый аргумент в этом случае на этот раз будет содержать два имени переменных, разделенных знаком «~»:

ggplot(data = mpg) +

geom_point(mapping = aes(x = displ, y = hwy)) +

facet_grid(drv ~ cyl)

Если необходимо расположить группы только по одной переменной, в одну строку, либо в столбец, то достаточно поставить «.» вместо имени второй переменной. Так, например окончание +facet_grid(cyl ~ .) предпишет расположить графики вертикально, сгруппировав автомобили по числу цилиндров.

Упражнения

1. Что произойдет, если будете группировать данные по непрерывной переменной?

2. Что означают пустые ячейки на графике с facet_grid(drv~cyl)?

3. Каковы преимущества использования группирования данных частями по сравнению с цветовым выделением точек на одном графике? Каковы их недостатки? Как соблюдать баланс достоинств и недостатков этих подходов на больших объемах данных?

4. Прочитайте справку по ?facet_wrap. Что регулируется параметрами nrow, ncol? Какие ещё параметры управляют компоновкой? Почему функция facet_grid() не имеет аргументов nrow и ncol?

5. При использовании facet_grid() рекомендуется в столбцах располагать переменные с большим количеством уникальных значений. Почему?

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

My_table <– structure(list(Класс = c("7а", "7а", "7а", "7а", "7а", "7а", "7а", "7а", "7а",

"7а", "7б", "7б", "7б", "7б", "7б", "7б", "7б", "7б", "7б", "7б", "эталон", "отстающий"),

`Фимилия Имя` = c("Иванов Иван", "Петров Петр", "Сидоров Сидор", "Егоров Егор",

"Романов Роман", "Николаев Николай", "Григорьев Григогий", "Викторов Виктор",

"Михайлов Михаил", "Тимуриев Тимур", "Ульянова Ульяна", "Ольгина Ольга",

"Людмилова Людмила", "Дарьева Дарья", "Кристинина Кристина",

"Натальина Наталья", "Глафирова Глафира", "Янина Яна", "Иринова Ирина",

"Валентинова Валентина", "Идеальный ученик", "Другая крайность"), Тема1 = c(5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 1), Тема2 = c(2, 3, 3, 2, 2, 3, 3, 2, 2, 3, 4, 5, 5, 4, 4, 4, 5, 5, 4, 5, 5, 1), Тема3 = c(1, 2, 2, 1, 1, 2, 2, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 2, 5, 1), Тема4 = c(4, 5, 5, 4, 4, 4, 5, 5, 5, 4, 5, 5, 4, 4, 5, 5, 4, 4, 5, 4, 5, 1), `Тема 5` = c(1, 2, 2, 2, 1, 2, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 1, 2, 5, 5, 1), `№№` = c(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22)),

row.names = c(NA, -22L), class = c("tbl_df", "tbl", "data.frame"))

Представим успеваемость графически:



Насколько похожи эти две иллюстрации?

Оба графика содержат одну и ту же переменную x, один и тот же y, оба визуализируют одни и те же данные. Но их сюжет не идентичен. Каждая иллюстрация описается на свои визуальные образы для представления данных. В синтаксисе ggplot2 они используют разные геометрические объекты (geom). Geom – это геометрический объект, который применяет графопостроитель для представления данных. Например, линейные диаграммы используют линейные геометрические объекты, прямоугольные диаграммы используют геометрические объекты прямоугольной формы и так далее. Диаграммы рассеяния нарушают этот тренд, они используют точечное представление данных. Как видели выше, можно использовать разные геометрические объекты для визуализации одних и тех же данных. На левом графике используется точечная геометрия, а в правом – гладкая линия, усредняющая данные. Чтобы изменить геометрические примитивы на вашем чертеже, измените функцию geom_, которую добавляете к ggplot (). Например, чтобы воспроизвести вышеприведенные рисунки, выполните код:

# левый график

ggplot (data = My_table) +

geom_point (mapping = aes (x = `№№`, y = Тема2))

# правый график

ggplot (data = My_table) +

geom_smooth (mapping = aes (x = `№№`, y = Тема2))

Каждая функция geom в ggplot2 принимает аргумент mapping, однако не каждая настройка эстетики работает с любой функцией geom. Можно было бы установить форму точки, но нельзя установить форму линии. С другой стороны, можно установить параметр linetype, тогда geom_smooth() нарисует линии разного типа для каждого уникального значения переменной, которая сопоставлена с типом линии.

Например, функция geom_smooth() может разделить обучающихся по классам:

ggplot (data = My_table) +

geom_smooth (mapping = aes (x = `№№`, y = Тема2, linetype = Класс))

Одна линия описывает успехи в освоении Темы2 для всех одноклассников из «7а», а другая из «7б»:


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

ggplot2 обеспечивает более 40 вариантов функции geom_, пакеты расширений предоставляют ещё больше возможностей. Лучший способ получить исчерпывающий обзор, используйте справку: ?geom_smooth.

Многие варианты функции geom_, такие как geom_smooth(), например, используют один геометрический объект для отображения нескольких строк данных. Для этих функций, можно выносить эстетику группы в категориальную переменную для рисования нескольких объектов в едином стиле, так как ggplot2 нарисует отдельный объект для каждого уникального объекта значение группирующей переменной. На практике ggplot2 будет автоматическая группировка данных для этих функций всякий раз, когда сопоставляется эстетика для дискретной переменной (как было в примере с linetype). Удобно использовать эту особенность, потому что в таком случае группа эстетических параметров оказывается самой по себе, она не выносится на поле легенды или в настройки каждого объекта. К слову, показ легенды можно запретить вовсе, установкой значения параметра show.legend = FALSE, как это показано в примере кода ниже:

ggplot (data = My_table) + geom_smooth (mapping = aes (x = `№№`, y = Тема2))

ggplot (data = My_table) +

geom_smooth (mapping = aes (x = `№№`, y = Тема2, group = Класс))

ggplot (data = My_table) +

geom_smooth( mapping = aes(x = `№№`, y = Тема2, color = Класс),

show.legend = FALSE)

Чтобы изобразить несколько графиков на одном чертеже, добавьте несколько вызовов функции geom к ggplot():

ggplot (data = My_table) +

geom_point (mapping = aes (x = `№№`, y = Тема2)) +

geom_smooth (mapping = aes (x = `№№`, y = Тема2))

Это, однако, вносит некоторое дублирование в код. Представите, что хотите изменить ось y для отображения успеваемости по теме 3 вместо темы 2, нужно будет менять переменную в дух местах, при этом можно забыть про обновление в одном из них.

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

ggplot (data = My_table, mapping = aes (x = `№№`, y = Тема2)) +

geom_point() + geom_smooth()

Если же размещаете параметры mapping внутри каждой функции geom, то ggplot2 будет рассматривать их как локальные настройки для слоя. Будет использоваться параметр mapping для расширения или перезаписи глобальных настроек слоя. Это позволяет настраивать различную эстетику внутри индивидуальных слоёв:

ggplot (data = My_table, mapping = aes (x = `№№`, y = Тема2)) +

geom_point (mapping = aes (color = Класс)) + geom_smooth()


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

ggplot (data = My_table, mapping = aes (x = `№№`, y = Тема2)) +

geom_point (mapping = aes (color = Класс)) +

geom_smooth (data = My_table[My_table$Класс == "7а", ], se = FALSE)


В приведенном примере, гладкая линия охватывает только подмножество исходного набора данных. Локальный аргумент в geom_smooth() переопределяет глобальный аргумент отбора данных в ggplot().

Разберем как работает фильтрация чуть позже, на данный момент достаточно понять, что эта команда выбирает только учеников 7а класса, а опция se = FALSE отключает подсветку доверительного интервала.

Упражнения

1. Какую функцию из категории geom_ вы бы использовали для построения линейного графика? А для круговой, лепестковой диаграммы, гистограммы?

2. Что меняет опция show.legend = FALSE? Что происходит если её убрать? Как думаете, почему она использовалась ранее в примере?

3. Что делает аргумент se для функции geom_smooth ()?

4. Воссоздайте код R, необходимый для создания следующего рисунка и дайте ему соответствующую интерпретацию:


Подробнее остановимся на гистограммах, – так называемых прямоугольных диаграммах. Они кажутся простыми, но интересны тем, что открывают потенциальные закономерности в наблюдаемой статистике. Рассмотрим базовую линейчатую диаграмму, построенную следующим образом с помощью функции geom_bar(). Принимая во внимание, как Роберт Грин Ингерсолл (1833-1899) за оффлайн-школой закрепил хлёсткое определение: «Школа – это место, где шлифуют булыжники и губят алмазы», – медленно, но верно приобщаясь к принципиально иной онлайн-школе попробуем всё же научиться правильному обращению с алмазами. На диаграмме ниже будет показано общее количество обработанных алмазов – бриллиантов, хранящихся в предустановленной с пакетом ggplot2 базе данных, сгруппированных по огранке.

База данных о бриллиантах (diamonds) поставляется в комплекте ggplot2 и содержит информацию о ~54 000 дорогостоящих украшениях, включая цену, размер в каратах, цвет, прозрачность и огранку каждого из них. Несомненно, онлайн-учителю любой по карману. Диаграмма показывает, что бриллиантов с идеальной огранкой имеется гораздо больше, чем с черновой обработкой:

ggplot (data = diamonds) +

geom_bar (mapping = aes (x = cut, colour = diamonds$color))


На оси x диаграмма показывает огранку (cut) алмазов. На оси y с учетом цвета отображается их общее количество (count), но в базе данных не хранится поле count. Откуда же берется информация о количестве? Одни алгоритмы графопостроителей, например диаграммы рассеяния, формируют изображение по необработанным значениям исходного набора данных. Другие, например гистограммы, вычисляют новые вспомогательные значения при построении. Гистограммы, как частотные диаграммы, преобразуют ваши данные, осуществляют подсчеты числа записей определенного типа, будто раскладывая их по ящикам. При масштабировании последних диаграмма адаптируется к объему исходных данных, а затем строятся прямоугольники нужного размера. Вычисляется статистическая сводка выборки и после этого рисуется специально отформатированный прямоугольник. Алгоритм, используемый при вычислении новых значений для графиков, определяется параметром stat, сокращенно от «статистическое преобразование». В примере ниже показано, как это работает с geom_bar(). Вы можете узнать, какое статистическое преобразование использует та или иная функция, проверив значение по умолчанию аргумента stat. Например, в документации по функции ?geom_bar сказано, что её значение по умолчанию для аргумента stat это count, то есть geom_bar() использует функцию stat_count(), описанную на той же странице, что и geom_bar(), и если прокрутить вниз, то можно найти раздел «вычисляемые переменные», в котором сказано, что вычисляются две новые вспомогательные переменные: count и prop.

Как правило, префиксы geom_ и stat_ взаимозаменяемы. Например, можно запустить предыдущий пример с использованием stat_count() вместо geom_bar(). Это работает, потому что каждая функция категории geom_ имеет параметр stat по умолчанию, а каждая функция категории stat_ имеет двойственный параметр geom по умолчанию. Это означает, что можно используйте функции построения графиков, не беспокоясь о лежащих в их основе статистических преобразованиях данных. Есть три причины, по которым может потребоваться использовать параметр stat в явном виде:

1) Возможно, захотите переопределить используемое по умолчанию статистическое преобразование. В коде ниже, заменено значение аргумента stat в geom_bar() с count (принятого по умолчанию) на identity. Это позволяет сопоставить высоту баров с необработанным значением переменной. Когда говорят о столбцевой диаграмме, можно иметь ввиду такой тип гистограммы, в котором высота столбика уже присутствует в данных, либо предыдущую диаграмму, на которой высота генерируется с помощью подсчет строк.


Историческая справка.

Как известно, из всех систем оценивания знаний в России поныне жива 5-балльная, которая была в 1837 году официально установлена Министерством народного просвещения. Положим, что продемонстрированные воспитанницами на одном из уроков математики в Серпуховской женской гимназии результаты были занесены в следующую демонстрационную таблицу.

library(tidyverse)

demo <– tribble( ~оценка, ~количество,

"слабо", 1,

"посредственно", 1,

"достаточно", 3,

"хорошо", 2,

"отлично", 3 )

ggplot(data = demo) +

geom_bar(mapping = aes(x = оценка, y = количество), stat = "identity")


Не волнуйтесь, что не видели <– tribble раньше. Из контекста понятно назначение этих операторов, но что именно они делают в общем случае, будет подробно рассказано чуть позже.

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

library(tidyverse)

demo <– tribble( ~оценка, "слабо", "посредственно",

"достаточно", "достаточно", "достаточно",

"хорошо", "хорошо",

"отлично", "отлично", "отлично" )

ggplot (data = demo) +

geom_bar (mapping = aes (x = оценка, y = stat (prop), group = 1))


Чтобы найти полный список переменных, вычисляемых в статистике, достаточно заглянуть в раздел справки, озаглавленный как «вычисляемые переменные».

3) Возможно, захотите извлечь больше статистической информации в вашем коде. Например, если использовать функцию stat_summary(), то будет получена дополнительная описательная статистика, которую можно показать на диаграмме. Следующий фрагмент кода выберет из тестовой базы успеваемость обучающихся в 7а или 7б классах по теме 2, найдет наименьшую оценку в каждом классе, наибольшую и медианное значение. После этого найденные статистики будут отображены на диаграмме соответствующими линиями:

ggplot(data = My_table[My_table$Класс == "7а" | My_table$Класс == "7б",]) +

stat_summary(

mapping = aes(x = Класс, y = Тема2),

fun.ymin = min,

fun.ymax = max,

fun.y = median

)


На данном этапе развития проекта, пакет ggplot2 предоставляет пользователям более 20 статистик. Каждое значение параметра stat является функцией, поэтому получить справку по ним можно обычным способом, например, введя ?stat_bin в консоли.

Упражнения

1. Что такое geom по умолчанию, связанный с stat_summary()? Как переписать код из примеров, чтобы использовать функцию начинающуюся с geom_ вместо stat_?

2. Что делает функция geom_col()? Чем она отличается от geom_bar()?

3. Большинство значений параметров geom и stat парные, и почти всегда используется вместе. Ознакомьтесь с документацией и составьте список всех пар, что у них общего?

4. Какие вспомогательные переменные вычисляет функция stat_smooth()? Какие параметры контролируют её поведение?

5. В диаграмме частот из примера установлено значение group = 1. Зачем? Другими словами, что будет нарисовано без указания этого параметра?

Есть еще одна интересная опция, связанная с гистограммами. Можно раскрасить её элементы с помощью любого цвета, указав значения параметров цвета границы (color) и заливки (fill). Обратите внимание, что произойдет, если сопоставите настройки заливки с отдельной переменной: каждый цветной прямоугольник будет представлять комбинированную информацию из двух параметров.

Регулировка положения прямоугольников задается соответствующим аргументом (position). Если его не менять, то построится столбчатая диаграмма, но можете использовать один из трех других вариантов: используемый по умолчанию (identity), развернутый по горизонтали (dodge) или с заполнением прямоугольников до равной высоты (fill). Указание position = "identity" будет размещать каждый объект ровно там, где он попадает в контекст графика. Это не очень полезно в случае детализированных прямоугольников, потому что фрагменты могут перекрываться между собой внутри одного прямоугольного столбика. Чтобы увидеть это перекрытие, можно сделать заливку полупрозрачной, придав уровню прозрачности (alpha) небольшое значение, либо использовав настройку fill = NA. Такое расположение прямоугольников полезно для 2d-примитивов, в виде точек. Указание position = "fill" работает как штабелирование, оно сделает каждый набор прямоугольников одинаковой суммарной высоты. Такой подход значительно облегчает сравнение пропорций внутри групп. И наконец position = "dodge" нарисует перекрывающиеся объекты непосредственно рядом друг с другом, что облегчает сравнение индивидуальных значений.

Заключительный тип регулировки является не очень полезным для гистограмм, но может быть очень полезен для диаграмм рассеяния. Вспомните примеры из первой главы. неужели не заметили, что график отображает только 126 точек, хотя в базе данных об автомобилях записано 234 значения. Как в известном письме на Балабановскую спичечную фабрику: «Я 11 лет считаю спички у вас в коробках – их то 59, то 60, иногда 58. Вы там сумасшедшие что ли все???». Источник обозначенной проблемы в том, что значения x и y округлены. В результате, многие точки появляясь на сетке перекрывают друг друга. Эта проблема известна как «overplotting». Такое расположение делает график трудным для понимания, когда на нём находится много данных. Распределены ли точки данных поровну на всем графике, или есть комбинация координат x и y, которая содержит 109 значений одновременно? Проблемы можно избежать, переключив регулировку положения в режим дрожания (jitter). Настройка position = "jitter" добавляет небольшое количество случайных шумов в каждую точку. Это распространяется на всю поверхность и поэтому не окажется двух точек, которые, вероятно, получат одинаковое количество случайных шумов. Добавление случайности кажется странным способом улучшения изображения, но несмотря на то, что график получится менее точным на малом масштабе, в больших масштабах график становится более иллюстративным. Поскольку это такая полезная опция, в ggplot2 внесена отдельная краткая форма записи выражения geom_point(position = "jitter"), вместо него лучше использовать geom_jitter().

Чтобы узнать больше о регулировке положения, загляните в раздел справки, посвященный каждой из перечисленных настроек.

Упражнения

1. Какие параметры функции geom_jitter() регулируют количество дрожаний?

2. Примените geom_jitter() и geom_count(), сравните полученные результаты.

3. Какая настройка положения используется в функции geom_boxplot() по умолчанию? Создайте на её основе визуализацию своего набора данных.

Заключительной частью настоящей главы рассмотрим настройку систем координат для построения графиков. Система координат, пожалуй, имеет самый сложный функционал в ggplot2. Естественно, по умолчанию используется прямоугольная декартова система координат, в которой значения x и y позволяют однозначно определить местоположение каждой точки. Но есть и другие системы координат, которые иногда полезны. Функция coord_flip() меняет местами оси x и y. Это пригодится, если хотите нарисовать горизонтальные боковые диаграммы,а также полезно для длинных графиков, которые трудно подгонять без перекрытия по оси x.

# левый график

ggplot(data = My_table[My_table$Класс == "7а" | My_table$Класс == "7б",],

mapping = aes(x = Класс, y = Тема2)) +

geom_boxplot()

# правый график

ggplot(data = My_table[My_table$Класс == "7а" | My_table$Класс == "7б",],

mapping = aes(x = Класс, y = Тема2)) +

geom_boxplot() +

coord_flip()



Функция coord_quickmap() устанавливает соотношение сторон правильным для карт. Это очень важно, если строите планы карт местности с помощью ggplot2. Например:

1) Установите пакет карт, если не использовали его ранее.

install.packages("maps")

2) Подключите соответствующую библиотеку.

library(maps)

3) Заполните переменную картографическими данными.

ru <– map_data("world")

4) Теперь можно получить изображение карты в корректном масштабе

ggplot(ru, aes(long, lat, group = group)) +

geom_polygon(alpha=1/5, fill = "green", color = "black") +

coord_quickmap()


Функция coord_polar() переключает графопостроитель в режим полярных координат. Полярный координаты позволяют визуализировать интересную связь между линейчатой и круговой диаграммами. Напоследок вернёмся к тому, с чего начинали, – алмазам и их популярности в зависимости от качества. В следующем примере переменная bar заполняется вызовом процедуры формирования блоков данных для изображения. Далее, диаграмма транспонируется, тем самым приводя к линейчатому виду, и изображается в полярной системе координат отдельно:

bar <– ggplot(data = diamonds) +

geom_bar(

mapping = aes(x = cut, fill = cut),

show.legend = FALSE,

width = 1

) +

theme(aspect.ratio = 1) +

labs(x = NULL, y = NULL)

bar + coord_flip()

bar + coord_polar()



Упражнения

1. Преобразуйте линейчатую диаграмму с накоплением в круговую диаграмму с помощью coord_polar().

2. Где и как используется функция labs()? Ознакомьтесь с документацией.

3. В чем разница между coord_quickmap() и coord_map()?

4. Почему важно применение coord_fixed()? Что делает функция geom_abline()?

5. Выполните аналогично разобранной визуализацию успеваемости учеников своего класса.

Выше было показано как создавать диаграммы рассеяния, гистограммы и прямоугольные-диаграммы. После закрепления на практике сформировался навык, легко применимый к освоению диаграмм ggplot2 любого типа. Чтобы закрепить изученное, добавим настройки положения, статистическую обработку, настройки системы координат и разбиение данных к исходному шаблону кода:

ggplot(data = <данные>) +

<geom_основная функция графопостроителя>(

mapping = aes(<сопоставления с координатными осями и эстетикой>),

stat = <сбор дополнительной статистики>,

position = <позиция фрагментов диаграммы>

) +

<настройка координатной системы> +

<функция группирования данных>

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

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

Чтобы понять, как это работает, вспомните, как строился простейший график с самого начала: фиксировался набор данных, затем выполнялась статистическая обработка для извлечения вспомогательной информации. Далее, выбирался способ представления каждого исходного значения и новых данных. При этом настраивались эстетические свойства геометрических примитивов, чтобы сопоставление значений каждой переменной с положением, цветом или формой объекта несло определенную смысловую нагрузку. Затем выбирали систему координат, чтобы в ней наглядно разместить полученное изображение, это само по себе тоже несет определенную эстетику, сопоставляя значения переменных с x и y. В результате получался график, но опционально ещё настраивалось местоположение объектов внутри системы координат (корректировка положения) и разбиение графика на подграфики (фасетирование). Также можно было улучшить изображение, добавив один или несколько дополнительных слоёв, на каждом из которых использовался свой набор данных, функция графопостроителя, набор сопоставлений, собиралась дополнительная статистика и регулировалось положение.

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

Перейдем ко второй части, анонсированной в названии раздела. Да, визуализация является важным инструментом понимания, но считается большой удачей получить исходные данные сразу в пригодном для визуализации формате. Часто приходится создавать новые переменные или сводные таблицы, переименовывать переменные или изменять порядок следования наблюдений, чтобы сделать данные немного проще для повышения наглядности их визуализации. Рассмотрим, как сделать все это и многое другое, как преобразовывать данные с помощью пакет dplyr на примере обширного набора данных о рейсах, вылетающих из Нью-Йорка.

Историческая справка. На прилагаемом фото запечатлён трансарктический самолёт АНТ-25 в ангаре аэропорта Флойд Беннет, февраль 1939 года, ознаменовавший успешное участие СССР в Нью-Йоркской выставке 1939-1940 годов. Нью-Йорк, Бруклин.

Сосредоточимся на том, как использовать пакет dplyr, – один из базовых инструментов tidyverse. Проиллюстрируем ключевые идеи, используя данные из базы nycflights13, и пакета ggplot2, чтобы визуализировать эти данные.

library(nycflights13)

library(tidyverse)

Обратите внимание на сообщение о возможных конфликтах, которое выводится при загрузке tidyverse, так как dplyr перезаписывает некоторые функции R. Если хотите использовать эти функции после загрузки dplyr, то нужно будет вводить их полные имена через два двоеточия, например, stats::filter().

Чтобы изучить основные способы работы с данными из dplyr, будем пользоваться базой данных nycflights13::flights, она содержит информацию по всем 336 776 рейсам, вылетевшим из Нью-Йорка. Данные поступают из Бюро статистики транспорта США, и вы можете с ними ознакомиться в любое время, просто введя в консоли:

flights

Заметим, что эта база данных при выводе в консоль отличается от вывода из других баз данных, которые применяли ранее. Показаны лишь первые несколько строк и столбцы, которые поместились на экране. Чтобы просмотреть весь набор данных, необходимо запустить:

view(flights)

Откроется таблица средствами просмотра RStudio, в слегка упрощенном виде, чтобы легче было применять инструментарий tidyverse. На данный момент не нужно беспокоиться о нюансах, позже вернемся к табличному представлению данных в соответствующей главе. Ряд из нескольких буквенных сокращения под названиями столбцов описывает тип каждой переменной: int означает целые числа; dbl означает действительные числа; chr означает символьные строки; dttm означает дату-время (дата + время). Существуют и другие распространенные типы переменных, они не используются в данном наборе, но будут рассмотрены отдельно: lgl означает логические значения, которые содержат только TRUE или FALSE; fctr означает факторы, которые R использует для представления категориальных переменных с фиксированными возможными значениями; date означает данные.

Следующие пять ключевых функций dplyr позволяют решить подавляющее большинство задач обработки данных: filter() отфильтрует наблюдения по заданным условиям; arrange() меняет порядок строк; select() выберет переменные по их именам; mutate() создаёт новые переменных со свойствами существующих переменных; summary() сворачивает множество значений до одного. Перечисленные функции можно использовать совместно с group_by(), которая изменяет область действия каждой функции со всего набора данных на определенные группы. Собственно перечисленные шесть функции и предоставляют собой команды языка обработки данных.

Все функции работают по общей схеме:

1) Первый аргумент – фрагмент данных.

2) Последующие аргументы описывают, что нужно делать с выбранными данными, используя имена переменных без кавычек, либо в одиночных апострофах кавычках «'», если имена содержат пробел « ».

3) Результатом является новый фрагмент данных.

Перечисленные свойства делают легко реализуемой последовательность из нескольких простых шагов к достижению желаемого результата. Разберем на примерах, как это работает. filter() позволяет выбирать подмножество наблюдений на основе определенных условий. Первый аргумент содержит имя базы данных. Второй и последующие аргументы являются выражениями, фильтрующими данные. Например, выберем все рейсы на 5 мая следующей командой:

filter(flights, month == 5, day == 5)

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

may5 <– filter(flights, month == 5, day == 5)

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

(may5 <– filter(flights, month == 5, day == 5))

Чтобы эффективно использовать фильтрацию, нужно знать, как выбрать наблюдения, используя операторы сравнения. R предоставляет стандартный набор операторов: > (больше), >= (больше или равно), < (меньше), <= (меньше или равно), != (не равны), == (равны). Начинающие пользователи R зачастую ставят = вместо == при проверке равенства. Если допустить такое, то возникнет предупреждение об ошибке. Есть еще одна распространенная проблема, с которой сталкиваются при использовании ==, это числа с плавающей запятой. Поистине альтернативная арифметика:

sqrt (4) ^ 2 == 4

# > [1] TRUE

sqrt (5) ^ 2 == 5

# > [1] FALSE

1 / 50 * 50 == 1

# > [1] TRUE

1 / 49 * 49 == 1

# > [1] FALSE

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

near(sqrt(5) ^ 2, 5)

# > [1] TRUE

Несколько аргументов функции filter() перечисленные через запятую равносильны объединению условий союзом «и», при этом, каждое выражение должно оказаться истинным, чтобы из входных данных соответствующая запись была сохранена в выходные данные. Для остальных логических связок можно использовать булевы операторы: & это «и», | это «или», ! это отрицание «не», xor(xy) это исключающее или с аргументами xy.

Следующий код находит все рейсы, которые вылетели в феврале или марте:

filter(flights, month == 2 | month == 3)

Если попытаться ввести команду буквально

filter(flights, month == (2|3))

то вместо желаемого будут найдены все месяцы равные результату булевой операции 2|3, значение которой обращается в TRUE. В числовом контексте TRUE становится равным единице 1, поэтому будут найдены все январские вылеты, что вовсе не соответствует задуманному.

Полезным клавиатурным сокращением для решения обозначенной проблемы является x %in% y. Это позволит выбрать каждую строку, где x является одним из значений в y. Можно было бы использовать следующую альтернативу для кода выше:

filter(flights, month %in% c(2, 3))

Иногда можно упростить сложное выражение вспомнив законы де Моргана из курса математической логики: !(x & y) == !х | !y, и !(x | y) == !x & !y. Например, если нужно найти рейсы, которые не задерживались (по прилету или отправлению) более чем на час, можно воспользоваться любым из следующих фильтров:

filter(flights,!(arr_delay > 60 | dep_delay > 60))

filter(flights, arr_delay <= 60, dep_delay <= 60)

Кроме & и |, в R есть && и ||, но не используйте их сейчас, позже узнаете, при каких условиях уместно их применение.

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

NA > 1

#> [1] NA

2 == NA

#> [1] NA

NA + 3

#> [1] NA

NA / 4

#> [1] NA

Самым алогичным результатом может показаться следующий:

NA == NA

#> [1] NA

Но его легко понять в конкретном контексте: совпадает ли содержимое двух ящиков, внутри которых неизвестно что? Мы не знаем! Если хотите определить, отсутствует ли значение конкретной переменной, можно воспользоваться функцией is.na(), в качестве аргумента задав интересующее имя. Функция filter() отбирает только те строки, для проверяемые условия обращаются в TRUE, при этом исключаются как значения FALSE, так и NA. Если хотите сохранить пропущенные значения, то запрашивайте их в явном виде:

filter (flights, is.na(month) | month > 1)

Упражнения

1. Найти все рейсы, которые: имели задержку прибытия на два и более часа; прилетели в Хьюстон; управлялись компанией Delta; улетели летом; прибыл с опозданием более чем на два часа; задержались они как минимум на час, но наверстали более 30 минут в полете; отбыли между полуночью и 6 утра (включительно).

2. Функция between() из пакета dplyr тоже полезна для фильтрации. А что она делает? Можно ли использовать её для упрощения кода, необходимого для получения ответов в предыдущем задании?

3. Сколько рейсов имеет отсутствующее значение dep_time? Какие еще переменные у них отсутствуют? Что могут представлять собой эти записи в базе данных?

4. Почему значение NA^0 определено, NA / TRUE не определено, а FALSE & NA определено? Можете ли сформулировать общее правило, охватывающее и случай NA * 0?

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

arrange(flights, year, month, day)

Используя desc() можно переданный в аргументе столбец упорядочить по убыванию значений:

arrange(flights, desc(dep_delay))

Пропущенные значения (NA) всегда оказываются в конце сортировки.

Упражнения

1. Как использовать функцию arrange() для переноса всех пропущенных значений в начало списка? (Подсказка: применимо is.na()).

2. Сортировка рейсов позволяет найти самые задерживаемые рейсы. Найдите рейсы, которые вылетали пунктуальнее всех.

3. Отсортируйте рейсы так, чтобы найти самые скоростные перелёты.

4. Какие рейсы летали дальше всех? Какой маршрут был самым коротким?

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

# поимённый выбор столбцов «месяц», «день»

select(flights, month, day)

# выбор всех столбцов между «месяц» и «день» включительно

select(flights, month:day)

# выбор всех столбцов, кроме тех, что лежат между «месяц» и «день» включительно

select(flights, -(month:day))

Существуют вспомогательные функции, которые уместно вызывать внутри select(): функция starts_with("абв") выбирает имена столбцов начинающихся с «абв»; функция ends_with("эюя") выбирает имена столбцов заканчивающиеся на «эюя»; функция contains("клм") выбирает имена содержаие подстроку «клм»; функция matches("(.)\\1") выбирает переменные, имена которых соответствуют заданному регулярному выражению, конкретно в данном случае магическим образом выбираются переменные, содержащие повторяющиеся символы, подробнее о регулярных выражениях в строках расскажем в соответствующей главе; вызов num_range("m", 2:4) соответствует набору m2, m3, m4. Всегда можно заглянуть в ?select для получения более подробной информации.

А еще, select() можно использовать для переименования переменных, но это редко когда бывает полезным, так как отбрасывает не упомянутые явно переменные. Вместо этого для переименования используется функция rename(), который является вариантом select(), но сохраняет все переменные, которые не указаны явно:

rename(flights, год = year)

Другой вариант использования select(), совместно со вспомогательной функцией everything(), бывает необходим если есть несколько переменных, которые нужно переместить в начало базы данных. Например, месяц (month) и день (day) вылета будут показаны первыми при выводе данных из таблицы, содержащей информацию обо всех авиаперелётах (flights) по команде:

select (flights, month, day, everything ())

Аналогично запланированную дату и время полёта (time_hour), и время, проведенное в воздухе, выраженное в минутах (air_time) можно перекинуть в начало.

Упражнения

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

2. Что произойдет, если имя одной переменной использовать несколько раз при вызове функции select()?

3. Что делает функция one_of()? Насколько полезно её применение в сочетании с вектором c("month", "day")?

4. Является ли результатом выполнения следующего кода неожиданным? Что вспомогательные функции выбора переменных в нём возвращают по умолчанию? Как изменить их значение по умолчанию?

select(flights, -contains("TIME"))

Помимо выборки существующих столбцов, полей таблицы базы данных, переменных, бывает необходимым добавление новых столбцов, которые хранят значения, являющиеся функциями от существующих. Это выполняется путём обращения к функции mutate(), которая всегда добавляет новые столбцы в конце имеющегося набор данных. Поэтому создадим более узкий набор данных, чтобы видеть новые переменные. Помните, что в RStudio самый простой способ увидеть все столбцы таблицы это вызов функции view(). Создадим укороченный_вариант_таблицы, содержащий все поля между «год» (year) и «день» (day) включительно, плюс поля, содержащие информацию о задержках (заканчивающиеся на delay), покрытом расстоянии (distance) и времени полёта (air_time) в минутах:

укороченный_вариант_таблицы <– select(flights,

year:day,

ends_with("delay"),

distance,

air_time)

Теперь добавим вычисляемые поля с информацией об опоздании, – задержке вылета минус задержка прилета, в минутах, и о средней скорости полёта. Обратите внимание, что можно ссылаться на столбцы, которые уже созданы. Если вдруг захотите сохранить только новые переменные, то используйте transmute() вместо mutate():

mutate(укороченный_вариант_таблицы,

опоздание = dep_delay – arr_delay,

скорость = distance / air_time * 60,

часы_полёта= air_time / 60,

опоздание_в_каждом_часе = опоздание / часы_полёта )

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

Арифметические операторы: +, -, *, /, ^. Все они работают с векторами используя так называемые «правила рециркуляции», заключающиеся в том, что если один параметр короче другого, то произойдет автоматическое удлинение до равного размера путём клонирования короткого вектора достаточное количество раз. Это полезно, когда один из аргументов – число. В примере выше так были вычислены часы_полёта делением вектора на скаляр, а скорость умножением вектора на скаляр. Арифметические операторы также полезны в связке с агрегирующими функциями, о которых узнаете позже. Например, x / sum(x) вычисляет долю от общей суммы значений переменной, а y – mean(y) вычисляет отклонение величины от среднего.

Модулярная арифметика: %/% (целочисленное деление) и %% (остаток), здесь x == y * (x %/% y) + (x %% y). Модулярная арифметика очень удобный инструмент, потому что позволяет представлять большие целые числа сравнительно небольшими остатками. Например, в наборе данных flights можно выделить полные часы и оставшиеся минуты из общей продолжительности полёта, представленной в формате ЧЧММ или ЧММ (dep_time). Тогда вместо хранения и выполнения различных операций над одним большим числом, можно будет хранить и выполнять операции над двумя маленькими:

transmute(flights,

dep_time,

час = dep_time %/% 100,

минута = dep_time %% 100)

Логарифмические функции: log(), log2(), log10(), являются невероятно полезным преобразованием при работе с данными, диапазон которых охватывает несколько порядков наблюдаемой величины. Они также преобразуют мультипликативные операции в аддитивные, к этой особенности вернемся в разделе, посвященном моделированию. При прочих равных условиях, рекомендуется использовать функцию log2() так как её значения легко интерпретировать: разница в 1 на логарифмической линейке соответствует удвоению в исходном масштабе, а разница в -1 соответствует делению пополам.

Смещения: вперёд lead() и назад lag() позволяют просматривать последующие и предыдущие значения списка. Бывают необходимо вычислить приращение аргумента, например, х – lag(x), или проверить неизменность его значений, выражением x != lag(x). Смещения особенно полезны в сочетании с group_by(), но не будем забегать вперёд.

Накопительные и скользящие агрегаторы: R предоставляет функции для вычисления накапливаемой суммы cumsum(), произведения cumprod(), минимума cummin() и максимума cummax() элементов списка; кроме того, dplyr имеет функцию cummean() для вычисления среднего значения. Если нужны скользящие агрегаторы, когда сумма вычисляется по скользящему окну, то обращаются к функционалу пакета RcppRoll.

Логические сравнения: < (меньше), <= (не больше), > (больше), >= (не меньше), != (не равны), и == (равны), о них мы узнали ранее. Напомню лишь, если осуществляется сложная последовательность логических операций, то настоятельно рекомендуется сохранять промежуточные значения в отдельных вспомогательных переменных, чтобы проверить значение выражения на каждом шаге вычислений.

Ранжирование: объединяет в себе целый ряд функций, начиная с min_rank(), которая осуществляет вычисление простого порядкового номера (например, 1-й, 2-й, 3-й, 4-й). По умолчанию присваиваются меньшие номера меньшим значениям, но можно воспользоваться функцией desc() для обращения порядка значений аргумента, чтобы придать наибольшие порядковым номера наименьшим значениям элементов исходного списка. Если min_rank() не делает то, что нужно, загляните в описание функций ранжирования на страницах справки для получения более подробной информации.

Упражнения

1. На переменные, хранящие длительность перелёта, удобно смотреть, но трудно выполнять операции над ними, так как они не совсем порядковые числа, за 159 (которое символизирует 1 час, 59 минут) идет сразу 200 (2 часа ровно). Конвертируйте их в более удобное представление, чтобы хранилось общее количество минут начиная с полуночи.

2. Сравните значения часы_полёта с опоздание_в_каждом_часе. Что надеялись увидеть и что увидели? Что нужно сделать, чтобы исправить ошибку?

3. Найдите 10 самых задерживаемых рейсов, используя функции ранжирования. Как это связано? Внимательно прочитайте текст документация по min_rank().

4. Что возвращает 2:4 – 5:8 и почему?

5. Какие тригонометрические функции определены в R?

Последняя ключевая функция summary(), – она собирает сводную статистику по переменным при помощи вспомогательных функций. Например, среднее значение (mean) переменной dep_delay посчитается в переменную средняя_задержка_рейсов:

summarise(flights, средняя_задержка_рейсов = mean(dep_delay, na.rm = TRUE))

Ниже объясним подробно, что значит последний параметр na.rm = TRUE. Функция summary() не очень полезна, если используется без вспомогательной функции group_by(), которая переключает разбивает анализ всего набора данных на отдельные группы. Когда вызывается функция из пакета dplyr на сгруппированных данных, автоматически подключается group_by() для распараллеливания вычислений в целях повышения производительности и дробления информации. Например, если применить точно такой же вызов как в предыдущем примере, но для сгруппированных по дате записях, то на выходе получится средняя задержка по дням:

сгруппированные_по_дням <– group_by(flights, year, month, day)

summarise(сгруппированные_по_дням,

средняя_задержка_рейсов_по_датам = mean(dep_delay, na.rm = TRUE))

Вызов функций group_by() совместно с summary() чаще всего используется при работе в пакете dplyr для получения статистических отчетов по группам. Но прежде, чем погрузиться в детали, дополнительно изложим одну техническую идею, касающуюся обработки информации путём её направления по специальным каналам. Представьте, что хотим исследовать закономерность между расстоянием и средней задержкой рейса для каждого пункта назначения. Опираясь на имеющиеся знания о возможностях dplyr, для этого достаточно использовать такой код:

группы_рейсов_по_месту_назначения <– group_by(flights, dest)

задержки <– summarise(группы_рейсов_по_месту_назначения,

опозданий = n(), средняя_длина_маршрута = mean(distance, na.rm = TRUE),

средняя_задержка = mean(arr_delay, na.rm = TRUE))

Оставим в выборке рейсы имеющие более сотни регулярных опозданий и, например, не на московских направлениях:

задержки <– filter(задержки, опозданий > 100, dest != "MSK")

Визуализируем оставшиеся записи:

ggplot(data = задержки, mapping =

aes(x = средняя_длина_маршрута, y = средняя_задержка)) +

geom_point(aes(size = опозданий), alpha = 1/5) +

geom_smooth(se = FALSE)


Похоже, что задержки растут с увеличением расстояния до ~750 миль, а затем сокращаются. Неужели, когда рейсы становятся длиннее, появляется возможность компенсировать опоздание находясь в полёте?

Предварительно было пройдено три вспомогательных этапа подготовки данных:

1. Сгруппированы рейсы по направлениям.

2. В каждой из групп усреднены расстояния, длительность задержки и вычислено количество опоздавших рейсов.

3. Отфильтрованы шумы и аэропорт, который не подчиняется законам логики.

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

задержки <– flights %>%

group_by(dest) %>%

summarise(

опозданий = n(),

средняя_длина_маршрута = mean(distance, na.rm = TRUE),

средняя_задержка = mean(arr_delay, na.rm = TRUE) ) %>%

filter(опозданий > 100, dest != " MSK ")

Такой синтаксис фокусирует внимание исследователя на выполняемых преобразованиях, а не на том, что получается на каждом из вспомогательных этапов, и делает код более читаемым. Это звучит как ряд предписаний: сгруппируй, после этого подведи итоги, после этого отфильтруй полученное. Как подсказывает здравый смысл, можно читать %>% в коде как «после этого». По сути же, формируется информационный канал последовательной передачи данных на обработку от одной функции через другую к третьей. Технически, x %>% f(y) превращается в f(x, y), а x %>% f(y) %>% g(z) превращается в композицию функций g(f(x, y), z) и так далее, что позволяет использовать канал для объединения нескольких операций в одну, которую можно читать слева направо, сверху вниз. Будем часто пользоваться каналами, так как это значительно упрощает читаемость кода, разберём их более подробно в соответствующем разделе.

Работа с каналами это одна из ключевых особенностей tidyverse. Единственным исключением является ggplot2, так как библиотека была написано до появления такой возможности в R. К сожалению, являющаяся наследником ggplot2 библиотека ggvis хотя и поддерживает работу с каналами, но пока еще не в полной мере.

Внимательный читатель наверняка задавался вопросом о смысле и предназначении аргумента na.rm. Что будет, если его не писать? Получим много пропущенных значений! Дело в том, что агрегационные функции подчиняются обычному правилу пропущенных значений: если на входе есть какое-либо отсутствующее значение, то выход будет отсутствующим значением. К счастью, все функции агрегации имеют аргумент na.rm, который удаляет пропущенные значения перед началом вычислений. В том случае, где пропущенные значения представляют отмененные рейсы, мы также могли бы решить эту проблему, сначала удалив отмененные рейсы. Сохраним этот набор данных, чтобы использовать его повторно в нескольких следующих нескольких примерах:

неотмененные <– flights %>%

filter(!is.na(dep_delay), !is.na(arr_delay))

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

неотмененные %>%

group_by(year, month, day) %>%

summarise(средняя_задержка = mean(dep_delay))

Всякий раз, когда осуществляется подобная агрегация, правилом хорошего тона является добавление счетчика числа учтенных значений функцией n(), либо путём подсчета используемых непустых значений командой sum(!is.na(x)). Таким способом можно удостовериться, что не делается поспешных выводов на основании выборок очень малых объемов. Например, сгруппировав рейсы по бортовому номеру, хранящемуся в переменной tailnum из таблицы неотмененных рейсов, на графике посмотрим каковы самые высокие задержки в среднем на борт:

задержки <– неотмененные %>%

group_by(tailnum) %>%

summarise(

средняя_задержка = mean(arr_delay)

)

ggplot(data = задержки, mapping = aes(x = средняя_задержка)) +

geom_freqpoly(binwidth = 5)


Неужели много самолетов со средней задержкой рейса более 5 часов (300+ минут)? На самом деле не всё так печально, как могло показаться при поверхностном ознакомлении. Можно получить более глубокое представление об опозданиях, если нарисовать диаграмму рассеяния количества рейсов относительно средней задержки:

задержки <– неотмененные %>%

group_by(tailnum) %>%

summarise(

средняя_задержка = mean(arr_delay, na.rm = TRUE),

количество_выполненных_рейсов = n()

)

ggplot(data = задержки, mapping = aes(x = количество_выполненных_рейсов,

y = средняя_задержка)) +

geom_point(alpha = 1/15)


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

Именно поэтому, когда решаете аналогичные задачи, полезно отфильтровывать группы с наименьшим количеством наблюдений, тогда можно будет увидеть общие закономерности и уменьшить выбросы значений на малых группах. На примере следующего кода будет демонстрирован удобный шаблон интеграции ggplot2 с каналами в dplyr. Немного странным может показаться смешение стилей %>% и +, дело привычки, со временем это станет естественным. Отфильтруем на предыдущем графике экспериментальные самолёты с малым количеством вылетов, не превышающим 33:

задержки %>% filter(количество_выполненных_рейсов > 33) %>%

ggplot(mapping = aes(x = количество_выполненных_рейсов,

y = средняя_задержка)) +

geom_point(alpha = 1/15)


Полезным сочетанием клавиш RStudio является Ctrl + Shift + P, для повторной отправки ранее отправленного фрагмента из редактора в консоль. Это очень удобно, когда экспериментируете с граничным значением 33 в приведенном выше примере: отправляете весь блок в консоль нажатием Ctrl + Enter, а затем изменяете значение границ фильтрации на новое и нажимаете Ctrl + Shift + P, чтобы повторно отправить весь блок в консоль.

Есть еще одна хрестоматийная иллюстрация к применению изложенного метода. Рассмотрим среднюю эффективность бейсбольных игроков относительно количества подач, когда они находятся на базе. Воспользуемся данными из пакета Lahman для вычисления среднего показателя эффективности (количество попаданий / количество попыток) каждого ведущего игрока бейсбольной лиги.

Историческая справка. Не сказать чтобы бейсбол был нашей национальной забавой, но многие в России знают «сколько пинчеров на базе», и именно россиянин, уроженец Нижнего Тагила, Виктор Константинович Старухин стал первым питчером, кто одержал 300 побед в японской бейсбольной лиге, являясь одним из лучших бейсболистов мира в 20 веке.

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

1. Как было в примере с самолётами, вариативность показателей уменьшается при увеличении количества наблюдений.

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

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

удары <– as_tibble(Lahman::Batting)

эффективность <– удары %>%

group_by(playerID) %>%

summarise(

результативность = sum(H, na.rm = TRUE) / sum(AB, na.rm = TRUE),

возможность = sum(AB, na.rm = TRUE)

)

эффективность %>%

filter(возможность > 100) %>%

ggplot(mapping = aes(x = возможность, y = результативность)) +

geom_point() +

geom_smooth(se = FALSE)

Функция geom_smooth() здесь формирует график методом обобщенных аддитивных моделей с интегрированной оценкой гладкости (method = "gam") рассчитывая значения по формуле formula = y ~ s(x, bs = "cs"), так как имеется более 1 000 наблюдений.


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

эффективность %>%

arrange(desc(результативность))

Можно найти хорошее объяснение этого парадокса в пословице «новичкам везёт». Используя простые инструменты, подсчет количества одинаковых значений, их суммирование, можно долго искать любопытные закономерности, но R предоставляет и много других полезных функций для генерации статистических отчетов:

Выше использовалась функция, вычисляющая среднее значение mean(x), но вычисляющая медианное значение функция median(x) тоже бывает полезна. Ведь среднее как 36.6° по больнице, а медиана – это величина, относительно которой 50% значений x находится выше, и 50% находится ниже, что гораздо информативнее. Иногда полезно комбинировать подобные функции с логическим условием. Мы еще не говорили о таких вещах как подмножество значений, этому можно посвятить целый раздел, пока лишь приведем наглядный пример, на тех же неотмененных авиарейсах, сгруппированных по дате вылета.

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

неотмененные %>% group_by(year, month, day) %>%

summarise(

средняя_задержка = mean(arr_delay),

средняя_положительная_задержка = mean(arr_delay[arr_delay > 0])

)

Особый интерес вызывают функции вычисления стандартного отклонения sd(x), меры разброса наблюдаемой величины, вычисления интерквартильного размаха IQR(x) и вычисления медианы абсолютного отклонения mad(x), которые являются надежными эквивалентами друг друга и могут быть полезны, если у данных есть выбросы. Любопытно, почему расстояние до одних пунктов назначения варьируются сильнее, чем до других, являя собой не иначе как чудеса телепортации:

неотмененные %>% group_by(dest) %>%

summarise(среднеквадратическое_отклонение_дистанции = sd(distance)) %>%

arrange(desc(среднеквадратическое_отклонение_дистанции))

Функции поиска минимального значение min(x), первого квантиля quantile(x, 0.25), вычисления максимума max(x), неизменные спутники при построении ранжирования. Квантили являются обобщением медианы. Так, например, quantile(x, 0.25) найдет значение x, которое больше чем 25% значений из всех возможных значений анализируемой переменной, и меньше чем остальные 75%.

Найдем время отправления первого и последнего рейса каждый день:

неотмененные %>% group_by (year, month, day) %>%

summarise( первый_рейс = min (dep_time),

последний_рейс = max (dep_time) )

Измерение позиции указателя на элементах списка осуществляется функциями first(x) для выбора первого элемента переменной x, nth(x, n) для выбора n-ного, last(x) для выбора последнего. Они работают аналогично адресации массивов в нотации x[1], x[n] и x[length(x)], но возвращают значение аргумента default, если запрошенная позиция не существует. Например, не увенчается успехом попытка получить значение такого элемента, как неотмененные$dep_time[length(неотмененные$dep_time)+1], вернув NA, неопределенное значение переменной, но при этом на выходе даст «Бинго!» вызов nth(неотмененные$dep_time,length(неотмененные$dep_time)+1, default = "Бинго!").

Следующая функция range() дополняет фильтрацию. Приведём пример, в котором сначала все записи группируются по датам и ранжируются, а потом фильтрация оставляет в строках значения, имеющие наибольший и наименьший из рангов в группе. Для сравнения, вызов функции range(неотмененные$dep_time) вернёт список, состоящий из наибольшего и наименьшего значений переменной dep_time:

неотмененные %>% group_by (year, month, day) %>%

mutate(ранжирование = min_rank(desc(dep_time))) %>%

filter(ранжирование %in% range(ранжирование) )

Ранее в вычислениях уже использовалась функция n(), которая вызывается без аргументов, и возвращает размер текущей группы. Чтобы посчитать количество непустых значений в группе х, используется конструкция sum(!is.na(x)), а чтобы подсчитать число различных (уникальных) значений вызывается n_distinct(x). Например, вычислим, какие направления имеют наибольшее количество перевозчиков:

неотмененные %>% group_by(dest) %>%

summarise(перевозчики= n_distinct(carrier)) %>%

arrange(desc(перевозчики))

Подсчеты значений настолько востребованы, что в пакете dplyr выделена отдельная функция count() для этого. Подсчитаем число повторений каждого направления, хранящихся в переменной dest таблицы неотмененных авиарейсов:

неотмененные %>% count(dest)

При необходимости указывается параметр веса каждого слагаемого (wt). Например, это можно использовать для подсчета общей суммы количества миль, которые пролетел самолет с фиксированным бортовым номером, взятым из поля talinum в базе неотмененных рейсов:

неотмененные %>% count(tailnum, wt = distance)

Подсчет числа значений удовлетворяющих логическому выражению, sum(x > 777), или их среднее количество, mean(y == 0), предполагает, что в связке с числовыми функциями TRUE преобразуется в 1, а FALSE в 0. Это делает функции sum() и mean() очень востребованными: sum(x) возвращает количество значений TRUE для аргумента x, а mean(x) возвращает их долю. Вычислим, сколько неотмененных рейсов было до 6 утра по данным за каждые сутки, это обычно указывает на задержку с предыдущего дня:

неотмененные %>% group_by(year, month, day) %>%

summarise(утренние_рейсы = sum(dep_time < 600))

Какова доля неотмененных рейсов, задержавшихся более часа:

неотмененные %>% group_by(year, month, day) %>%

summarise(часовая_задержка = mean(arr_delay >= 61))

При группировании по нескольким переменным, каждая новая сводка выносится на новый уровень группировки. Это облегчает восприятие и постепенно упрощает данные:

группы_по_дням <– group_by(flights, year, month, day)

(сводка_по_дням <– summarise(группы_по_дням, полётов = n()))

(сводка_по_месяцам <– summarise(сводка_по_дням, полётов = sum(полётов)))

(сводка_за_год <– summarise(сводка_по_месяцам, полётов = sum(полётов)))

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

группы_по_дням %>%

ungroup() %>% # разгруппируем данные обратно

summarise(полётов = n()) # подсчитаем все полёты

Упражнения

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

а) в 50% случаев вылет осуществляется на 15 минут раньше запланированного, и в 50% рейс задерживается на 15 минут.

б) рейс всегда опаздывает на 10 минут.

в) 50% рейсов вылетает на 30 минут раньше, и 50% на 30 минут опаздывает.

г) в 99% случаев рейс выполняется точно по графику, а в оставшемся 1% происходит опоздание на 2 часа.

Что более важно для пассажира, задержка прибытия или задержка вылета, а для работы аэропортов?

2. Придумайте альтернативный способ решения той же задачи, что и в примерах неотмененные %>% count (dest), неотмененные %>% count(tailnum, wt = distance), но без использования функции count().

3. Следующее определение отмененных рейсов не оптимально:

отмененные <– flights %>%

filter( is.na(dep_delay) | is.na(arr_delay) )

Почему? Какая колонка важнее в этом случае: задержка времени вылета (dep_delay) или задержка времени прилёта (arr_delay)?

4. Посмотрите на ежедневное количество отмененных рейсов. Есть ли здесь закономерность? Связана ли доля отмененных рейсов со средней задержкой?

5. Какой перевозчик (carrier) имеет худшую статистику по задержкам рейсов? Можно ли обнаружить зависимость плохих статистических показателей аэропортов от качества работы перевозчиков? Если да, то как, если нет, то почему?

6. За что отвечает аргумент выбора способа сортировки sort в функции count()? Когда уместно его использование?

Группирование данных бывает полезным в сочетании с функцией подведения итогов summarise(), но есть удобные шаблоны и для операций преобразования mutate() с фильтрацией filter(). Вспомним про укороченный_вариант_таблицы <– select(flights, year:day, ends_with("delay"), distance, air_time), хранящий лишь сведения об опозданиях, и выделим по 5 злостных нарушителей регламента полётов на каждый день:

укороченный_вариант_таблицы %>% group_by(year, month, day) %>%

filter(rank(desc(arr_delay)) <= 5)

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

( популярные_направления <– flights %>% group_by(dest) %>%

filter(n() > 17282) )

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

популярные_направления %>% filter(arr_delay > 0) %>%

mutate(относительная_задержка = arr_delay / sum(arr_delay)) %>%

select(year:day, dest, arr_delay, относительная_задержка)

Как видим, группирующая фильтрация приводит к изменениям, за которыми следует обычная фильтрация. Желательно избегать подобного, за исключением поверхностного анализа данных, в противном случае бывает трудно проверить корректность выполненных манипуляций. Функции, которые наиболее естественно работают со сгруппированными данными, например mutate() и filter(), называются оконными функциями, в отличие от резюмирующих функций типа summary(). Узнать больше об оконных функциях можно вызвав соответствующий раздел справки, введя консольную команду vignette("window-functions").

Упражнения

1. Вернитесь к примерам использования функций mutate() и filter() со списками. Как меняется результат каждой операции при создании промежуточных групп данных?

2. Какой самолет (бортовой номер) имеет рекордно худшее время вылета?

3. В какое время суток нужно лететь, чтобы максимальное избежать задержек?

4. Для каждого пункта назначения вычислите суммарное время задержек. Для каждого рейса вычислите долю его задержек в общей сумме.

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

6. Просмотрите каждый пункт назначения. Можно ли найти рейсы, долетевшие подозрительно быстро? То есть те полеты, которые потенциально представляют собой ошибку ввода данных. Вычислите долю воздушного времени полета относительно самого скоростного рейса до выбранного пункта назначения. Какие рейсы были дольше всего задержаны в воздухе?

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

8. У каждого самолета найдите количество рейсов до первой задержки более 1 часа.

Основы статистической обработки педагогической информации

Подняться наверх