הקדמה

עכשיו שאתם יודעים די הרבה על תכנות ב R , אנחנו נתמקד בשיעור הזה על דרכים ליעל את העבודה שלנו ב R בעזרת כתיבת ושימוש בפונקציות.

R בבסיסה היא שפה פונקציונאלית - וככל שתשכילו להבין ולדעת איך לנצל את האופי הזה של R כך תוכלו לעשות יותר, ולכתוב קוד יעיל, תמציתי ומובן יותר.


מטרות השיעור

כתבית פונקציות בכל שפה הינה תהליך מורכב עם הרבה פרטים ודקויות

בהתאם לכך המטרה של השיעור איננה להפוך אותם למומחים לכתיבת פונקציונאלית, אלא:

  • להכיר את סוגי הפונקציות השונים
  • לתרגל ברמה בסיסית כתיבת פונקציות
  • להכיר יישומים נפוצים של כתיבת פונקציות ב R

עד סוף השיעור תדעו להשתמש בפונקציות בשביל לייעל את העבודה שלכם


ספרים שימושיים

גילוי נעות:

רוב החומר לשיעור הזה וכן התמונות המוצגים בו לקוחים משני ספרים של Hadley Wickham:

הספר הראשון מומלץ לכולם והספר השני למי שרוצה ממש להבין לעומק איך הדברים עובדים


נקודות כלליות

לפני שנגיע לפרקטיקה יש כמה נקודות כלליות שכדאי שיישבו לנו בראש:

  • הכל ב R זה פונקציה
  • פונקציה הוא אזרח של כבוד
  • Lexical Scoping

הכל ב R זה פונקציה

כל דבר שתראו ב R הוא פונקציה. אין לזה הרבה משמעות מעשית עבורכם (בשלב הזה) אבל כדאי לזכור זאת להמשך.

למשל האופרטור + הוא בעצם פונקציה עטופה בצורה יפה כאשר באמת קוראים לה כך

# This:

3+5 
## [1] 8
# is really this:

`+`(3,5)
## [1] 8

פונקציה הוא אזרח של כבוד

פונקציה הוא אזרח של כבוד - הוא אובייקט בדומה לכל שאר האובייקטים שאתם מכירים :

  • ניתן לשמור אותו
  • ניתן לאגד אותם ברשימה
  • ניתן להדפיס אותו
  • ניתן להעביר אותו
  • ניתן לשנות אותו
  • ועוד…

למי שלא בא משפה פונקציונאלית זה יראה קצת מוזר בהתחלה…


Lexical Scoping

הנושא הזה הוא קצת מתקדם אבל רצוי הבנה בסיסית שלו…


ב R אין מושג של local ו global כפי שיש ברוב השפות הפונקציונאליות.

גם אין מושג של משתני macro או scalar כפי שאתם מכירים.

הכל פשוט - קיים או שהוא לא קיים


אבל, בדומה למושגים הללו, R עובד על שיטת ה Lexical Scoping

בגדול זה אומר שכש R מריץ פונקציה הוא מחפש בסביבה המיידית ואם הוא לא מוצא הוא מחפש בסביבה (environment) היותר רחוקה, וחוזר חלילה עד שהוא מוצא או לא מוצא.


כמשל, אם אתם מחפשים את המפתחות לאוטו שאיבדתם:

  • קודם תחפשו בכיסים
    • ואז בחדר
      • ואז בכל הבית
        • ואז בכל היקום

עד שתמצאו :)


בפרט, בכתיבת פונקציות:

כש R נמצא בתוך פונקציה הוא מחפש את השמות המוכרים לו בתוך העולם של הפונקציה שהוא נמצא בה - רק אם הוא לא מוצא הוא יחפש את אותו שם בסביבה הרחבה יותר.

בהקשר הזה כדאי לכם לקרוא על האופרטור ->>


סוגי פונקציות

בגדול יש 3 סוגים כלליים של פונקציות שנעבוד אתם ב R:

  1. פונקציות שמיות
  2. פונקציות אננונימיות
  3. פונקציות מרמה גבוהה

נתחיל בללמוד על פונקציות שמיות כאשר העקרונות שנלמד יהיו נכונות גם לשאר הסוגים


פונקציות שמיות

מייצרים פונקציה שמית כפי שמייצרים כל אובייקט ב R, עם אופרטור ההשמה.

הצורה הבסיסית להגדרת פונקציה היא בעזרת קריאת לפקודה function בצורה ההבאה:

print_hello <- function(){
                              print("hello world")
                              }

וקוראים לפונקציה כפי שהיינו קוראים לכל אובייקט אחר

print_hello()
## [1] "hello world"

ערך מוחזר מפונקציה

  • פונקציה תמיד תחזיר את תוצאת שורת הקוד האחרונה
  • אפשר לשנות התנהגות זאת ולהחזיר תוצאה אחרת בעזרת הפקודה return
# 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

אם מתייחסים לשמות של עמודות בתוך פונקציות יש כמה אופציות:

  1. ב dplyr לכל פקודה יש מקבילה המסתיימת עם _ היודעת להבין טקסט
    • ככלל רוב הפעולות עובדות בפשטות - חוץ מ mutate שם צריך לרוב להשתמש ב interp
    • interp היא פקודה מספריית lazyeval
    • אם מערבבים באותו שורה עמודה עם טקסט ועמודה בלי צריך לציין זאת בעזרת interp.
  2. תמיד נעדיף לעשות את הפונקציה ב BASE R ולקרוא לפונקציה מתוך שרשור של dplyr

  3. ב 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 לעוד פרטים


פונקציות מסדר גבוה

פונקציות מסדר גבוה היא פונקציה המקבלת בתור פרמטר פונקציה אחרת או מחזיר פונקציה כתוצאה.

בגדול אלה מתחלקים לשני סוגים עיקריים:

  • Closures - פונקציות המחזירות פונקציות לאחר שינוי
  • Functionals - פונקציות המקבלות פונקציות כפרמטר

לסוג השני של פונקציות גבוהות נקדיש פרק נפרד


Closures - פונקציות המחזירות פונקציות

היכולת להגדיר פונקציה מתוך פונקציה הוא דבר מאוד שימושי….

למעשה הוא מאפשר יצירת “מפעלים ליצירת פונקציות”

למשל:

power <- function(exponent) {
  function(x) {
    x ^ exponent
  }
}


square <- power(2)
square(2)
## [1] 4
cubed <- power(3)
cubed(2)
## [1] 8

פונקציות וקטוריות - Functionals

הדבר שהכי הרבה תשתמשו בו ב R הוא פונקציות פונקציונאליות המקבלות בתור פרמטרים פונקציות אחרות.

המשפחה הכי שימושים של פונקציות האלה הוא משפחת ה apply - המקבלת בתור פרמטר:

  1. רשימה של ערכים (list)
  2. פונקציה לביצוע

ומפעילה את הפונקציה עבור כל ערך ב list שהוזן.

אנו נלמד על lapply ונכיר את החברים שלו שפועלים באופן דומה.

לאחר מכן נכיר בקצרה את החבילה purrr העושה אותו דבר בצורה יותר מבוקרת.


lapply

lapply לוקחת בתור פרמטרים רשימה ופונקציה ומחזירה תמיד רשימה.

היא פועלת בערך כך:


שיטות לשימוש ב lapply

כפי שב loop רגיל שאתם מכירים יש 3 דרכים בסיסיים לעבור בתוך ה loop , כך יש 3 דרכים לעבוד עם lapply:

  1. לפי חלקים השייכים לרשימה:
         in a for loop:  for (x in xs)  
         in lapply:      lapply(xs, function(x) {})  
  1. לפי מיקום ברשימה
         in a for loop:  for (i in seq_along(xs))  
         in lapply:      lapply(seq_along(xs), function(i)   {})  
  1. לפי שמות של ערכים ברשימה
         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 תמיד מתחילות בשם הפונקציה בסיומת _ סוג הוקטור שהפונקציה מחזירה. כאשר הסוגים האפשריים הם:

  • chr - character
  • lgl - logical
  • int - integer
  • dbl - numeric double
  • df - data frame

אם אין סיומת זה מחזיר list.

למשל map_dbl מבצעת את פקודת lapply ומחזירה וקטור נומרי.

ככה תמיד יודעים מה נכנס ומה יוצא - ואם יש טעות יודעים על זה מיד


פונקציות מיפוי מסוג 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 שעוזרים לכתוב קוד בצורה יותר מהירה:

  1. שימוש ב ~ ו . במקום בסינטקס של יצירת פונקציה אנונימית

למשל

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

שני הקיצורים הבאים קשורים לבחירת וקטור מתוך רשימה

  1. שימוש בשם של וקטור באופן פשוט במקום בסימן $
  2. שימוש במיקום של וקטור באופן פשוט במקום בסימן $

שני הקיצורים האלה ביחד עם כל פעולה _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 תמיד מחזיר טבלה
  • שימוש ב .$ בשביל לפנות לוקטור