В інших частинах силабусу ми бачили різні типи даних із різними властивостями.
У цій концепції ми розглянемо способи зберігати багатовимірні різнорідні дані. На практиці більшість реальних даних має саме такий вигляд, тож тепер ми дістаємося до самої суті того, як R використовують (переважно) на практиці.
За десятиліття R додав чимало типів даних для роботи з табличними даними.
У цьому силабусі ми зосередимося переважно на tibble, але корисно знати й про альтернативи.
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
Маємо стовпець рядків тексту (англ. 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
Звернімо увагу на типовий формат друку: рядок із розмірами друкується автоматично, і типи стовпців теж показуються.
Tibble - основна складова Tidyverse, тож додати їх можна через library(tibble) або library(tidyverse).
Найпростіше - використати функцію tibble(), щоб поєднати вектори-стовпці, так само як для data.frame().
Приклад цього був у попередньому розділі.
Якщо зручніше вводити значення порядково, для цього є функція tribble().
На практиці є десятки способів створити tibble, адже вони - типовий формат виведення для найрізноманітніших функцій Tidyverse.
У концепції функціонального програмування ми говорили про бібліотеку purrr для роботи з векторами та списками (одновимірними структурами даних).
Для датафреймів (традиційних чи tibble) відповідна бібліотека - 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
# Use type of column
tbl |> select(where(is.numeric))
# A tibble: 4 × 1
#> created
#> <dbl>
#> 1 1957
#> 2 1993
#> 3 1991
#> 4 2012
Можна поєднувати кілька умов за допомогою булевих операторів &, | і ! (і, або, не).
На нашому іграшковому датафреймі з мовами така потужність здається цілком безглуздою.
На щастя, у dplyr входить tibble starwars, тож є на чому потренуватися на більших даних.
# 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
Дістати рядки, що відповідають певним умовам, можна функцією 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
Перше застереження: типова поведінка copy-on-modify означає, що початковий tibble зазвичай залишається незмінним.
Більшість змін застосовуються до стовпців.
Назви стовпців можна змінити функцією rename(newname = oldname) або rename_with(), щоб застосувати функцію.
Звернімо увагу на синтаксис усередині 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() потужна, часом заплутана, і саме через неї варто бути дуже вдячними за copy-on-modify.
Немає очевидної причини цікавитися індексом маси тіла персонажів «Зоряних воєн», але про всяк випадок:
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
Операції над рядками рідше застосовують для змінювання окремих 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
У великому вишневому саду зазвичай є невеликий запас зайвих дерев, які хотілося б продати як деревину. Це допомагає підтримувати сад здоровим і водночас дає додатковий дохід. Сад попросив нас допомогти написати код, який спростить вибір дерев, коли телефонує потенційний покупець.
Вбудована бібліотека datasets містить багато невеликих демонстраційних наборів даних, і в цій вправі використовується набір даних trees.
Щоб звернутися до набору даних, достатньо використати назву trees.
library(datasets) # loading is optional
trees |> head(3)
#> A data.frame: 3 × 3
#> Girth Height Volume
#> <dbl> <dbl> <dbl>
#> 1 8.3 70 10.3
#> 2 8.6 65 10.3
#> 3 8.8 63 10.2
Звернімо увагу на три стовпці: Girth, Height і Volume.
Спочатку набір даних має форму data.frame, але за бажанням можна працювати з ним як із tibble.
На жаль, сталася канцелярська помилка, яку так і не виправили, і один стовпець у наборі даних має назву Girth, хоча насправді це Diameter.
Визначте data.frame/tibble із назвою tree_data, який виправляє назву стовпця Girth у trees на Diameter.
head(tree_data, 3)
#> A tibble: 3 × 3
#> Diameter Height Volume
#> <dbl> <dbl> <dbl>
#> 8.3 70 10.3
#> 8.6 65 10.3
#> 8.8 63 10.2
Обхват (тобто довжина кола) усе ще лишається корисною мірою, як і вага деревини.
Обхват легко обчислити за формулою πD, де D - діаметр.
Вага всієї деревини обчислюється за формулою ρV, де ρ = 35 - густина вишневої деревини, а V - обʼєм деревини.
Визначте функцію girth_n_weight(data, rnd_digits), яка приймає датафрейм і числові аргументи.
Функція повинна повернути новий датафрейм із двома додатковими стовпцями: Girth і Weight, округленими до відповідної кількості знаків, заданої в rnd_digits.
girth_n_weight(tree_data, 1) |> head(3)
#> A tibble: 3 × 5
#> Diameter Height Volume Girth Weight
#> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 8.3 70 10.3 26.1 360.5
#> 8.6 65 10.3 27.0 360.5
#> 8.8 63 10.2 27.6 357.0
Для кожного потенційного покупця сад зберігає окрему копію набору даних, щоб допомогти з продажем.
У цьому наборі найважливіші стовпці, Weight і Height, переміщено на початок, а рядки відсортовано за Weight.
Визначте функцію orchard_copy(data, important_cols), яка приймає датафрейм.
Вона повинна повернути новий датафрейм, у якому Weight і Height переміщено на початок, а рядки відсортовано за Weight.
tree_data |> girth_n_weight() |> orchard_copy() |> head(3)
#> A tibble: 3 × 5
#> Weight Height Diameter Volume Girth
#> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 357 63 9 10 28
#> 360 70 8 10 26
#> 360 65 9 10 27
Кожен потенційний покупець отримає власну версію набору даних відповідно до своїх уподобань. Для цього покупець може вказати: мінімальну висоту, максимальну висоту та максимальну вагу окремого дерева.
Визначте функцію customer_copy(data, min_height, max_height, max_weight).
Поверніть копію датафрейму для покупця зі стовпцями Height, Weight, Diameter і Girth та рядками, що задовольняють задані обмеження.
tree_data |>
girth_n_weight(1) |>
orchard_copy() |>
customer_copy(65, 75, 1500) |>
head(3)
#> A tibble: 3 × 4
#> Height Weight Diameter Girth
#> <dbl> <dbl> <dbl> <dbl>
#> 70 360.5 8.3 26.1
#> 65 360.5 8.6 27.0
#> 66 546.0 11.0 34.6
Зареєструйтеся на Exercism, щоб вивчати й опановувати R, а також 21 концепція111 вправ та справжнє наставництво від людей, і все це безкоштовно.