עכשיו שאתם יודעים די הרבה על תכנות ב R , אנחנו נתמקד בשיעור הזה על דרכים ליעל את העבודה שלנו ב R בעזרת כתיבת ושימוש בפונקציות.
R בבסיסה היא שפה פונקציונאלית - וככל שתשכילו להבין ולדעת איך לנצל את האופי הזה של R כך תוכלו לעשות יותר, ולכתוב קוד יעיל, תמציתי ומובן יותר.
כתבית פונקציות בכל שפה הינה תהליך מורכב עם הרבה פרטים ודקויות
בהתאם לכך המטרה של השיעור איננה להפוך אותם למומחים לכתיבת פונקציונאלית, אלא:
עד סוף השיעור תדעו להשתמש בפונקציות בשביל לייעל את העבודה שלכם
גילוי נעות:
רוב החומר לשיעור הזה וכן התמונות המוצגים בו לקוחים משני ספרים של Hadley Wickham:
הספר הראשון מומלץ לכולם והספר השני למי שרוצה ממש להבין לעומק איך הדברים עובדים
לפני שנגיע לפרקטיקה יש כמה נקודות כלליות שכדאי שיישבו לנו בראש:
כל דבר שתראו ב R הוא פונקציה. אין לזה הרבה משמעות מעשית עבורכם (בשלב הזה) אבל כדאי לזכור זאת להמשך.
למשל האופרטור +
הוא בעצם פונקציה עטופה בצורה יפה כאשר באמת קוראים לה כך
# This:
3+5
## [1] 8
# is really this:
`+`(3,5)
## [1] 8
פונקציה הוא אזרח של כבוד - הוא אובייקט בדומה לכל שאר האובייקטים שאתם מכירים :
למי שלא בא משפה פונקציונאלית זה יראה קצת מוזר בהתחלה…
הנושא הזה הוא קצת מתקדם אבל רצוי הבנה בסיסית שלו…
ב R אין מושג של local
ו global
כפי שיש ברוב השפות הפונקציונאליות.
גם אין מושג של משתני macro
או scalar
כפי שאתם מכירים.
הכל פשוט - קיים או שהוא לא קיים
אבל, בדומה למושגים הללו, R עובד על שיטת ה Lexical Scoping
בגדול זה אומר שכש R מריץ פונקציה הוא מחפש בסביבה המיידית ואם הוא לא מוצא הוא מחפש בסביבה (environment) היותר רחוקה, וחוזר חלילה עד שהוא מוצא או לא מוצא.
כמשל, אם אתם מחפשים את המפתחות לאוטו שאיבדתם:
עד שתמצאו :)
בפרט, בכתיבת פונקציות:
כש R נמצא בתוך פונקציה הוא מחפש את השמות המוכרים לו בתוך העולם של הפונקציה שהוא נמצא בה - רק אם הוא לא מוצא הוא יחפש את אותו שם בסביבה הרחבה יותר.
בהקשר הזה כדאי לכם לקרוא על האופרטור ->>
בגדול יש 3 סוגים כלליים של פונקציות שנעבוד אתם ב R:
נתחיל בללמוד על פונקציות שמיות כאשר העקרונות שנלמד יהיו נכונות גם לשאר הסוגים
מייצרים פונקציה שמית כפי שמייצרים כל אובייקט ב R, עם אופרטור ההשמה.
הצורה הבסיסית להגדרת פונקציה היא בעזרת קריאת לפקודה function בצורה ההבאה:
print_hello <- function(){
print("hello world")
}
וקוראים לפונקציה כפי שהיינו קוראים לכל אובייקט אחר
print_hello()
## [1] "hello world"
# without return
give_me_x <- function(){
x <-1
x
x <-10+x
x
}
give_me_x()
## [1] 11
# with return
give_me_x <- function(){
x <-1
return(x)
x <-10+x
x
}
give_me_x()
## [1] 1
הזנת ערכים לפונקציה תעשה בצורה ההבאה:
add_values <- function(x,y){
z <- x + y
z
}
add_values(4,5)
## [1] 9
add_values <- function(x=1,y=20){
z <- x + y
z
}
add_values()
## [1] 21
# This is equivelant to x=5
add_values(5)
## [1] 25
add_values(y=5)
## [1] 6
הזנת מספר ערכים בלתי מוגדרים לפונקציה תעשה בעזרת הסימון ...
למשל:
simple_text <- function(x,...){
paste(x, ...)
}
simple_text("hi", "how", "are", "you")
## [1] "hi how are you"
אחד הדברים הכי יעילים ב R זה האפשרות להפעיל פונקציה מורכבת בצורה חד פעמית.
דבר זה שימושי במיוחד ברגע שנתשמש בפונקציות apply למיניהן….
קריאה לפונקציה נעשת באותה צורה רק מבלי להשתמש באופרטור ההשמה:
(function(x) x^2)(4)
## [1] 16
אנחנו נראה את הכוח של זה ממש עוד מעט…
dplyr
ו ggplot2
אם מתייחסים לשמות של עמודות בתוך פונקציות יש כמה אופציות:
_
היודעת להבין טקסט
mutate
שם צריך לרוב להשתמש ב interp
interp
היא פקודה מספריית lazyeval
תמיד נעדיף לעשות את הפונקציה ב BASE R ולקרוא לפונקציה מתוך שרשור של dplyr
ב ggplot2
במקומות בהן היינו כותבים aes
נשתמש במקום זה ב aes_string
ונעביר את שמות העמודות כפרמטר בגרשיים
# option 1: This works fine because we are not mixing variable names and values.
# Wherever we use variable names passed as strings we need the functions ending with _ .
sum_pop_by_col <- function(by_col){
N_month_seg %>%
group_by_(by_col) %>%
summarise(total_pop = sum(n_pop, na.rm = TRUE))
}
h <- sum_pop_by_col("segmento")
# print
h
segmento | total_pop |
---|---|
01 - TOP | 7934 |
02 - PARTICULARES | 65854 |
03 - UNIVERSITARIO | 26212 |
# option 1b: this is a pain - adding a value to a coloumn; because we are mixing variable names with values in the same dplyr function...
library(lazyeval)
add_pop_val <- function(by_col, value){
mut_call <-interp(~(a + b)
, a = as.name(by_col)
, b = as.numeric(value))
N_month_seg %>%
mutate_( .dots = setNames(list(mut_call), "new_col_name"))
}
h <- add_pop_val("n_pop",5)
h <- h %>% select(n_pop, new_col_name)
# print
h %>% slice(1:5)
n_pop | new_col_name |
---|---|
1569 | 1574 |
12448 | 12453 |
4507 | 4512 |
1479 | 1484 |
12614 | 12619 |
# option 2: This is much easier...
add_pop_val <- function(by_col, value){
by_col + value
}
h <- N_month_seg %>%
mutate( new_col_name = add_pop_val(n_pop,5)) %>%
select(n_pop, new_col_name)
# print
h %>% slice(1:5)
n_pop | new_col_name |
---|---|
1569 | 1574 |
12448 | 12453 |
4507 | 4512 |
1479 | 1484 |
12614 | 12619 |
וב ggplot2
אפשר לעשות משהו כזה:
ghist <- function(var){
ggplot(Santander_sample,aes_string(var))+
geom_histogram(fill = "blue", color = "black")
}
ghist("age")
שימו לב להזין את שמות העמודות בגרשיים
ראו vignette של NSE ב dplyr לעוד פרטים
פונקציות מסדר גבוה היא פונקציה המקבלת בתור פרמטר פונקציה אחרת או מחזיר פונקציה כתוצאה.
בגדול אלה מתחלקים לשני סוגים עיקריים:
לסוג השני של פונקציות גבוהות נקדיש פרק נפרד
היכולת להגדיר פונקציה מתוך פונקציה הוא דבר מאוד שימושי….
למעשה הוא מאפשר יצירת “מפעלים ליצירת פונקציות”
למשל:
power <- function(exponent) {
function(x) {
x ^ exponent
}
}
square <- power(2)
square(2)
## [1] 4
cubed <- power(3)
cubed(2)
## [1] 8
הדבר שהכי הרבה תשתמשו בו ב R הוא פונקציות פונקציונאליות המקבלות בתור פרמטרים פונקציות אחרות.
המשפחה הכי שימושים של פונקציות האלה הוא משפחת ה apply
- המקבלת בתור פרמטר:
ומפעילה את הפונקציה עבור כל ערך ב list שהוזן.
אנו נלמד על lapply
ונכיר את החברים שלו שפועלים באופן דומה.
לאחר מכן נכיר בקצרה את החבילה purrr
העושה אותו דבר בצורה יותר מבוקרת.
lapply
לוקחת בתור פרמטרים רשימה ופונקציה ומחזירה תמיד רשימה.
היא פועלת בערך כך:
כפי שב loop רגיל שאתם מכירים יש 3 דרכים בסיסיים לעבור בתוך ה loop , כך יש 3 דרכים לעבוד עם lapply:
in a for loop: for (x in xs) in lapply: lapply(xs, function(x) {})
in a for loop: for (i in seq_along(xs)) in lapply: lapply(seq_along(xs), function(i) {})
in a for loop: for (nm in names(xs)) in lapply: lapply(names(xs), function(nm) {})
כיוון ש dataframe הוא סוג של list המאגדת וקטורים בתור עמודות - אפשר להשתמש בו כדי לבצע פעולות על כל עמודה
# 1. loop over everything
lapply(N_month_seg, is.character)
## $fecha_dato
## [1] FALSE
##
## $segmento
## [1] TRUE
##
## $n_pop
## [1] FALSE
##
## $sum_cc
## [1] FALSE
# 2. loop over places
seq_along(N_month_seg)
## [1] 1 2 3 4
lapply( c(1,3,4) ,
function(i) is.numeric(N_month_seg[[i]])
)
## [[1]]
## [1] FALSE
##
## [[2]]
## [1] TRUE
##
## [[3]]
## [1] TRUE
# 3. loop over names
names(N_month_seg)
## [1] "fecha_dato" "segmento" "n_pop" "sum_cc"
lapply(c("sum_cc", "segmento"),
function(nm) {is.numeric(N_month_seg[[nm]])}
)
## [[1]]
## [1] TRUE
##
## [[2]]
## [1] FALSE
הפונקציה פועלת על הערך הראשון.
אם רוצים לשנות ערך אחר אפשר בעזרת פונקציה אנונימית.
# regular option
x <- list(a = seq(0,100, by=5), b = rnorm(100))
lapply(x, function(x) mean(x, na.rm = T))
## $a
## [1] 50
##
## $b
## [1] 0.06809009
# another option
lapply(c(2,5),function(x) mean(rnorm(1000, mean = x)))
## [[1]]
## [1] 1.997085
##
## [[2]]
## [1] 4.970159
funcs <- c(mean, max, min)
lapply(funcs, function(f){f(N_month_seg$sum_cc, na.rm = T)})
## [[1]]
## [1] 55.75
##
## [[2]]
## [1] 199
##
## [[3]]
## [1] 11
lapply
אם רוצים שהפונקציה תחזיר וקטור במקום רשימה משתמשים באחד הפקודות הבאות:
sapply
- מחזיר וקטור מהסוג שהכי נראה לו לנכוןvapply
- מחזיר וקטור מהסוג המוגדר בפונקציהאבל אנחנו נעדיף להשתמש בחבילת purrr
purrr
חבילת purrr
נועדה להוסיף קונסיסטנטיות לפונקציות הבסיסיות של R.
בגלל ש R לא תמיד צפוי ביחס לסוג הוקטור שהוא מחזיר מהפעולות הוקטוריות המתוארים לעיל - מומלץ לעשות שימוש בפונקציות מספריית purrr
שהן הרבה יותר מקפידות על כללים של מה נכנס ומה יוצא….
בהמשך המצגת יעשה שימוש בפונקציות של purrr
כשבכל קטע תהיה הפניה לפונקציה המקבילה ב BASE R.
הפקודות של purr
תמיד מתחילות בשם הפונקציה בסיומת _
סוג הוקטור שהפונקציה מחזירה. כאשר הסוגים האפשריים הם:
אם אין סיומת זה מחזיר list
.
למשל map_dbl
מבצעת את פקודת lapply
ומחזירה וקטור נומרי.
ככה תמיד יודעים מה נכנס ומה יוצא - ואם יש טעות יודעים על זה מיד
הפקודה המקבילה ל lapply
היא map
כאשר היא מחזירה list
.
אם מוסיפים סיומת אחרת היא תחזיר וקטור מהסוג הרצוי.
למשל:
library(purrr)
x <- list(a = seq(0,100, by=5), b = rnorm(100))
# return list
map(x, function(x) mean(x, na.rm = T))
## $a
## [1] 50
##
## $b
## [1] 0.2706669
# return numeric vector
map_dbl(x, function(x) mean(x, na.rm = T))
## a b
## 50.0000000 0.2706669
# return data frame
map_df(x, function(x) mean(x, na.rm = T))
a | b |
---|---|
50 | 0.2706669 |
# return character vector
map_chr(x, function(x) mean(x, na.rm = T))
## a b
## "50.000000" "0.270667"
כל זה טוב ויפה - אבל מה אם רוצים להפעיל פונקציה על יותר משני וקטורים במקביל?
לצורך זה משתמשים באחד הפקודות:
map2
- למיפוי על 2 וקטורים במקבילmap2(.x, .y, .f, …)
או
pmap
- למיפוי על מספר וקטורים במקבילpmap(.l1, .l2, .l3, ..ln, .f, …)
כאשר .f היא פונקציה וכל מה שבא לפני הפונקציה מובן כרשימה לעבור לעיבוד מקביל לפונקציה
למשל:
mu <- list(5, 10, -3)
sigma <- list(1, 5, 10)
map2(mu, sigma, rnorm, n = 5) %>% str()
## List of 3
## $ : num [1:5] 7.07 5.58 4.43 5.6 4.53
## $ : num [1:5] 10.72 11.34 13.55 6.97 7.57
## $ : num [1:5] -14.08 -10.45 -11.73 1.78 3.71
בתמונה זה מה שקרה
פקודות מקבילות ב R BASE:
map
mapply
אם רוצים לבצע פעולה כזו בהינתן תנאי מסוים נשתמש באחד מהפקודות:
keep
- לבצע על חלקים מהרשימה העומדים בתנאיdiscard
- לבצע על חלקים מהרשימה שלא עומדים בתנאיdetect
- החזר ערך ראשון העומד בתנאיdetect_index
- החזר מיקום ערך ראשון העומד בתנאיhead_while
- החזר n ערכים ראשונים העומדים בתנאיtail_while
- החזר n ערכים אחרונים העומדים בתנאי# before
names(N_month_seg)
## [1] "fecha_dato" "segmento" "n_pop" "sum_cc"
# after1
keep(N_month_seg, is.numeric) %>% str()
## Classes 'tbl_df', 'tbl' and 'data.frame': 15 obs. of 2 variables:
## $ n_pop : int 1569 12448 4507 1479 12614 4510 1547 13316 5361 1616 ...
## $ sum_cc: int 45 199 15 50 NA NA 37 NA NA 41 ...
# after2
discard(N_month_seg, is.numeric) %>% names()
## [1] "fecha_dato" "segmento"
פקודות מקבילות ב R BASE:
Filter
Find
Position
לפעמים אנו רוצים לבצע פונקציה בצורה איטרטיבית - כל פעם לפי זוגות.
כלמור, לבצע על שני הערכים הראשונים, ואת התוצאה לבצע עם הערך השלישי וחוזר…
במקרה כזה נשתמש ב:
reduce
- בצע פעולה באופן איטרטיבי ותחזיר את התוצאה הסופיתaccumulate
- בצע פעולה באופן איטרטיבי ותחזיר גם את תוצאות הבינייםשימוש אחד נפוץ הוא לחבר טבלאות עם אותו מפתח, בזה אחר זה:
c(1:10)
## [1] 1 2 3 4 5 6 7 8 9 10
reduce(c(1:10),sum)
## [1] 55
accumulate(c(1:10),sum)
## [1] 1 3 6 10 15 21 28 36 45 55
פקודה מקבילה ב R BASE:
Reduce
Aggregate
לפעמים נרצה מספר פונקציות על וקטור מסוים של נתונים.
נעשה זאת בעזרת הפקודה:
invoke_map
- בצע מספר פקודות עם פרמטרים משתנים על וקטורגם כאן התוצה תלויה בסיומת הפקודה
f <- c("runif", "rnorm", "rpois")
param <- list(
list(min = -1, max = 1),
list(sd = 5),
list(lambda = 10)
)
invoke_map(f, param, n = 5) %>% str()
## List of 3
## $ : num [1:5] -0.497 0.772 -0.847 0.121 0.291
## $ : num [1:5] 1.36 -6.54 -2.16 1.62 2.25
## $ : int [1:5] 9 12 11 13 5
וזה מה שזה עשה
הרבה פעמים נרצה לבצע פונקציה לשם פעולת הלוואי שהיא מבצעת ולא לצורך החזרת תוצאה מסוימת. למשל, אם נרצה גרף לכל קבוצה בנפרד, או לשמור גרף לכל קבוצה בנפרד.
במקרים כאלה עדיף להשתמש באחד הפקודות הבאות (הפעם אין צורך בסיומת)
walk
- בצע על כל ערך ברשימהwalk2
- בצע על כל ערך בשתי רשימות במקבילpwalk
- בצע על כל ערך במספר רשימות במקבילהיתרון בשימוש בהן הוא שאם רוצים לשמור לאובייקט - אז הוא גם מחזיר את הנתונים ששומשו לצורך ביצוע הפעולה - אחרת הוא לא מחזיר אותן…
walk(c("renta","age"), function(x)hist(Santander_sample[[x]]))
purrr
יש שלושה קיצורי דרך שימושיים ב purrr
שעוזרים לכתוב קוד בצורה יותר מהירה:
~
ו .
במקום בסינטקס של יצירת פונקציה אנונימיתלמשל
x <- list(a = seq(0,100, by=5), b = rnorm(100))
# Instead of:
map_dbl(x, function(x) mean(x, na.rm = T))
## a b
## 50.00000000 -0.05659641
# Use:
map_dbl(x, ~mean(., na.rm = T))
## a b
## 50.00000000 -0.05659641
שני הקיצורים הבאים קשורים לבחירת וקטור מתוך רשימה
$
$
שני הקיצורים האלה ביחד עם כל פעולה _map
שתרצו, מאפשרים שליפה מהירה ויעילה של חלקי רשימות.
לדוגמא:
x <- list(list(1, n = 2, 3), list(4, n = 5, 6), list(7, n = 8, 9))
x %>% map_dbl(2)
## [1] 2 5 8
# or
x %>%
map_chr("n")
## [1] "2.000000" "5.000000" "8.000000"
purrr
אין ב prurr
פונקציות וקטוריות שפועלים על מטריצות בשביל זה תצטרכו להשתמש בפקודות מ R BASE כגון:
sweep
outer
apply
פקודה אחרת נפוצה היא tapply , שבמהותו עושה מה שלמדתם לעשות בעזרת dplyr בפקודות group_by
ו summarise
:
tapply(N_month_seg$n_pop, N_month_seg$segmento, mean)
## 01 - TOP 02 - PARTICULARES 03 - UNIVERSITARIO
## 1586.8 13170.8 5242.4
יש הרבה מאוד נושאים ודקויות שלא נגענו בהן. רובן קשורים לשימוש באובייקטים מסוג רשימה list .
אפנה את צומת לבכם לדרך יעילה יותר, אך מעט מורכבת של לעבוד עם טבלאות המכילות עמודות בהן שמורות רשימות.
מי שמעוניין שיחפש ב tidyr שימוש ב nest
פקודות אחרות שימושיות הקשורות לרשימות:
safely
- תנסה בלי להפיל את הפקודהpossibly
- תנסה בלי להפיל את הפקודה - תחזיר ערך ברירת מחדלtranspose
- תחליף הירארכיה ברשימהאם תבינו לעומק איך לעבוד עם רשימות זה יקדם אתכם הרבה
dplyr
עוד נושאים שלא הספקנו לגעת בהן באופן פורמלי:
do
ביחד עם dplyr
לעשות פעולה שרירותיתrowwise
ב dplyr
לעשות חישוב לפי שורות ולא עמודותdplyr
תמיד מחזיר טבלה.$
בשביל לפנות לוקטור