مسیرها
/
R
R
/
تمرین‌ها
/
باشگاه پنیر
باشگاه پنیر

باشگاه پنیر

تمرین یادگیری

مقدمه

برنامه‌نویسی تابعی چیست؟

برای هدف ما، این‌ها چند ویژگی کلیدی هستند:

  • توابع خالص، که ورودی‌ها را به‌صورت پیوسته و بدون عوارض جانبی به خروجی نگاشت می‌کنند.
  • داده‌های تغییرناپذیر.
  • توابع درجه‌یک، که می‌توان با آن‌ها مانند هر نوع دیگری به‌عنوان مقدار رفتار کرد.
  • توابع مرتبه‌بالا، که توابع دیگر را به‌عنوان آرگومان یا به‌عنوان مقدار بازگشتی می‌گیرند.

با این حال، R یک زبان برنامه‌نویسی تابعی خالص نیست (برخلاف مثلاً Haskell).

به R به‌عنوان یک زبان عمل‌گرا نگاه کنید، که (روزافزون) قابلیت‌های تابعی را در جایی فراهم می‌کند که راهی تمیز و آسان برای انجام کار هستند. به‌طور کلی، تحلیل داده هدف است و برنامه‌نویسی تنها وسیله‌ای برای رسیدن به آن هدف است.

پایپ‌ها

استفاده از «پایپ»های |> برای اتصال توابع، تکنیکی روزافزون‌رایج در چندین زبان است.

به‌طور خلاصه:

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

برخی برنامه‌نویسان هنگام مرور کد، تلفظ پایپ را به‌صورت «then» مفید می‌دانند: مرحله ۱ را انجام بده، سپس مرحله ۲ را انجام بده...

مثال زیر نحوه‌ی نگارش را نشان می‌دهد.

library(stringr)

"Monday-25-February" |> 
  str_split("-") |>     
  unlist() |>               # [1] "Monday"   "25"       "February"
  str_sub(1, 3) |>          # [1] "Mon" "25"  "Feb"
  str_to_upper() |>         # [1] "MON" "25"  "FEB"
  str_flatten(collapse = ", ")
#> [1] "MON, 25, FEB"

این نوع پایپ در نسخه‌ی 4.1.0 به R پایه اضافه شد و ممکن است ببینید که به آن «پایپ بومی» گفته می‌شود.

ویرایشگر RStudio (در میان دیگران) از Ctrl-Shift-M به‌عنوان میان‌بر صفحه‌کلید برای افزودن پایپ با فاصله‌های مناسب استفاده می‌کند.

توابع apply

پیش‌تر دیدیم که بسیاری از توابع در R «برداری‌شده» هستند: آن‌ها روی کل بردارها عمل می‌کنند، بدون نیاز به نوشتن حلقه‌های صریح.

توابعی که تنها از توابع برداری‌شده استفاده می‌کنند، خودشان به‌طور خودکار برداری‌شده هستند. در یک مثال ساده:

triple <- function(x) return(3 * x)
triple(1:10)
 [1]  3  6  9 12 15 18 21 24 27 30

با این حال، توابع پیچیده‌تر ممکن است ورودی برداری را نپذیرند، مثلاً اگر شامل حلقه‌ها یا بلوک‌های if-else باشند.

ما به روشی عمومی‌تر برای اعمال توابع روی بردارها نیاز داریم (مشابه map() در برخی زبان‌های دیگر).

از آنجا که R در زبان پایه تنوع غنی‌ای از ساختارهای داده دارد، خانواده‌ای کامل از توابع *apply() نیز برای کار با آن‌ها دارد. از نظر فنی‌تر، برنامه‌نویسان اغلب به این‌ها «توابع مرتبه‌بالا» و ریاضی‌دانان «تابعی‌ها» می‌گویند، چون توابع را به‌عنوان آرگومان می‌گیرند.

برای الان، lapply() و sapply() را در نظر بگیرید.

lapply()

«List-apply» برای کار روی ورودی‌های لیست طراحی شده است، اما بردارها در صورت نیاز بی‌سروصدا به لیست تبدیل می‌شوند.

خروجی یک لیست است، بنابراین ممکن است برای گرفتن یک بردار نیاز به بازکردن آن باشد:

h <- function(size) {
    switch(size,
           "small" = 6,
           "medium" = 8,
           "large" = 10
    )
}

v <- c("small", "large", "medium")

h(v) # bad use of an unvectorized function
#> Error in switch(size, small = 6, medium = 8, large = 10) : 
#>   EXPR must be a length 1 vector

lapply(v, h)  # output is a list
#> [[1]]
#> [1] 6

#> [[2]]
#> [1] 10

#> [[3]]
#> [1] 8

lapply(v, h) |> unlist() # output is a vector
#> [1]  6 10  8

کسانی که با مفهوم Switch آشنا هستند ممکن است متوجه شوند که می‌توانیم به‌جای آن، switch اسکالر را با recode_values() برداری‌شده از کتابخانه‌ی dplyr جایگزین کنیم. برداری‌سازی خوب است و R مدرن راه‌های متعددی برای رسیدن به آن فراهم می‌کند.

library(dplyr)

h <- function(size) {
  recode_values(size,
         "small" ~ 6,
         "medium" ~ 8,
         "large" ~ 10
  )
}

v <- c("small", "large", "medium")
h(v)
#> [1]  6 10  8

sapply()

«Simplified-apply» کاری مشابه lapply انجام می‌دهد، اما همچنین تلاش می‌کند مقدار بازگشتی را ساده کند: مثلاً یک بردار نام‌دار به‌جای یک لیست. بسته به زمینه، استفاده از unname() ممکن است لازم باشد:

sapply(v, h)
 small  large medium 
     6     10      8 

sapply(v, h) |> unname()
#> [1]  6 10  8

کتابخانه‌ی purrr

lapply و sapply ساده‌اند، برای بیشتر کاربران R آشنا هستند و همچنان برای بردارها و لیست‌های ساده بسیار محبوب‌اند.

R پایه بسیاری توابع apply دیگر نیز دارد. فراهم کردن جایگزینی ساده‌تر و یکدست‌تر برای این نوع سردرگمی، هدفی کلیدی برای Tidyverse است.

اکنون بسته‌ی purrr وجود دارد که چندین گزینه‌ی اضافی مانند map() را فراهم می‌کند، در حالی که یکدستی در طراحی و شفافیت در پیام‌های خطا را بهبود می‌بخشد.

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

توابع در purrr متنوع و فراوان‌اند و به‌سادگی خلاصه نمی‌شوند. منابعی که به یافتن راه خود در این کتابخانه کمک می‌کنند عبارت‌اند از:

توابع map متعدد

تابع map() یک تابع را روی هر عنصر ورودی اعمال می‌کند و همیشه یک لیست برمی‌گرداند.

تا اینجا، به نظر می‌رسد lapply() است که فقط تغییر نام داده و شاید پیام‌های خطای بهتری دارد.

با این حال، map() تنها یک عضو از خانواده‌ای بزرگ است.

چند پرسش برای اندیشیدن هنگام انتخاب یک تابع:

  • می‌خواهید چه چیزی برگردانید؟ map() همیشه یک لیست برمی‌گرداند، اما اگر نوع خاصی از بردار اتمی می‌خواهید، برای رشته‌ها از map_chr()، برای اعداد صحیح از map_int() و غیره استفاده کنید.
  • آیا به هر اندیس مکانی از ورودی نیاز دارید؟ نام توابع را با i شروع کنید، مانند imap().
  • آیا می‌خواهید چندین ورودی را به‌صورت موازی پردازش کنید؟
    • در نام، map را با map2 عوض کنید، مانند map2_dbl() برای دو ورودی (مثلاً مقادیر و وزن‌ها، برای محاسبه‌ی میانگین وزنی)
    • برای یک لیست از ورودی‌های موازی با pmap جایگزین کنید، مانند pmap_chr(). و بله، موازی‌سازی در سطح سخت‌افزار ممکن است.
  • آیا می‌خواهید همه‌ی عناصر ورودی را پردازش کنید؟ اگر نه، به map_if() برای اعمال یک فیلتر محمولی («منطقی»)، map_at() برای تعیین معیارهای اندیس مکانی، و head_while() برای پردازش ورودی تا وقتی عنصری محمول را برآورده نکند فکر کنید.
  • آیا داده‌های ورودی به‌شدت تودرتو هستند؟ برای آن وضعیت توابعی وجود دارد.
  • آیا می‌خواهید مقدار بازگشتی همیشه از همان نوع ورودی باشد؟ به‌جای map از توابع modify() استفاده کنید، یا برای اعمال بازگشتی از modify_tree().

این توابع هر زمان ممکن، داده‌ی ورودی را به‌عنوان آرگومان اول دارند تا اطمینان حاصل شود که با پایپ‌ها خوب کار می‌کنند.

همچنین، purrr (به‌عمد) سخت‌گیرتر از R پایه است. اگر چیز خاصی بخواهید، توابع دقیقاً همان را برمی‌گردانند یا با پیام خطا متوقف می‌شوند. سخت‌گیرانه، اما در کاهش باگ‌های سخت‌یافت بسیار مؤثر است.

توابع reduce

توابع map به‌طور کلی مقدار بازگشتی با همان طول ورودی دارند.

برخی توابع دیگر ابعاد داده را کاهش می‌دهند، مثلاً sum().

sum(1:100)
#> [1] 5050

در مورد بالا، یک بردار با طول ۱۰۰ را به یک عدد صحیح با طول ۱ تبدیل می‌کنیم. در یک مفهوم آینده درباره‌ی ماتریس‌ها و آرایه‌ها خواهیم دید که مفهوم کاهش ابعاد عمومی‌تر است.

تابع sum() توکار است (همان‌طور که بسیاری از توابع آماری دیگر نیز توکارند). با این حال، ما به روشی نیاز داریم که توابع کاهنده‌ی ابعاد دلخواه را روی یک ساختار داده اعمال کنیم، با استفاده از یک تابع مرتبه‌بالا معادل map().

در چندین زبان دیگر، از نام‌های توابعی مانند fold، foldl و foldr استفاده می‌شود. در R، تابع مربوطه reduce() است که با چارچوب مشهور MapReduce (برای علاقه‌مندان به الگوریتم) مطابقت دارد.

آرگومان اول، مثل همیشه، داده‌ی ورودی است. آرگومان دوم یک تابع دو‌آرگومانی است: اغلب یک تابع بی‌نام، اما ممکن است به‌سادگی یک عملگر حسابی مانند + باشد.

# surround an infix operator with backticks, to use as a function
reduce(1:100, `+`)
#> [1] 5050  # same as sum(1:100)

# anonymous function, alternately add and subtract
reduce(1:10, \(accum, nextval) ifelse(nextval %% 2 == 0, accum + nextval, accum - nextval))
#> [1] 7

آیا تفاوت دارد که تابع را از ابتدا یا از انتهای ورودی اعمال کنیم؟

با عملگرهای شرکت‌پذیر مانند + یا * خیر. واضح است که (1 + 2) + 3 == 1 + (2 + 3).

در مقابل، - و / شرکت‌پذیر نیستند:

(1 - 2) - 3
#> [1] -4
1 - (2 - 3)
#> [1] 2

برای توابعی که جهت در آن‌ها اهمیت دارد، یک آرگومان اختیاری .dir وجود دارد.

# default is .dir = "forward"
reduce(1:10, `-`)
#> [1] -53
reduce(1:10, `-`, .dir = "backward")
#> [1] -5

مانند map() و map2()، تابع reduce2() نیز برای کار روی دو بردار ورودی به‌صورت موازی وجود دارد.

در R، ما کار با بردارها را دوست داریم. فرض کنید به‌جای یک مقدار نهایی واحد، می‌خواهید همه‌ی مراحل میانی را ببینید.

برای این کار، توابع مربوطه accumulate() و accumulate2() هستند. نحوه‌ی نگارش مشابه reduce() است، با چند آرگومان اختیاری اضافی برای تعیین نوع خروجی.

accumulate(1:10, `+`)
#> [1]  1  3  6 10 15 21 28 36 45 55

برخی انواع رایج accumulate توابع خودشان را دارند. مفهوم توابع برداری درباره‌ی cumsum() و cumprod() در R پایه بحث کرد و کتابخانه‌ی dplyr برای میانگین‌های عددی cummean() و برای بردارهای منطقی cumall() و cumany() را دارد.

توابع filter

معمولاً می‌خواهیم عناصری از ورودی را که با یک محمول مطابقت دارند بگنجانیم (یا حذف کنیم).

در مفهوم فیلتر کردن بردار یک روش اصطلاحی برای انجام این کار دیدیم.

v <- 1:3
v[v >= 2]
#> [1] 2 3

این روش در R سنتی بسیار استفاده می‌شود، اما در خطوط پایپ تابعی مدرن خوب جا نمی‌افتد.

در نتیجه، purrr مجموعه‌ای از توابع محمولی را فراهم می‌کند که از قرارداد معمول آرگومان f(input, func, ...) برای کار با پایپ‌ها پیروی می‌کنند.

نام‌ها کمی با بیشتر زبان‌های دیگر متفاوت‌اند (نام‌هایی مانند filter و drop در R معنای کاملاً متفاوتی دارند).

توابع متناظر در purrr شامل keep() و discard() و چند تای دیگر است.

همچنین every()، some() و none() را داریم تا جای all() و any() را در برگرداندن یک مقدار منطقی واحد بگیرند.

دستورالعمل‌ها

ما در حال راه‌اندازی یک باشگاه پنیر هستیم که از ML استفاده می‌کند تا بر اساس سابقه و سلیقه‌ی مشتریان پنیردوستمان، پنیرهای تازه‌ای را برای عرضه انتخاب کند.

اعضای جدید باید یک نظرسنجی اولیه را پر کنند تا ما بتوانیم داده‌های پایه‌ای را برای شروع جمع‌آوری کنیم. از این طریق مشخص شده است که زیرمجموعه‌ای از مشتریان پرشور وجود دارند که در نقدهایشان ظرافت کمی دارند. چون این کار می‌تواند به‌طور جبران‌ناپذیری باعث سوگیری یک الگوریتم دقیق‌تر شود، الگوریتم جداگانه‌ای برای رسیدگی به نیازهای آنان در نظر گرفته شده است. از شما خواسته شده است چند تابع کمکی بنویسید تا داده‌های آنان را سر و سامان دهید.

Note

هرچند ممکن است راه‌های مختلفی برای حل تمرین‌های زیر وجود داشته باشد، هرکدام را می‌توان با حداکثر دو تابع مرتبه‌بالا حل کرد.

1. دسته‌بندی مشتریان

سیستم امتیازدهی بر پایه‌ی پنج ستاره است که به‌سادگی از اعداد صحیح 1:5 تشکیل شده است. مشتریان پرشور فقط امتیاز 1 یا 5 می‌دهند و می‌خواهیم بدانیم آیا یک مشتری چنین رفتاری دارد یا نه.

تابع all_15() را پیاده‌سازی کنید که یک بردار از امتیازها می‌گیرد و اگر همه‌ی امتیازها 1 یا 5 باشند true و در غیر این صورت false برمی‌گرداند.

all_15(c(2, 3, 4, 4, 1))
#> [1] FALSE

all_15(c(1, 5, 5, 1, 5))
#> [1] TRUE

2. اسم‌گذاری مشتریان

باید اسم مشتری (یا شناسه‌ی او) را به امتیازهایش نسبت دهیم.

تابع name_customers() را پیاده‌سازی کنید که یک بردار از اسم‌ها و یک لیست از امتیازها می‌گیرد و آن‌ها را در یک لیست از لیست‌ها «زیپ» می‌کند.

عناصر لیست درونی باید اسم‌های name و rating را داشته باشند.

names <- c("c1", "c2")
ratings <- list(c(2, 3, 5), c(1, 1, 5))
name_customers(names, ratings)
#> [[1]]
#> [[1]]$name
#> [1] "c1"

#> [[1]]$rating
#> [1] 2 3 5

#> [[2]]
#> [[2]]$name
#> [1] "c2"

#> [[2]]$rating
#> [1] 1 1 5

3. جدا کردن مشتریان پرشور

باید مشتریان پرشورتر را از بقیه جدا کنیم.

تابع emphatics() را پیاده‌سازی کنید که مشتریان و امتیازها را می‌گیرد. لیستی برمی‌گرداند که فقط به کسانی محدود است که تنها امتیازهای ستاره‌ای 1 یا 5 می‌دهند.`

# names and ratings as in the task 2 example

emphatics(names, ratings)
#> [[1]]
#> [[1]]$name
#> [1] "c2"

#> [[1]]$rating
#> [1] 1 1 5

4. تبدیل امتیازها به دودویی

از آنجا که مشتریان پرشور فقط از امتیازهای 1 و 5 استفاده می‌کنند، از نظر محاسباتی راحت‌تر است که این‌ها به 0 و 1 تغییر داده شوند.

تابع to_binary() را پیاده‌سازی کنید که یک بردار از امتیازهای پرشور می‌گیرد. امتیازهای دودویی را برمی‌گرداند، که در آن 1 به 0 و 5 به 1 تغییر یافته است.

to_binary(c(1, 1, 5, 5, 1))
#> [1] 0 0 1 1 0

5. محاسبه‌ی میانگین امتیازها در طول زمان

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

تابع satisfactions() را پیاده‌سازی کنید که یک بردار از امتیازها می‌گیرد. یک بردار از میانگین‌های تجمعی امتیازها تا این نقطه را برمی‌گرداند. نتایج را به ۲ رقم اعشار گرد کنید.

امیدواریم این اعداد برای مشتریان خوشحال، همچنان رشد کنند!

satisfactions(c(1, 2, 2, 3))
#> [1] 1.00 1.50 1.67 2.00
ویرایش از طریق GitHub این لینک در پنجره یا زبانه‌ی جدیدی باز می‌شود
R Exercism

آماده‌اید باشگاه پنیر را شروع کنید؟

در Exercism ثبت‌نام کنید تا R را همراه با 21 مفهوم111 تمرین و مربی‌گری انسانی واقعی یاد بگیرید و در آن استاد شوید، همه‌ی این‌ها رایگان.