Перейти к основному содержимому

tidyverse и ggplot2

Разработчику Архитектору

Дальше: простые приложения · архитектура аналитики


tidyverse и ggplot2

tidyverse — набор согласованных пакетов R для работы с tidy data (аккуратные табличные данные): каждый столбец — переменная, каждая строка — наблюдение. ggplot2 реализует грамматику графиков — график собирается слоями (geom, aes, facet) вместо одного монолитного вызова plot().

Практикум по шагам — установка, загрузка CSV, трансформации dplyr, формы tidyr, графики ggplot2, воспроизводимость и мини-проект.

ШагТемаЗачем
1Установка tidyverseПодготовить пакеты
2Tidy dataПравильная форма таблицы
3readrЗагрузка CSV
4dplyrfilter, group_by, summarise
5joinСвязь справочников
6tidyrpivot_longer / pivot_wider
7ggplot2Слои и эстетики
8Сохранение и отчётggsave, R Markdown
9Мини-проектСквозной сценарий
МатериалЗачем
Основы RВекторы, data.frame, синтаксис
Простые приложенияCSV, отчёты, первый pipeline
Типы и векторизацияNA, факторы
Функции и пакетыlibrary(), свои функции
SQL joinsЛогика слияния таблиц
CSVФормат файлов
pandasАналог в Python
Pkg и Plots в JuliaТот же CSV в Julia

Навигация по блоку R

RStudio / Posit

Удобно выполнять код по ячейкам в RStudio. Установка и Rscript — в первой программе. Сохраняйте скрипты в UTF-8 для кириллицы в подписях графиков.


Шаг 1 — установка

install.packages("tidyverse")
library(tidyverse)

Мета-пакет tidyverse подтягивает согласованные версии:

ПакетРоль
ggplot2Графики
dplyrТрансформации таблиц
tidyrФорма данных
readrЧтение CSV
tibbleСовременные data frame
purrrФункциональные итерации
stringrСтроки
forcatsФакторы

Проверка:

packageVersion("ggplot2")
packageVersion("dplyr")

Если корпоративный прокси блокирует CRAN — настройте repos или зеркало. Список зеркал: cran.r-project.org/mirrors.html.


Шаг 2 — принципы tidy data

ПравилоНарушениеИсправление
Один столбец — одна переменнаяsales_2023, sales_2024 в одной строкеpivot_longer
Одна строка — одно наблюдениенесколько сущностей в строкенормализовать строки
Одна таблица — один тип сущностивсё в одном листеразделить таблицы

Пример широкой таблицы (плохо для ggplot):

wide <- tibble(
region = c("North", "South"),
Q1 = c(100, 80),
Q2 = c(120, 90)
)

Цель — long формат:

long <- wide %>%
pivot_longer(
cols = starts_with("Q"),
names_to = "quarter",
values_to = "revenue"
)

Разбор:

  • starts_with("Q") выбирает столбцы кварталов.
  • names_to — имя нового столбца с метками кварталов.
  • values_to — столбец с числами.

Подробнее о модели данных — анализ данных.


Шаг 3 — readr и загрузка CSV

library(readr)

sales <- read_csv("sales.csv", show_col_types = FALSE)
glimpse(sales)

Преимущества readr перед базовым read.csv:

  • автоматическое определение типов столбцов;
  • выше скорость на больших файлах;
  • единообразная обработка NA;
  • возвращает tibble с удобным выводом.

Пример sales.csv:

date,region,amount,customer_id
2024-01-15,North,1200,C001
2024-01-16,South,800,C002
2024-01-17,North,0,C003
sales <- read_csv("sales.csv",
col_types = cols(
date = col_date(),
region = col_character(),
amount = col_double(),
customer_id = col_character()
)
)

Явные col_types убирают предупреждения и ускоряют повторное чтение.

Запись результата:

write_csv(by_region, "report/by_region.csv")

См. CSV в энциклопедии.


Шаг 4 — dplyr, pipeline трансформаций

library(dplyr)

by_region <- sales %>%
filter(amount > 0) %>%
group_by(region) %>%
summarise(
total = sum(amount, na.rm = TRUE),
n = n(),
avg = mean(amount, na.rm = TRUE),
.groups = "drop"
) %>%
arrange(desc(total))

Глаголы dplyr

ГлаголНазначениеПример
filterСтроки по условиюfilter(amount > 0)
selectСтолбцыselect(date, amount)
mutateНовые столбцыmutate(tax = amount * 0.2)
summariseАгрегатыsummarise(total = sum(amount))
group_byГруппы для summarisegroup_by(region)
arrangeСортировкаarrange(desc(total))
distinctУникальные строкиdistinct(customer_id)
slice_maxТоп-Nslice_max(amount, n = 5)

Pipe %>% и native |>

# Магритт pipe (tidyverse)
sales %>% filter(amount > 0) %>% count()

# Native pipe R 4.1+
sales |> filter(amount > 0) |> count()

Читается сверху вниз: возьми sales, отфильтруй, посчитай. Такой стиль называют pipeline.

mutate и case_when

sales_labeled <- sales %>%
mutate(
tier = case_when(
amount >= 1000 ~ "high",
amount >= 500 ~ "mid",
TRUE ~ "low"
),
amount_with_tax = amount * 1.2
)

Шаг 5 — join, связь таблиц

customers <- read_csv("customers.csv", show_col_types = FALSE)
# customer_id, customer_name, segment

enriched <- sales %>%
left_join(customers, by = "customer_id")
Тип joinРезультат
left_joinВсе строки слева + совпадения справа
inner_joinТолько совпавшие ключи
right_joinВсе строки справа
full_joinВсе строки с обеих сторон
anti_joinСтроки слева без пары справа

Проверка дубликатов ключа перед join:

customers %>%
count(customer_id) %>%
filter(n > 1)

Идеи те же, что в SQL.


Шаг 6 — tidyr, pivot и separate

pivot_longer / pivot_wider

long <- wide %>%
pivot_longer(
cols = starts_with("Q"),
names_to = "quarter",
values_to = "revenue"
)

wide_again <- long %>%
pivot_wider(
names_from = quarter,
values_from = revenue
)

separate и unite

df <- tibble(code = c("RU-MSK", "US-NYC"))
df %>%
separate(code, into = c("country", "city"), sep = "-")

df2 <- tibble(country = "RU", city = "MSK")
df2 %>%
unite("code", country, city, sep = "-")

drop_na и replace_na

sales %>%
drop_na(amount)

sales %>%
replace_na(list(amount = 0))

Шаг 7 — ggplot2, грамматика графиков

Базовый столбчатый график:

library(ggplot2)

p <- ggplot(by_region, aes(x = region, y = total, fill = region)) +
geom_col() +
labs(
title = "Продажи по регионам",
x = "Регион",
y = "Сумма"
) +
theme_minimal()

print(p)

Слои графика

СлойФункции
Данныепервый аргумент ggplot(data, ...)
Эстетикиaes(x, y, color, fill, size, linetype)
Геометрияgeom_point, geom_line, geom_col, geom_histogram
Масштабыscale_x_continuous, scale_fill_brewer
Фасетыfacet_wrap(~ year), facet_grid(region ~ .)
Темаtheme_minimal(), правки theme()
Координатыcoord_flip(), coord_fixed()

Линейный график времени

ggplot(sales, aes(x = date, y = amount, color = region)) +
geom_line(linewidth = 0.8) +
geom_point(size = 2) +
scale_x_date(date_labels = "%Y-%m") +
labs(x = "Дата", y = "Сумма")

geom_col и geom_bar

# geom_col — высоты уже в данных
ggplot(by_region, aes(region, total)) + geom_col()

# geom_bar — считает частоты категорий
ggplot(sales, aes(region)) + geom_bar()

Гистограмма и плотность

ggplot(sales, aes(x = amount)) +
geom_histogram(bins = 30, fill = "steelblue", color = "white")

ggplot(sales, aes(x = amount)) +
geom_density(fill = "steelblue", alpha = 0.4)

Факторы и порядок осей

by_region %>%
mutate(region = fct_reorder(region, total)) %>%
ggplot(aes(region, total)) +
geom_col() +
coord_flip()

Шаг 8 — сохранение, purrr, воспроизводимость

ggsave

ggsave(
"report/by_region.png",
plot = p,
width = 8,
height = 5,
dpi = 150
)

purrr — несколько файлов

library(purrr)

files <- list.files("data", pattern = "\\.csv$", full.names = TRUE)
all_data <- map_dfr(files, ~ read_csv(.x, show_col_types = FALSE))

map_dfr читает каждый файл и склеивает строки — замена цикла for. Подробнее — функции и пакеты.

renv и sessionInfo

# install.packages("renv")
renv::init()
renv::snapshot()
ПрактикаЦель
renvФиксация версий пакетов
set.seed()Повторяемость случайных операций
R MarkdownОтчёт = код + текст
sessionInfo()Снимок окружения в лог

Минимальный .Rmd — в простых приложениях.

R Markdown

Файл report.Rmd объединяет код, графики и пояснения. Команда rmarkdown::render("report.Rmd") собирает HTML или PDF. Формулы — LaTeX в отчётах.


Шаг 9 — мини-проект по шагам

1. Подготовка данных

library(tidyverse)

sales <- read_csv("data/sales.csv", show_col_types = FALSE)
regions <- read_csv("data/regions.csv", show_col_types = FALSE)

2. Очистка и агрегаты

monthly <- sales %>%
filter(amount > 0) %>%
mutate(month = floor_date(date, "month")) %>%
group_by(month, region) %>%
summarise(total = sum(amount), .groups = "drop")

3. Обогащение справочником

monthly_enriched <- monthly %>%
left_join(regions, by = "region")

4. Два графика

p_bar <- ggplot(by_region, aes(fct_reorder(region, total), total)) +
geom_col(fill = "steelblue") +
coord_flip() +
labs(title = "Итого по регионам")

p_line <- ggplot(monthly, aes(month, total, color = region)) +
geom_line(linewidth = 1) +
labs(title = "Динамика по месяцам")

5. Экспорт

dir.create("report", showWarnings = FALSE)
ggsave("report/bar.png", p_bar, width = 8, height = 5)
ggsave("report/line.png", p_line, width = 10, height = 5)
write_csv(monthly_enriched, "report/monthly.csv")

Шаблон расширяется в простых приложениях.


Типичные ошибки

СимптомПричинаРешение
summarise() не тоНет group_byЯвная группировка
Группы залипаютСтарый group_by.groups = "drop" или ungroup()
Неверный порядок баровФактор по алфавитуfct_reorder, fct_inorder
Пустой графикВсе NAdrop_na(), filter
geom_col vs geom_barПутаницаgeom_col — готовые высоты
Раздувание после joinДубликаты ключаcount(key), distinct
Предупреждение readrУгадывание типовЯвный col_types
Кириллица в PDFШрифтНастроить theme / device

tidyverse и base R

Аспектtidyversebase R
Читаемость pipelineВысокаяЦиклы, [, $
ЗависимостиНабор пакетовТолько base
ОбучениеСтандарт в индустрииНужен для legacy
Скорость на огромных данныхdata.table часто быстрееdata.table

На собеседованиях и в командах аналитики tidyverse + ggplot2 — де-факто стандарт. Base R полезен для чтения старого кода.


Упражнения

  1. Загрузите sales.csv, посчитайте средний amount по region и постройте столбчатый график.
  2. Преобразуйте широкую таблицу с кварталами в long и постройте линию revenue по quarter.
  3. Сделайте left_join с таблицей, где есть дубликаты customer_id — найдите раздувание строк через nrow.
  4. Сохраните график в PNG 300 dpi и приложите sessionInfo() в текстовый лог.
  5. Перепишите pipeline с %>% на native |> (R 4.1+).

FAQ

Нужен ли весь tidyverse? library(tidyverse) загружает основные пакеты. Для минимального скрипта достаточно dplyr, ggplot2, readr.

Чем tibble отличается от data.frame? Tibble не превращает строки в факторы по умолчанию, печатает компактно, строже с [.

ggplot2 или plotly? ggplot2 — статичные отчёты; plotly — интерактив. Для дашбордов — Shiny.

Как ускорить большие данные? Пакет data.table или duckdb; tidyverse остаётся для средних объёмов и прототипов.

Где учить дальше? R for Data Science — официальный путь по tidyverse.


Глоссарий

ТерминОпределение
Tidy dataСтолбец = переменная, строка = наблюдение
PipeПередача результата следующей функции
Глагол dplyrfilter, mutate, summarise и др.
Эстетика aesСвязь столбцов с осями и цветом
GeomГеометрический слой точек, линий, столбцов
FacetРазбивка графика по категории
JoinСлияние таблиц по ключу
PivotПеревод wide ↔ long
TibbleСовременная таблица tidyverse
renvВоспроизводимые версии пакетов

Что дальше

  • lubridate — даты во временных рядах.
  • broom — tidy-вывод моделей (lm, glm).
  • Shiny — интерактивные дашборды.
  • targets — оркестрация pipeline.
  • Практика — простые приложения.
Итог

tidyverse и ggplot2 — ядро современного анализа на R: от CSV до публикации графика в одном стиле. Освойте pipeline dplyr и слои ggplot — остальные пакеты надстраиваются поверх.


Шаг 10 — stringr и forcats

stringr

library(stringr)

dirty <- c(" North ", "SOUTH", "north-east")
clean <- str_trim(str_to_lower(dirty))
str_replace(clean, "north-east", "north_east")
str_detect(clean, "^north")
ФункцияНазначение
str_trimУбрать пробелы по краям
str_to_lower / str_to_upperРегистр
str_replaceЗамена подстроки
str_detectЛогический вектор совпадений
str_splitРазбить строку

forcats — порядок категорий

library(forcats)

by_region %>%
mutate(region = fct_reorder(region, total)) %>%
ggplot(aes(region, total)) +
geom_col() +
coord_flip()

fct_lump сворачивает редкие категории в "Other" — удобно для отчётов с длинным хвостом.


Шаг 11 — расширенный ggplot2

facet_wrap и facet_grid

ggplot(sales, aes(date, amount)) +
geom_line() +
facet_wrap(~ region, ncol = 2)

ggplot(sales, aes(date, amount)) +
geom_line() +
facet_grid(region ~ tier)

Палитры и scale

ggplot(by_region, aes(region, total, fill = region)) +
geom_col() +
scale_fill_brewer(palette = "Set2") +
scale_y_continuous(labels = scales::comma)

ggplot(sales, aes(amount)) +
geom_histogram(fill = "steelblue") +
scale_x_log10()

Аннотации и подписи

ggplot(by_region, aes(region, total)) +
geom_col(fill = "steelblue") +
geom_text(aes(label = scales::comma(total)), vjust = -0.3) +
labs(
title = "Продажи по регионам",
caption = "Источник: sales.csv"
) +
theme_minimal(base_family = "sans")

Сохранение для печати

ggsave("report/print.pdf", p, width = 10, height = 6, device = cairo_pdf)
ggsave("report/slide.png", p, width = 16, height = 9, dpi = 300)

Шаг 12 — tibble и column types

sales_typed <- read_csv("sales.csv",
col_types = cols(
date = col_date(format = "%Y-%m-%d"),
region = col_factor(levels = c("North", "South", "East", "West")),
amount = col_double(),
customer_id = col_character()
)
)

Преимущества явных типов:

  • фактор с фиксированным порядком на графике;
  • нет предупреждений col_spec;
  • быстрее повторное чтение.
glimpse(sales_typed)
spec(sales_typed)

Шаг 13 — window functions и slice

library(dplyr)

sales %>%
group_by(region) %>%
arrange(date) %>%
mutate(
running_total = cumsum(amount),
rank_in_region = row_number()
) %>%
slice_max(amount, n = 3, by = region)
ФункцияСмысл
row_numberПорядковый номер в группе
lag / leadСдвиг по времени
cumsumНакопительная сумма
slice_maxТоп-N в группе

Шаг 14 — R Markdown одностраничный отчёт

# report.Rmd YAML
# title: "Отчёт по продажам"
# output: html_document
## Сводка

```{r setup, include=FALSE}
library(tidyverse)
sales <- read_csv("data/sales.csv", show_col_types = FALSE)
```

```{r summary}
sales %>%
filter(amount > 0) %>%
group_by(region) %>%
summarise(total = sum(amount))
```

```{r plot, fig.width=8}
ggplot(sales, aes(region, amount)) + geom_col()
```
rmarkdown::render("report.Rmd")

Отчёт воспроизводим: код и график пересобираются из данных.


Шаг 15 — сравнение с pandas (Python)

Задачаtidyversepandas
Чтение CSVread_csvread_csv
Фильтрfilter(x > 0)df[df.x > 0]
Группировкаgroup_by %>% summarisegroupby().agg()
Графикggplotmatplotlib / seaborn
Pipe%>%method chain

Подробнее — pandas в анализе. Тот же CSV можно прогнать в Julia.


Дополнительные упражнения

  1. Постройте facet_wrap по region для линейного графика amount по date.
  2. Используйте str_detect чтобы отфильтровать регионы, начинающиеся на "N".
  3. Сделайте running_total по date внутри каждого region.
  4. Соберите report.Rmd с одной таблицей и двумя графиками.
  5. Инициализируйте renv, добавьте snapshot, проверьте на второй машине renv::restore().

Дополнительный FAQ

Чем data.table отличается от dplyr? data.table быстрее на очень больших данных; синтаксис компактнее. tidyverse читается лучше новичкам.

Как подписать оси по-русски в PDF? Укажите шрифт с кириллицей в theme(text = element_text(family = "...")) или используйте showtext.

Где взять учебные данные? data() в base R (mtcars, iris); пакет nycflights13; CSV из простых приложений.

Как отладить пустой ggplot? nrow(data), summary(data), убрать filter, проверить aes и имена столбцов.


Разбор типичного pipeline — построчно

result <- sales %>%
filter(amount > 0) %>% # 1. только положительные
mutate(month = floor_date(date, "month")) %>% # 2. месяц
group_by(region, month) %>% # 3. ключ группы
summarise(total = sum(amount), .groups = "drop") %>% # 4. агрегат
arrange(region, month) # 5. порядок для графика

Каждый %>% передаёт tibble следующей функции — если тип сломался, ошибка обычно на ближайшем шаге.

Полный walkthrough — отчёт sales за один скрипт

Файл scripts/sales_report.R — воспроизводимый отчёт без ручных шагов.

#!/usr/bin/env Rscript
# scripts/sales_report.R

suppressPackageStartupMessages({
library(tidyverse)
})

main <- function() {
dir.create("report", showWarnings = FALSE)

sales <- read_csv("data/sales.csv", show_col_types = FALSE)
regions <- read_csv("data/regions.csv", show_col_types = FALSE)

summary_df <- sales %>%
filter(amount > 0) %>%
left_join(regions, by = "region") %>%
group_by(region, tier = coalesce(tier, "unknown")) %>%
summarise(
total = sum(amount, na.rm = TRUE),
orders = n(),
.groups = "drop"
) %>%
arrange(desc(total))

write_csv(summary_df, "report/summary.csv")

p <- ggplot(summary_df, aes(fct_reorder(region, total), total, fill = tier)) +
geom_col() +
coord_flip() +
labs(title = "Продажи по регионам", x = NULL, y = "Сумма") +
theme_minimal()

ggsave("report/sales_bar.png", p, width = 10, height = 6, dpi = 150)

monthly <- sales %>%
filter(amount > 0) %>%
mutate(month = floor_date(date, "month")) %>%
group_by(month, region) %>%
summarise(total = sum(amount), .groups = "drop")

p2 <- ggplot(monthly, aes(month, total, color = region)) +
geom_line(linewidth = 1) +
labs(title = "Динамика по месяцам") +
theme_minimal()

ggsave("report/sales_line.png", p2, width = 10, height = 6, dpi = 150)

message("OK: report/summary.csv, report/sales_bar.png, report/sales_line.png")
}

main()

Запуск:

Rscript scripts/sales_report.R

Тот же CSV можно обработать в Julia для сравнения результатов.


Ссылки на смежные темы

ТемаМатериал
Основы R2.md
Простые приложения103.md
SQL joinsSQL 101
CSV219
Анализ данных3.11 intro
pandas427

В подборках

Статья входит в маршрут R и раздел анализ данных. Сравнение с Julia Plots и pandas.



Практикум — шаг 10: stringr и forcats

library(stringr)
library(forcats)

sales %>%
mutate(region = str_trim(region)) %>%
mutate(region = fct_reorder(region, amount, .fun = sum))

Практикум — шаг 11: lubridate

library(lubridate)

sales %>%
mutate(
date = ymd(date),
month = floor_date(date, "month")
)

Практикум — шаг 12: broom + lm

library(broom)

fit <- lm(amount ~ region, data = sales)
tidy(fit)
glance(fit)

Практикум — шаг 13: ggplot2 темы

p + theme_minimal() +
theme(
plot.title = element_text(face = "bold"),
axis.text.x = element_text(angle = 45, hjust = 1)
)

Воркшоп tidyverse (60 мин)

МинЗадача
0–10read_csv
10–25dplyr pipeline
25–35left_join
35–50ggplot bar + line
50–60ggsave + renv

Troubleshooting R

СимптомРешение
summarise wronggroup_by
sticky groups.groups = "drop"
geom_col vs barcol vs count
join explodedistinct keys

FAQ tidyverse

Весь tidyverse? — можно dplyr+ggplot2+readr.

tibble vs data.frame? — современный вывод.

ggplot vs plotly? — static vs interactive.

Большие данные? — data.table, duckdb.



Дополнительные сценарии (tidyverse)

Сценарий A: read_csv → glimpse

sales <- read_csv("sales.csv", show_col_types = FALSE)
glimpse(sales)

Сценарий B: pipeline без pipe

arrange(summarise(group_by(filter(sales, amount > 0), region), total = sum(amount)), desc(total))

Сравните читаемость с %>%.

Сценарий C: join и nrow

До join: nrow(sales); после left_join с дубликатами — рост строк.

Сценарий D: fct_reorder на графике

Столбцы по убыванию total, не по алфавиту.

Сценарий E: renv snapshot

renv::init()
renv::snapshot()

Упражнения — контрольная точка

  1. lubridate: агрегат по month.
  2. broom::tidy(lm(...)).
  3. facet_wrap по region.
  4. ggsave 300 dpi.
  5. Сравнение с Julia 104.
Содержание