5  מבנה וחיבור נתונים

5.1 מבנה רחב/ארוך

אחת מהפעולות השימושיות היא העברה של דאטה בין סוגי פורמטים: פורמט ארוך ופורמט רחב. לדוגמה כשרוצים שמשתנים מסוימים יהפכו לתצפיות נוספות, או להיפך. נדגים זאת עם הנתונים של penguins, שראינו בפרקים הקודמים. נוסיף שינוי קטן לקובץ שמזהה כל תצפית באמצעות מזהה penguin_id לשם כך אנו משתמשים בפונקציה seq_along שפשוט נותנת וקטור של מספר רץ בהתאם לתצפיות.

5.1.1 רחב לארוך

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

library(tidyverse)
library(palmerpenguins)

penguins_w_id <- penguins %>% 
  mutate(penguin_id = seq_along(species))

penguins_w_id
# A tibble: 344 × 9
   species island    bill_length_mm bill_depth_mm flipper_length_mm body_mass_g
   <fct>   <fct>              <dbl>         <dbl>             <int>       <int>
 1 Adelie  Torgersen           39.1          18.7               181        3750
 2 Adelie  Torgersen           39.5          17.4               186        3800
 3 Adelie  Torgersen           40.3          18                 195        3250
 4 Adelie  Torgersen           NA            NA                  NA          NA
 5 Adelie  Torgersen           36.7          19.3               193        3450
 6 Adelie  Torgersen           39.3          20.6               190        3650
 7 Adelie  Torgersen           38.9          17.8               181        3625
 8 Adelie  Torgersen           39.2          19.6               195        4675
 9 Adelie  Torgersen           34.1          18.1               193        3475
10 Adelie  Torgersen           42            20.2               190        4250
# ℹ 334 more rows
# ℹ 3 more variables: sex <fct>, year <int>, penguin_id <int>
longer_penguins <- penguins_w_id %>% 
  select(penguin_id, species, contains("_mm")) %>% 
  pivot_longer(cols = -c(species, penguin_id),
               names_to = "measurement_type",
               values_to = "measurement_value")

longer_penguins
# A tibble: 1,032 × 4
   penguin_id species measurement_type  measurement_value
        <int> <fct>   <chr>                         <dbl>
 1          1 Adelie  bill_length_mm                 39.1
 2          1 Adelie  bill_depth_mm                  18.7
 3          1 Adelie  flipper_length_mm             181  
 4          2 Adelie  bill_length_mm                 39.5
 5          2 Adelie  bill_depth_mm                  17.4
 6          2 Adelie  flipper_length_mm             186  
 7          3 Adelie  bill_length_mm                 40.3
 8          3 Adelie  bill_depth_mm                  18  
 9          3 Adelie  flipper_length_mm             195  
10          4 Adelie  bill_length_mm                 NA  
# ℹ 1,022 more rows

הארגומנט הראשון קובע את המשתנים שהולכים לעבור “pivot”. במקרה הזה הגדרנו אותו על דרך השלילה (-species אומר שכל המשתנים צריכים להשתתף ב-pivot למעט המשתנה species שנותר כעמודה מופרדת).

הארגומנט names_to קובע את שם העמודה שתכיל את שמות המשתנים במקור, והארגומנט values_to קובע את שם העמודה שתכיל את הערכים שהכילו המשתנים במקור.

5.1.2 ארוך לרחב

הפעולה ההפוכה (הפיכת טבלה ארוכה לטבלה רחבה) יכולה להיעשות באמצעות הפונקציה pivot_wider, באופן הבא:

longer_penguins %>% 
  pivot_wider(id_cols = c(species, penguin_id),
              names_from = measurement_type,
              values_from = measurement_value)
# A tibble: 344 × 5
   species penguin_id bill_length_mm bill_depth_mm flipper_length_mm
   <fct>        <int>          <dbl>         <dbl>             <dbl>
 1 Adelie           1           39.1          18.7               181
 2 Adelie           2           39.5          17.4               186
 3 Adelie           3           40.3          18                 195
 4 Adelie           4           NA            NA                  NA
 5 Adelie           5           36.7          19.3               193
 6 Adelie           6           39.3          20.6               190
 7 Adelie           7           38.9          17.8               181
 8 Adelie           8           39.2          19.6               195
 9 Adelie           9           34.1          18.1               193
10 Adelie          10           42            20.2               190
# ℹ 334 more rows

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

Tip

נסו להריץ את אותן הפקודות (pivot_longer ואז pivot_wider) על penguins (במקום על penguins_id).

מה שונה בפלט של כל אחת מהפקודות, ומדוע?

5.2 חיבור טבלאות

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

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

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

נדגים זאת בדוגמה קטנה שבה שתי טבלאות. בטבלה הראשונה נתונים על ארבעה לקוחות פיקטיביים של חברת אשראי. אנחנו נשתמש בפקודה tribble כדי להגדיר את הטבלה בקוד.

customers <- tribble(~customer_id, ~gender, ~home_address,
                     1, "גבר", "חיפה",
                     2, "אשה", "תל-אביב",
                     3, "גבר", "תל-אביב",
                     4, "אשה", "חיפה")

customers
# A tibble: 4 × 3
  customer_id gender home_address
        <dbl> <chr>  <chr>       
1           1 גבר    חיפה        
2           2 אשה    תל-אביב     
3           3 גבר    תל-אביב     
4           4 אשה    חיפה        

כעת נגדיר טבלה נוספת של הרכישות שביצעו הלקוחות:

transactions <- tribble(~customer_id, ~expense, ~dealer,    
                        1, 80, "Motty's Shawarma", 
                        1, 12, "Marina's coffee",
                        2, 350, "Dekek fuel",
                        4, 35, "Train",
                        4, 12, "Rokadin's Crossons")

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

נשתמש בפקודה left_join שמחברת בין טבלאות. המשתנה המשותף הוא customer_id והוא יאפשר לנו לחבר בין הטבלאות.

customers_transactions <- transactions %>%    
  left_join(customers)
Joining with `by = join_by(customer_id)`
customers_transactions
# A tibble: 5 × 5
  customer_id expense dealer             gender home_address
        <dbl>   <dbl> <chr>              <chr>  <chr>       
1           1      80 Motty's Shawarma   גבר    חיפה        
2           1      12 Marina's coffee    גבר    חיפה        
3           2     350 Dekek fuel         אשה    תל-אביב     
4           4      35 Train              אשה    חיפה        
5           4      12 Rokadin's Crossons אשה    חיפה        

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

customers_transactions %>%    
  group_by(home_address) %>%    
  summarize(total_expense = sum(expense))
# A tibble: 2 × 2
  home_address total_expense
  <chr>                <dbl>
1 חיפה                   139
2 תל-אביב                350

מעבר לפקודה שבה השתמשנו left_join יש עוד פקודות חיבור. הפקודה right_join עושה את אותו הדבר רק במקום לחבר את הטבלה השניה לראשונה היא מחברת את הראשונה לשניה:

customers %>%    
  right_join(transactions)
Joining with `by = join_by(customer_id)`
# A tibble: 5 × 5
  customer_id gender home_address expense dealer            
        <dbl> <chr>  <chr>          <dbl> <chr>             
1           1 גבר    חיפה              80 Motty's Shawarma  
2           1 גבר    חיפה              12 Marina's coffee   
3           2 אשה    תל-אביב          350 Dekek fuel        
4           4 אשה    חיפה              35 Train             
5           4 אשה    חיפה              12 Rokadin's Crossons

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

customers %>%    
  full_join(transactions)
Joining with `by = join_by(customer_id)`
# A tibble: 6 × 5
  customer_id gender home_address expense dealer            
        <dbl> <chr>  <chr>          <dbl> <chr>             
1           1 גבר    חיפה              80 Motty's Shawarma  
2           1 גבר    חיפה              12 Marina's coffee   
3           2 אשה    תל-אביב          350 Dekek fuel        
4           3 גבר    תל-אביב           NA <NA>              
5           4 אשה    חיפה              35 Train             
6           4 אשה    חיפה              12 Rokadin's Crossons

שימו לב, הפקודות הללו מזהות לבד מהם המשתנים החופפים ומחברות בהתאם. ניתן גם להגדיר חפיפות אם שמות המשתנים אינם זהים בין שתי הטבלאות, על ידי שימוש בארגומנט by.

Note

לגבי שיבושי עברית/אנגלית בקוד ובפלט… 😮‍💨

בהתייחס לטבלת ה-customers שבה הגדרנו ערכי שדות בעברית: שימו לב שהשדה gender הוגדר לפני השדה home_address אבל מכיוון שאנחנו משתמשים בעברית בהמשך שורות הקוד, זה נראה “כאילו” התוכן של מגדר מופיע אחרי התוכן של עיר (הפוך).

כמו כן גם הטקסט עצמו בעברית בפלט משתבש.

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

עוד על עברית ב-R תוכלו למצוא בפרק 12.

5.2.1 סינון מטבלה באמצעות join

ניתן להשתמש בפקודות anti_join ו-semi_join על מנת לסנן שורות. למעשה פקודות אלו אינן מחברות נתונים, אלא רק מסננות שורות.

לדוגמה, הקוד הבא משתמש ב-anti_join בשביל לחלץ את הלקוחות שאינם מופיעים בטבלת הפעולות:

customers %>%      
  anti_join(transactions)
Joining with `by = join_by(customer_id)`
# A tibble: 1 × 3
  customer_id gender home_address
        <dbl> <chr>  <chr>       
1           3 גבר    תל-אביב     

לא התווספו שדות לטבלת הלקוחות. קיבלנו רק את לקוח 3 שלא הופיעו עבורו פעולות.

תרגיל: semi_join לעומת anti_join

השתמשו בפקודה semi_join במקום anti_join. מה עושה הפקודה?

:::