Широкий против длинного: одни данные, две формы
Любой вопрос перестройки сводится к одному различию, так что давайте его увидим. Вот один небольшой набор данных — квартальные продажи по двум городам — построенный в обеих формах:
Идентичная информация, разная геометрия. У широкого формата одна строка на город и один столбец на квартал — квартал живёт в именах столбцов. У длинного одна строка на наблюдение (город × квартал), квартал понижен до обычного столбца, а все измерения лежат в единственном столбце sales.
Ни одна из форм не «правильная» — они служат разным господам. Широкий формат читают люди и экспортируют электронные таблицы: компактно, сравнимо с первого взгляда. Длинный формат потребляют инструменты, а преобразование между ними называется пивотом (или перестройкой — то же самое).
Почему для анализа побеждает длинный формат
Посмотрите, что широкий формат сделал с понятием «квартал»: он раскрошил одну переменную по именам столбцов. Ничто не умеет вычислять по имени столбца. В длинном формате quarter снова является данными, и весь инструментарий группового анализа включается:
aggregate(sales ~ quarter, ...)илиgroup_by(quarter)— в широком формате невозможно, там каждый квартал отдельный столбец, который пришлось бы обрабатывать вручную (групповые сводки все предполагают длинные данные).- ggplot2 отображает столбцы на эстетики:
aes(x = quarter, y = sales, color = city)требует существования этих трёх столбцов. Рисовать широкие данные — постоянная борьба; рисовать длинные — одна строка. - Добавление Q3 — это добавление строк, без изменения схемы, тогда как широкий формат обзаводится новым столбцом, о котором должен узнать каждый последующий шаг.
Практическое правило: храните и анализируйте в длинном формате, представляйте в широком. Поэтому оба преобразования ниже так востребованы: данные приходят широкими из электронных таблиц, удлиняются для анализа и снова расширяются ради итоговой таблицы в отчёте.
Из широкого в длинный: pivot_longer()
Пакет tidyr (родственник dplyr в tidyverse) владеет современной перестройкой. Его сниппеты здесь статичны — песочница выполняет только базовый R. Удлинение требует трёх решений, по аргументу на каждое:
library(tidyr)
long <- pivot_longer(wide,
cols = c(Q1, Q2), # which columns to stack
names_to = "quarter", # new column that receives the old NAMES
values_to = "sales" # new column that receives the cell VALUES
)
Разберём: cols называет растворяемые столбцы (синтаксис диапазона Q1:Q2 и помощники вроде starts_with("Q") тоже работают — удобно, когда их двадцать). Каждая выбранная ячейка становится строкой; столбец, из которого она пришла, попадает в quarter, а само число — в sales. Столбцы, не перечисленные в cols (здесь city), считаются идентификаторами и повторяются вниз по строкам. Результат — ровно тот фрейм long, что напечатан выше.
Из длинного в широкий: pivot_wider()
Обратный путь требует двух решений — откуда берутся новые имена столбцов и что их заполняет:
wide <- pivot_wider(long,
names_from = quarter, # distinct values here become new columns
values_from = sales # these values fill the cells
)
Каждое различное значение quarter (Q1, Q2) становится столбцом; каждая ячейка заполняется значением sales из строки, соответствующей этому городу и кварталу. Оставшиеся столбцы (city) выступают идентификаторами строк — по одной выходной строке на каждую различную комбинацию. Город без какого-то квартала получит NA в этой ячейке (аргумент values_fill подставляет что-то другое, например values_fill = 0 для счётных данных).
В старых учебниках вам встретится и предыдущее поколение: spread() — это pivot_wider(), а gather() — pivot_longer(). Они вытеснены с tidyr 1.0, по-прежнему работают, и учить их дальше узнавания не стоит.
В базовом R есть reshape() — честное предупреждение
Базовый R умеет это без пакетов, через reshape() — функцию, настолько знаменитую своей непонятностью, что её собственная документация исторически за неё извинялась. Она спроектирована вокруг словаря лонгитюдных исследований (idvar, timevar, direction), имена аргументов неуклюже ложатся на повседневные данные, и все забывают их между использованиями. Работать она работает:
Обратите внимание на имена в выводе: sales.Q1, sales.Q2 — имя столбца значений приклеено к каждому. Сойдёт на безрыбье, в окружении без зависимостей или когда вы встретили это в старом коде. Но если вы перестраиваете данные чаще раза в год, честной рекомендацией будет install.packages("tidyr") — это единственная задача обработки данных, где инструмент базового R действительно обходится дороже, чем зависимость, от которой он избавляет.
Ловушка дублирующихся ключей при расширении
Расширение предполагает, что каждая комбинация «идентификатор + имя» задаёт одно значение. Если у Lima две строки Q1, какое число попадёт в единственную ячейку Q1? pivot_wider() отказывается гадать: она выдаёт предупреждение (values are not uniquely identified) и кладёт в ячейку столбец-список — датафрейм с вложенными списками, что сломает следующее же действие с ним.
Увидев это предупреждение, не хватайтесь сразу за запасной выход values_fn — спросите, почему существуют дубликаты. Обычно данные оказываются более мелкозернистыми, чем вы думали (строки дневные, а не квартальные, — значит, сначала сводка, потом расширение), либо соединение выше по потоку продублировало строки. values_fn = mean (или sum) — законное решение только тогда, когда вы можете вслух сказать, какой агрегацией должны схлопнуться дубликаты. reshape() здесь хуже: она молча оставляет первый дубликат и отбрасывает остальные, лишь с предупреждением, которое легко проскроллить.
Что вы уносите с собой
- Широкий формат: переменные прячутся в именах столбцов, сделано для чтения. Длинный: одна строка на наблюдение, сделано для анализа и ggplot2.
- Храните и анализируйте в длинном формате, представляйте в широком.
pivot_longer(cols, names_to, values_to)складывает столбцы в строки; неперечисленные столбцы становятся повторяющимися идентификаторами.pivot_wider(names_from, values_from)разворачивает строки в столбцы; пробелы заполняютсяNA.spread()/gather()— старые имена; базоваяreshape()работает, но знаменита своей неуклюжестью.- Дублирующиеся пары «идентификатор + имя» делают расширение неоднозначным — сначала сводка, а не просто заглушение предупреждения.
Дальше: чтение настоящих данных из файлов — read.csv() и её острые края.
Часто задаваемые вопросы
В чём разница между широкими и длинными данными в R?
Одни и те же данные, разные формы. Широкий формат разносит переменную по столбцам (скажем, по столбцу на квартал) — одна строка на объект, сделано для чтения человеком. Длинный формат складывает её в строки — одна строка на наблюдение, со столбцом имени и столбцом значения, — сделано для группового анализа и ggplot2.
Как преобразовать широкие данные в длинные в R?
pivot_longer(df, cols = Q1:Q4, names_to = "quarter", values_to = "sales") из tidyr складывает выбранные столбцы в два новых: старые имена столбцов уходят в столбец names_to, а значения ячеек — в столбец values_to.
Как преобразовать длинные данные в широкие в R?
pivot_wider(df, names_from = quarter, values_from = sales) из tidyr разворачивает строки в столбцы: каждое различное значение names_from становится столбцом, заполняемым соответствующими записями values_from. Отсутствующие комбинации становятся NA.
Что заменило spread и gather в R?
pivot_wider() заменила spread(), а pivot_longer() заменила gather() в tidyr 1.0 (2019). Старые функции по-прежнему работают — вы встретите их в старых учебниках, — но весь новый код должен использовать пару pivot_* с более понятными аргументами и большими возможностями.