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