Треки
/
R
R
/
Салабус
/
Датафрейми
Да

Датафрейми у R

1 вправа

Про концепцію Датафрейми

В інших частинах силабусу ми бачили різні типи даних із різними характеристиками.

У цьому концепті ми розглянемо способи зберігати багатовимірні неоднорідні дані. На практиці більшість реальних даних саме такі, тож ми наближаємося до самої суті того, як R (переважно) використовують на практиці.

Різновиди датафреймів

За десятиліття R додав чимало типів даних для роботи з табличними даними.

Цей силабус зосередиться переважно на tibble, але корисно знати й про деякі альтернативи.

data.table

Tibble (описані нижче) і 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

Tibble - невіддільна частина Tidyverse, тож додамо їх через library(tibble) або library(tidyverse).

Документація досить велика, у стилі Tidyverse:

Створення tibble

Найпростіше ми можемо скористатися функцією 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. Ми повернемося до цього в одному з наступних концептів.

Маніпуляції з tibble

У концепті функціонального програмування ми говорили про бібліотеку 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    

Операції з рядками

Note

Очевидно, 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

Перше застереження: типова поведінка копіювання під час зміни означає, що початковий 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).

Редагувати через GitHub Посилання відкривається в новому вікні або вкладці

Вивчити концепцію Датафрейми