9  לולאות מיוחדות

בפרק זה נלמד על השימוש בחבילת purrr לצורך בניית לולאות. הפונקציות שזמינות לנו בחבילה זו מחליפות את רוב השימושים בהם נרצה להפעיל את for (שהוזכרה בפרק 2).

היתרונות בשימוש בפונקציות מחבילת purrr על פני לולאות for הינם:

הפונקציה המרכזית ב-purrr היא map. היא מקבלת שני ארגומנטים: האובייקט שעליו רוצים להריץ את הלולאה (וקטור או רשימה), והפונקציה שרוצים להריץ על אך איבר באובייקט שהוכנס בארגומנט הראשון.

נראה דוגמה לשימוש ב-map על גבי הדאטה של penguins ליצירת שלושה מודלי רגרסיה לינארית, עבור כל זן פינגויינים.

9.1 סידור הדאטה

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

library(tidyverse)
library(palmerpenguins)

penguins_nested <- penguins %>% 
  select(contains("mm"), species, contains("mass"), sex) %>% 
  group_by(species) %>% 
  nest()

glimpse(penguins_nested)
Rows: 3
Columns: 2
Groups: species [3]
$ species <fct> Adelie, Gentoo, Chinstrap
$ data    <list> [<tbl_df[152 x 5]>], [<tbl_df[124 x 5]>], [<tbl_df[68 x 5]>]

בעמודה האחרונה שנוצרה לנו בטבלת penguins_nested (שנקראת data), קיבלנו את הדאטה עבור כל אחד מסוגי הפינגויינים (זו למעשה טבלה בת שלוש שורות ושתי עמודות, העמודה השניה מכילה טבלאות עם המשתנים המקוריים שהיו בדאטה, למעט משתנה ה-Species).

9.2 הדגמה לבניית מודלים

כעת נבנה מודל רגרסיה לינארית, לחיזוי bill_length_mm על בסיס כל יתר המשתנים:

penguins_nested_lm <- map(penguins_nested$data,
                          function(x){lm(bill_length_mm ~ ., data = x)})

כעת קיבלנו שלושה מודלים, הנה המודל הראשון:

summary(penguins_nested_lm[[1]])

Call:
lm(formula = bill_length_mm ~ ., data = x)

Residuals:
    Min      1Q  Median      3Q     Max 
-6.3921 -1.4294 -0.0276  1.4291  5.3977 

Coefficients:
                    Estimate Std. Error t value Pr(>|t|)    
(Intercept)       26.3151542  5.9906398   4.393 2.18e-05 ***
bill_depth_mm     -0.0121755  0.1868468  -0.065   0.9481    
flipper_length_mm  0.0388034  0.0305910   1.268   0.2067    
body_mass_g        0.0011487  0.0006183   1.858   0.0653 .  
sexmale            2.1964106  0.5473780   4.013 9.71e-05 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 2.124 on 141 degrees of freedom
  (6 observations deleted due to missingness)
Multiple R-squared:  0.3814,    Adjusted R-squared:  0.3639 
F-statistic: 21.74 on 4 and 141 DF,  p-value: 5.464e-14
תרגיל למחשבה

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

select(contains("mm"), species, contains("mass"), sex)

מתקבלת הודעת שגיאה. איזה משתנה יצר את הבעיה ומדוע?

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

שימו לב שבתוך פקודת ה-map הגדרנו את הפונקציה בארגומנט השני, בתוך הקוד “inline”.

ניתן היה להגדיר פונקציה מחוץ ל-map ואז פשוט להכניס את השם שלה, כך:

create_model <- function(x){lm(bill_length_mm ~ ., data = x)}
penguins_nested_lm <- map(penguins_nested$data, create_model)

כמו כן, סינטקס נוסף להגדרת פונקציות ב-“inline” הוא שימוש בתו ~, באופן הבא:

penguins_nested_lm <- map(penguins_nested$data,
                          ~ {lm(bill_length_mm ~ ., data = .x)})

שימו לב בחלק השני לשימוש ב-.x אשר מסמל בהגדרה זו היכן יש להכניס את הרכיב עצמו מתוך האובייקט עליו מבצעים את הלולאה.

9.3 סידור תוצאות המודלים

ניתן להשתמש בפקודה broom::tidy על מנת לקבל את תוצאות הפקודה באופן מסודר. במקרה זה נשתמש בסינטקס מלא של tidyverse ונטמיע את השימוש בפקודה map בתוך פקודת mutate שמופעלת על עמודת ה-data שראינו קודם.

penguins_nested_lm_tidy <- penguins_nested %>% 
  mutate(linear_model_res = map(data, 
                                function(x){
                                  lm(bill_length_mm ~ ., data = x) %>% 
                                    broom::tidy()
                                  })) %>% 
  unnest(linear_model_res)

penguins_nested_lm_tidy
# A tibble: 15 × 7
# Groups:   species [3]
   species   data               term        estimate std.error statistic p.value
   <fct>     <list>             <chr>          <dbl>     <dbl>     <dbl>   <dbl>
 1 Adelie    <tibble [152 × 5]> (Intercept) 26.3      5.99        4.39   2.18e-5
 2 Adelie    <tibble [152 × 5]> bill_depth… -0.0122   0.187      -0.0652 9.48e-1
 3 Adelie    <tibble [152 × 5]> flipper_le…  0.0388   0.0306      1.27   2.07e-1
 4 Adelie    <tibble [152 × 5]> body_mass_g  0.00115  0.000618    1.86   6.53e-2
 5 Adelie    <tibble [152 × 5]> sexmale      2.20     0.547       4.01   9.71e-5
 6 Gentoo    <tibble [124 × 5]> (Intercept)  0.611    8.64        0.0707 9.44e-1
 7 Gentoo    <tibble [124 × 5]> bill_depth…  0.674    0.339       1.99   4.90e-2
 8 Gentoo    <tibble [124 × 5]> flipper_le…  0.133    0.0466      2.86   5.11e-3
 9 Gentoo    <tibble [124 × 5]> body_mass_g  0.00150  0.000727    2.07   4.09e-2
10 Gentoo    <tibble [124 × 5]> sexmale      0.525    0.731       0.719  4.74e-1
11 Chinstrap <tibble [68 × 5]>  (Intercept) 29.4     10.8         2.72   8.49e-3
12 Chinstrap <tibble [68 × 5]>  bill_depth…  0.807    0.410       1.97   5.36e-2
13 Chinstrap <tibble [68 × 5]>  flipper_le… -0.00532  0.0573     -0.0929 9.26e-1
14 Chinstrap <tibble [68 × 5]>  body_mass_g  0.00114  0.00106     1.07   2.88e-1
15 Chinstrap <tibble [68 × 5]>  sexmale      2.75     0.889       3.09   2.94e-3

בתוצאת הטבלה ניתן לראות 15 שורות, עבור כל מין של פינגויינים יש חמש שורות כאשר כל שורה מתייחסת לערכיו של משתנה ספציפי (מקדם, טעות תקן, ערך הסטטיסטי, ורמת מובהקות). ניתן לסדר את הטבלה לפורמט רחב באופן הבא, נניח שיציג מקדמים ורמת מובהקות בלבד:

penguins_nested_lm_tidy_wide <- penguins_nested_lm_tidy %>% 
  select(species, term, estimate, p.value) %>% 
  pivot_wider(id_cols = species, names_from = term, values_from = c(estimate, p.value))

penguins_nested_lm_tidy_wide
# A tibble: 3 × 11
# Groups:   species [3]
  species   `estimate_(Intercept)` estimate_bill_depth_mm estimate_flipper_len…¹
  <fct>                      <dbl>                  <dbl>                  <dbl>
1 Adelie                    26.3                  -0.0122                0.0388 
2 Gentoo                     0.611                 0.674                 0.133  
3 Chinstrap                 29.4                   0.807                -0.00532
# ℹ abbreviated name: ¹​estimate_flipper_length_mm
# ℹ 7 more variables: estimate_body_mass_g <dbl>, estimate_sexmale <dbl>,
#   `p.value_(Intercept)` <dbl>, p.value_bill_depth_mm <dbl>,
#   p.value_flipper_length_mm <dbl>, p.value_body_mass_g <dbl>,
#   p.value_sexmale <dbl>
glimpse(penguins_nested_lm_tidy_wide)
Rows: 3
Columns: 11
Groups: species [3]
$ species                    <fct> Adelie, Gentoo, Chinstrap
$ `estimate_(Intercept)`     <dbl> 26.3151542, 0.6106378, 29.3903439
$ estimate_bill_depth_mm     <dbl> -0.01217552, 0.67352164, 0.80659313
$ estimate_flipper_length_mm <dbl> 0.038803444, 0.133148448, -0.005321531
$ estimate_body_mass_g       <dbl> 0.001148724, 0.001504786, 0.001138851
$ estimate_sexmale           <dbl> 2.196411, 0.525357, 2.752421
$ `p.value_(Intercept)`      <dbl> 2.184163e-05, 9.437902e-01, 8.492353e-03
$ p.value_bill_depth_mm      <dbl> 0.94813641, 0.04901556, 0.05364594
$ p.value_flipper_length_mm  <dbl> 0.206723627, 0.005109499, 0.926275133
$ p.value_body_mass_g        <dbl> 0.06529386, 0.04085049, 0.28771299
$ p.value_sexmale            <dbl> 9.707824e-05, 4.738359e-01, 2.935314e-03

9.4 הדגמה לויז’ואליזציות

כעת נדגים יצירה של ויז’ואליזציות עבור התפלגות שאריות המודלים, גם תוך שימוש ב-map.

ראשית נגדיר פונקציה אשר מייצרת ויז’ואליזציה לשאריות בהתבסס על המודל. ראינו דוגמה כזו בפרק 7:

plot_residuals <- function(lm_res){
  ggplot(tibble(resid = lm_res$residuals)) + 
    geom_qq(aes(sample = resid)) + 
    ggtitle("Model Residuals qq-plot")
}

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

penguins_nested_resid_plots <- map(penguins_nested_lm,
                                   plot_residuals)

penguins_nested_resid_plots
[[1]]

[[2]]

[[3]]
תרגיל: הצגת התפלגות השאריות בתרשים אחד

כתבו קוד, תוך שימוש ב-map לצורך סידור השאריות של המודלים, על מנת לייצר את התרשים הבא:

9.5 פונקציות נוספות בחבילת purrr

פונקצית map היא אולי בין הפונקציות השימושיות ביותר בחבילת purrr, אך ישנן מספר פונקציות נוספות שימושיות:

  • הפונקציה walk שימושית כאשר רוצים להריץ לולאה לא בשביל תוצאת החישוב אלא בשביל הפעולה של הפונקציה. לדוגמה, במקרה שרוצים להפיק דוחות מרובים של RMarkdown, או אם נניח רוצים לשלוח הרבה מיילים באוטומציה (הפונקציה הבסיסית שולחת מייל והוקטור עליו רצים הוא וקטור של כתובות מייל);

  • הפונקציה map2 והפונקציה pmap מאפשרות להריץ לולאה על פני מספר רכיבים. הלולאה רצה על השילובים הקיימים של הרכיבים לפי הסדר (אבל לא על ההצלבה ביניהם);

  • פונקציה נוספת שימושית היא list_rbind כאשר הפונקציה map מחזירה רשימה שבה רכיבים בודדים ורוצים להפוך אותה לוקטור של ערכים (לדוגמה, כחלק מסינטקס של tidyverse);

  • ארגומנט נוסף שימושי בפונקציה map הוא progress המאפשר להציג את ההתקדמות של הלולאה (“progress bar”). מספיק להוסיף ארגומנט progress=True והפקודה תציג את ההתקדמות, בהנחה שהפעולה לוקחת מספיק זמן.

9.6 סיכום

בפרק זה למדנו על השימוש בחבילת purrr ובפרט בפונקציה map בחבילה זו. השימוש בחבילה זו מאפשר לנו לבנות לולאות אשר נראות מסודרות יותר מלולאות for, בעלות פוטנציאל לרוץ מהר יותר, ומשתלבות יפה בסינטקס של tidyverse. בפרק ראיתם “טעימה קטנה” מהשימוש בפונקציות, אך ככל שתיתקלו במקרים כאלו ותנסו את הפונקציות הללו תיווכחו לראות שהן מועילות מאוד. נדרש קצת זמן להתרגל אליהן, אך זה שווה את המאמץ.