В інших частинах силабусу ми бачили різні типи даних із різними характеристиками.
У цьому концепті ми розглянемо способи зберігати багатовимірні неоднорідні дані. На практиці більшість реальних даних саме такі, тож ми наближаємося до самої суті того, як R (переважно) використовують на практиці.
За десятиліття R додав чимало типів даних для роботи з табличними даними.
Цей силабус зосередиться переважно на tibble, але корисно знати й про деякі альтернативи.
data.tableTibble (описані нижче) і data.table - обидві визнані спроби вдосконалити традиційний data.frame у Base R.
Неминуче точаться суперечки про те, який із них «кращий».
Можливо, існує певний консенсус щодо таких моментів (навіть якщо їх критикуватимуть як спрощення).
tibble оптимізовано передусім для простоти використання та інтеграції з екосистемою Tidyverse.data.table оптимізовано передусім для сили та масштабованості, особливо під час роботи з дуже великими наборами даних.Утім, data.table недоступний в Exercism, тож ми згадуємо його тут лише для повноти.
data.frameУ Base R data.frame - це list з vectors однакової довжини.
Його можна уявляти як прямокутну таблицю даних, у якій кожен стовпець однорідний, а кожен рядок може (і зазвичай так і робить) містити дані різних типів.
Приклад для ілюстрації:
# create the column vectors
languages <- c("Fortran", "R", "Python", "Julia")
created <- c(1957, 1993, 1991, 2012)
has.syllabus <- c(FALSE, TRUE, TRUE, TRUE)
# join columns to create the dataframe
df <- data.frame(languages, created, has.syllabus)
df
#> languages created has.syllabus
#> 1 Fortran 1957 FALSE
#> 2 R 1993 TRUE
#> 3 Python 1991 TRUE
#> 4 Julia 2012 TRUE
# look at the structure
str(df)
#> 'data.frame': 4 obs. of 3 variables:
#> $ languages : chr "Fortran" "R" "Python" "Julia"
#> $ created : num 1957 1993 1991 2012
#> $ has.syllabus: logi FALSE TRUE TRUE TRUE
Маємо стовпець рядків тексту (англ. string), стовпець чисел і стовпець булевих значень (англ. Boolean). У збільшеному масштабі це інтуїтивний спосіб подати багато наборів реальних даних.
tibbleДизайн data.frame старий.
Досвід кількох десятиліть і зміна того, як використовують R, привели до переосмислення й створення оновленої альтернативи в Tidyverse: tibble.
Порівняно з Base R, tibble мають:
Коротко кажучи, tibble прагне «робити менше і скаржитися більше», що ще описують як «ледачий і буркотливий».
Ці типи зазвичай взаємозамінні: будь-яка функція, яка приймає data.frame, прийме й tibble, і навпаки.
Перетворення між типами легкі: as_tibble(df) і as.data.frame(tbl).
Для нової роботи використання tibble, ймовірно, допоможе нам створювати надійніший код.
Однак у світі R дуже багато застарілого коду й даних, тож data.frame, імовірно, ще довго залишатиметься поширеним.
# column vectors are same as for data.frame
library(tibble)
tbl <- tibble(languages, created, has.syllabus)
tbl
# A tibble: 4 × 3
#> languages created has.syllabus
#> <chr> <dbl> <lgl>
#> 1 Fortran 1957 FALSE
#> 2 R 1993 TRUE
#> 3 Python 1991 TRUE
#> 4 Julia 2012 TRUE
str(tbl)
#> tibble [4 × 3] (S3: tbl_df/tbl/data.frame)
#> $ languages : chr [1:4] "Fortran" "R" "Python" "Julia"
#> $ created : num [1:4] 1957 1993 1991 2012
#> $ has.syllabus: logi [1:4] FALSE TRUE TRUE TRUE
Звернімо увагу на типовий формат виведення: рядок коментаря з розмірами друкується автоматично, а типи стовпців теж показуються.
Tibble - невіддільна частина Tidyverse, тож додамо їх через library(tibble) або library(tidyverse).
Документація досить велика, у стилі Tidyverse:
Найпростіше ми можемо скористатися функцією tibble(), щоб поєднати вектори-стовпці, як і для data.frame().
Приклад цього було показано в попередньому розділі.
Якщо зручніше вводити значення порядково, відповідна функція - tribble().
tbl_r <- tribble(
# column names marked with tilde prefix
~languages, ~created, ~has.syllabus,
"Fortran", 1957, FALSE,
"R", 1993, TRUE,
"Python", 1991, TRUE,
"Julia", 2012, TRUE
)
tbl_r
# A tibble: 4 × 3
#> languages created has.syllabus
#> <chr> <dbl> <lgl>
#> 1 Fortran 1957 FALSE
#> 2 R 1993 TRUE
#> 3 Python 1991 TRUE
#> 4 Julia 2012 TRUE
На практиці існують десятки способів створити tibble, адже вони є типовим форматом виведення багатьох різноманітних функцій Tidyverse. Ми повернемося до цього в одному з наступних концептів.
У концепті функціонального програмування ми говорили про бібліотеку purrr для роботи з векторами та списками (одновимірними структурами даних).
Для датафреймів (традиційних чи tibble) відповідна бібліотека - dplyr.
Ми вже знайомилися з dplyr раніше, у концепті Перемикання.
Там ми використали лише кілька допоміжних функцій, але тепер можемо почати досліджувати решту цієї великої бібліотеки.
Датафрейми, зокрема tibble, можна розглядати як списки векторів-стовпців, тож індексація списку повертає вказаний стовпець.
tbl
# A tibble: 4 × 3
#> languages created has.syllabus
#> <chr> <dbl> <lgl>
#> 1 Fortran 1957 FALSE
#> 2 R 1993 TRUE
#> 3 Python 1991 TRUE
#> 4 Julia 2012 TRUE
tbl$created
#> [1] 1957 1993 1991 2012
Датафрейм також можна індексувати у стилі матриць.
tbl[c(2, 4), 1:2]
# A tibble: 2 × 2
#> languages created
#> <chr> <dbl>
#> 1 R 1993
#> 2 Julia 2012
У сучасному R з екосистемою Tidyverse функції dplyr зазвичай гнучкіші та зручніші, і саме на них ми зосередимося в решті цього концепту.
Отримаймо один стовпець за допомогою pull(), вказавши назву або порядковий номер (відʼємні числа рахують справа наліво).
tbl |> pull(created)
#> [1] 1957 1993 1991 2012
Це той самий результат, що й tbl$created, але з функцією, зручною для конвеєра.
Щоб отримати кілька стовпців, відповідна функція - select(), надзвичайно універсальна.
Отримуймо (або відкидаймо) стовпці за властивостями їхньої назви чи типу.
# Range with position and/or name
tbl |> select(1:created)
# A tibble: 4 × 2
#> languages created
#> <chr> <dbl>
#> 1 Fortran 1957
#> 2 R 1993
#> 3 Python 1991
#> 4 Julia 2012
# Exclude a column
tbl |> select(!created)
# A tibble: 4 × 2
#> languages has.syllabus
#> <chr> <lgl>
#> 1 Fortran FALSE
#> 2 R TRUE
#> 3 Python TRUE
#> 4 Julia TRUE
# Use type of column
tbl |> select(where(is.numeric))
# A tibble: 4 × 1
#> created
#> <dbl>
#> 1 1957
#> 2 1993
#> 3 1991
#> 4 2012
Можна використовувати кілька критеріїв за допомогою булевих операторів &, | і ! (і, або, не).
Назви стовпців, які є дійсними ідентифікаторами R, не потребують лапок усередині select().
Неприпустимі назви (наприклад, ті, що містять пробіли) можна взяти в зворотні лапки, хоча, можливо, краще їх перейменувати.
Функція select() може працювати з низкою допоміжних функцій для вибору назв стовпців: starts_with, contains, num_range та багатьма іншими.
matches дає змогу використовувати повне зіставлення з регулярними виразами.
Докладніше дивімося в документації.
Така потуга здається цілком безглуздою на нашому іграшковому датафреймі мов.
На щастя, tibble starwars входить до dplyr, тож маємо на чому більшому потренуватися.
# limit display to top 3 rows of non-list columns
starwars |>
select(!where(is.list)) |>
head(3)
# A tibble: 3 × 11
#> name height mass hair_color skin_color eye_color birth_year sex gender homeworld species
#> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr> <chr> <chr> <chr>
#> 1 Luke Skywalker 172 77 blond fair blue 19 male masculine Tatooine Human
#> 2 C-3PO 167 75 NA gold yellow 112 none masculine Tatooine Droid
#> 3 R2-D2 96 32 NA white, blue red 33 none masculine Naboo Droid
# pick a subset of columns
starwars |>
select(name | ends_with("color")) |>
head(5)
# A tibble: 5 × 4
#> name hair_color skin_color eye_color
#> <chr> <chr> <chr> <chr>
#> 1 Luke Skywalker blond fair blue
#> 2 C-3PO NA gold yellow
#> 3 R2-D2 NA white, blue red
#> 4 Darth Vader none white yellow
#> 5 Leia Organa brown light brown
Очевидно, dplyr дає потужні способи вибирати стовпці за назвою.
Чи можемо ми робити подібне з назвами рядків?
Ні!
Традиційні датафрейми R можуть мати назви рядків, але (після історії помилок і проблем із продуктивністю) назви рядків не дозволені в tibbles.
Якщо нам потрібні назви, помістимо їх у стовпець <chr> (зазвичай стовпець 1), який використовують як будь-який інший стовпець.
Функції імпорту, як-от as_tibble(), створять його автоматично під час імпорту даних із названими рядками.
Якщо це обмеження з назвами рядків здається дивно суворим, згадаймо, що більшість великих систем керування базами даних працюють із таблицями так само: Oracle, SQL Server, PostgreSQL, MySQL...
Отримуймо рядки, що відповідають певним критеріям, за допомогою filter(), або виключаймо їх за допомогою filter_out().
starwars |>
select(name:mass) |>
filter(between(height, 150, 165) & !is.na(mass))
# A tibble: 4 × 3
#> name height mass
#> <chr> <int> <dbl>
#> 1 Leia Organa 150 49
#> 2 Beru Whitesun Lars 165 75
#> 3 Nien Nunb 160 68
#> 4 Ben Quadinaros 163 65
Критерії фільтрування можуть бути довільно складними, але завжди ґрунтуються на вмісті рядків.
Якщо відомі номери рядків, можемо використати низку функцій slice().
starwars |>
select(name | homeworld) |>
slice(20:25)
# A tibble: 6 × 2
#> name homeworld
#> <chr> <chr>
#> 1 Palpatine Naboo
#> 2 Boba Fett Kamino
#> 3 IG-88 NA
#> 4 Bossk Trandosha
#> 5 Lando Calrissian Socorro
#> 6 Lobot Bespin
# random sample of rows
starwars |>
select(name | homeworld) |>
slice_sample(n = 4)
# A tibble: 4 × 2
#> name homeworld
#> <chr> <chr>
#> 1 Shaak Ti Shili
#> 2 Luminara Unduli Mirial
#> 3 Grievous Kalee
#> 4 Palpatine Naboo
Щоб прибрати дублікати рядків, скористаймося distinct().
Перше застереження: типова поведінка копіювання під час зміни означає, що початковий tibble зазвичай залишається незмінним.
Більшість змін застосовують до стовпців.
Назви стовпців можна змінити за допомогою rename(newname = oldname) або rename_with(), щоб застосувати функцію.
Типове застосування - очищення імпортованих назв, щоб із ними було зручніше працювати в R: прибрати пробіли й привести повʼязані назви до єдиного формату.
Звернімо увагу на синтаксис усередині rename().
Вміст стовпця oldname привʼязується до назви newname, звідси й порядок.
Порядок стовпців можна змінити за допомогою relocate().
Указані стовпці типово переміщуються на найлівіші позиції, але для точнішого розташування можна використати аргумент .before або .after.
sw <- starwars |> select(name:species) |> slice(1:4)
sw
# A tibble: 4 × 11
#> name height mass hair_color skin_color eye_color birth_year sex gender homeworld species
#> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr> <chr> <chr> <chr>
#> 1 Luke Skywalker 172 77 blond fair blue 19 male masculine Tatooine Human
#> 2 C-3PO 167 75 NA gold yellow 112 none masculine Tatooine Droid
#> 3 R2-D2 96 32 NA white, blue red 33 none masculine Naboo Droid
#> 4 Darth Vader 202 136 none white yellow 41.9 male masculine Tatooine Human
sw |> relocate(c(species, homeworld), .after = name)
# A tibble: 4 × 11
#> name species homeworld height mass hair_color skin_color eye_color birth_year sex gender
#> <chr> <chr> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr> <chr>
#> 1 Luke Skywalker Human Tatooine 172 77 blond fair blue 19 male masculine
#> 2 C-3PO Droid Tatooine 167 75 NA gold yellow 112 none masculine
#> 3 R2-D2 Droid Naboo 96 32 NA white, blue red 33 none masculine
#> 4 Darth Vader Human Tatooine 202 136 none white yellow 41.9 male masculine
Для більших змін mutate() дає змогу:
NULL
Очевидно, mutate() потужний, потенційно заплутаний і є причиною бути дуже вдячними за копіювання під час зміни.
Немає очевидної причини перейматися індексом маси тіла персонажів «Зоряних воєн», але про всяк випадок:
starwars |>
select(c(name, species, height, mass)) |>
mutate(BMI = mass / (height / 100)^2) |>
head(4)
# A tibble: 4 × 5
#> name species height mass BMI
#> <chr> <chr> <int> <dbl> <dbl>
#> 1 Luke Skywalker Human 172 77 26.0
#> 2 C-3PO Droid 167 75 26.9
#> 3 R2-D2 Droid 96 32 34.7
#> 4 Darth Vader Human 202 136 33.3
Коли ми хочемо працювати з підмножиною стовпців за допомогою таких функцій, як mutate(), послідовність select() |> mutate() із наведеного вище прикладу - один із варіантів.
У результаті будуть лише вибрані стовпці.
Або ж буває зручно використати pick() усередині виклику mutate():
starwars |> mutate(pick(c(name, species, height, mass)), BMI = mass / (height / 100)^2) |> head(4)
When you want to operate on a subset of the columns with functions such as `mutate()`, the `select() |> mutate()` sequence in the above example is one option.
Only the selected columns will be in the result.
Alternatively, it can be convenient to use `pick()` _within_ the `mutate()` call:
```R
starwars |> mutate(pick(c(name, species, height, mass)), BMI = mass / (height / 100)^2) |> head(4)
# A tibble: 4 × 15
#> name height mass hair_color skin_color eye_color birth_year sex gender homeworld species films vehicles
#> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr> <chr> <chr> <chr> <lis> <list>
#> 1 Luke Skywal… 172 77 blond fair blue 19 male mascu… Tatooine Human <chr> <chr>
#> 2 C-3PO 167 75 NA gold yellow 112 none mascu… Tatooine Droid <chr> <chr>
#> 3 R2-D2 96 32 NA white, bl… red 33 none mascu… Naboo Droid <chr> <chr>
#> 4 Darth Vader 202 136 none white yellow 41.9 male mascu… Tatooine Human <chr> <chr>
# ℹ 2 more variables: starships <list>, BMI <dbl>
У зміні використовуються лише стовпці, вказані через pick, але повертаються всі стовпці.
Операції з рядками рідше використовують для зміни окремих tibble (обʼєднання кількох tibble розглянемо в пізнішому концепті).
Один виняток: arrange() сортує рядки за значеннями в одному чи кількох стовпцях.
tbl
# A tibble: 4 × 3
#> languages created has.syllabus
#> <chr> <dbl> <lgl>
#> 1 Fortran 1957 FALSE
#> 2 R 1993 TRUE
#> 3 Python 1991 TRUE
#> 4 Julia 2012 TRUE
tbl |> arrange(languages)
# A tibble: 4 × 3
#> languages created has.syllabus
#> <chr> <dbl> <lgl>
#> 1 Fortran 1957 FALSE
#> 2 Julia 2012 TRUE
#> 3 Python 1991 TRUE
#> 4 R 1993 TRUE
Датафрейми, традиційні чи tibble, - основа того, як зазвичай використовують сучасний R.
Більшість функцій Tidyverse (не лише dplyr) приймають tibble на вході та/або створюють їх на виході.
Цей концепт дав лише короткий вступ, ледве торкнувшись поверхні можливого.
У пізніших концептах ми розглянемо кілька інших аспектів датафреймів (у межах технічних обмежень Exercism).