دی

دیتافریم در R

1 تمرین

درباره‌ی دیتافریم

در بخش‌های دیگر سرفصل، انواع داده‌ی مختلفی با ویژگی‌های گوناگون دیده‌ایم.

این مفهوم به روش‌های ذخیره‌ی داده‌های چندبعدی و ناهمگن می‌پردازد. در عمل، بیشترِ داده‌های دنیای واقعی این‌گونه‌اند، بنابراین اکنون به قلب آن چیزی می‌رسیم که R در عمل (بیشتر وقت‌ها) برای آن به کار می‌رود.

گونه‌های دیتافریم

در طول چند دهه، R انواع داده‌ی متعددی را برای کار با داده‌های جدولی افزوده است.

این سرفصل عمدتاً بر tibbleها تمرکز می‌کند، اما دانستن چند جایگزین دیگر هم مفید است.

data.table

tibbleها (که در ادامه توضیح داده می‌شوند) و data.tableها هر دو تلاش‌هایی هستند که اعتبار زیادی دارند برای بهبود data.frame سنتی در R پایه. ناگزیر بحث‌های زیادی وجود دارد بر سر اینکه کدام «بهتر» است. شاید درباره‌ی نکات زیر تا حدی اجماع وجود داشته باشد (هرچند ممکن است ساده‌انگارانه خوانده شوند).

  • tibble عمدتاً برای سهولت استفاده و یکپارچگی با اکوسیستم Tidyverse بهینه شده است.
  • data.table عمدتاً برای توان خام و مقیاس‌پذیری بهینه شده است، به‌ویژه هنگام کار با مجموعه‌داده‌های بسیار بزرگ.

در هر حال، data.table در Exercism در دسترس نیست، پس اینجا فقط برای کامل بودن مطلب به آن اشاره شد.

data.frame

در 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

ما یک ستون از رشته‌های متنی، یک ستون از اعداد و یک ستون از مقادیر «منطقی» داریم. وقتی بزرگ‌تر شود، این روشی شهودی برای نمایش بسیاری از مجموعه‌های داده‌ی دنیای واقعی است.

tibble

طرح data.frame قدیمی است.

تجربه‌ی چندین دهه، به‌همراه تغییر الگوهای استفاده از R، به بازطراحی انجامید و جایگزینی مدرن در Tidyverse پدید آورد: tibbleها.

در مقایسه با 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 است.

پیش‌تر در مفهوم Switch با 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> بگذارید (معمولاً ستون ۱) و مثل هر ستون دیگری از آن استفاده کنید. توابع واردکردنی مانند 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

نخستین نکته: پیش‌فرض copy-on-modify یعنی 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() قدرتمند است، ممکن است گیج‌کننده باشد و دلیلی است برای اینکه بسیار سپاسگزار 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

وقتی می‌خواهید با توابعی مانند 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 این پیوند در پنجره یا زبانه‌ی جدیدی باز می‌شود

دیتافریم را یاد بگیرید