در بخشهای دیگر سرفصل، انواع دادهی مختلفی با ویژگیهای گوناگون دیدهایم.
این مفهوم به روشهای ذخیرهی دادههای چندبعدی و ناهمگن میپردازد. در عمل، بیشترِ دادههای دنیای واقعی اینگونهاند، بنابراین اکنون به قلب آن چیزی میرسیم که R در عمل (بیشتر وقتها) برای آن به کار میرود.
در طول چند دهه، R انواع دادهی متعددی را برای کار با دادههای جدولی افزوده است.
این سرفصل عمدتاً بر tibbleها تمرکز میکند، اما دانستن چند جایگزین دیگر هم مفید است.
data.tabletibbleها (که در ادامه توضیح داده میشوند) و 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ها بخشی اصلی از 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 است.
پیشتر در مفهوم 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
واضح است که 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() استفاده کنید.
نخستین نکته: پیشفرض 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).