2  תחביר בסיסי

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

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

כדי לתרגל את הפקודות שתלמדו בפרק זה (ובפרקים הבאים) מומלץ לפתוח חלון של RStudio ולנסות את הפקודות השונות תוך כדי שאתם קוראים את הפרק.

2.1 השמת משתנים, פעולות אריתמטיות ופונקציות

ניתן להריץ ב-R פעולות אריתמטיות (חיבור, חיסור, כפל, חילוק), פונקציות, ולהגדיר משתנים שונים. לדוגמה, הקוד הבא מגדיר משתנה a משתנה b ומכניס את הסכום שלהם למשתנה חדש שיקרא a_plus_b.

a <- 5
b <- 3
a_plus_b <- a + b
a_plus_b
[1] 8

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

a_plus_b = a + b  # this form of assignment `=` is less common, don't use it (use `<-`)

2.1.1 קביעת שמות משתנים

קודם השתמשנו בשמות a, b , ו-a_plus_b כדי לקבוע משתנים. ככלל, מומלץ להשתמש בשמות קצרים בעלי משמעות. שמות משתנים חייבים להתחיל באות באנגלית, ויכולים להכיל אותיות, מספרים, קו תחתון, ונקודה. לדוגמה gender, age, raw_data, וכו’.

2.1.2 סוגי משתנים

בבסיס השפה יש כמה סוגי משתנים, שקובעים מה סוג הערכים שהמשתנה יכול לקבל:

  • מספר שלם (Integer)

  • מספר רציף (Double)

  • מחרוזת (Character)

  • משתנה קטגוריות (Factor)

  • תאריך (Date)

  • משתנה לוגי (Logical)

כל משתנה חדש אנחנו מגדירים ב-R הוא למעשה וקטור. אגב, גם כשאנחנו מגדירים משתנה כערך בודד, בעצם הוא וקטור עם איבר אחד. אנחנו יכולים להשתמש בפקודה c (קיצור של המילה combine) כדי לשלב וקטורים שונים.

נראה דוגמאות להגדרות של וקטורים מסוגים שונים.

some_integer <- c(1L, 2L, 3L)  # The L sign stands for "Long integer"
some_integer
[1] 1 2 3
some_double <- c(1, 2, pi, exp(1))
some_double
[1] 1.000000 2.000000 3.141593 2.718282
some_character <- c("This", "is", "a", "character", "vector")
some_character
[1] "This"      "is"        "a"         "character" "vector"   
some_factor <- factor(c("Apples", "Oranges", "Paers", "Mangos", "Apples", "Oranges"))
some_factor
[1] Apples  Oranges Paers   Mangos  Apples  Oranges
Levels: Apples Mangos Oranges Paers
some_date <- c(Sys.Date(), as.Date("1993-08-01"))
some_date
[1] "2024-10-06" "1993-08-01"
some_logical <- c(TRUE, FALSE, FALSE, TRUE) # can also use c(T, F, F, T) is the same
some_logical
[1]  TRUE FALSE FALSE  TRUE

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

שימוש בפקודה typeof(some_variable) יציג את סוג המשתנה.

typeof(some_integer)
[1] "integer"
typeof(some_double)
[1] "double"
typeof(some_character)
[1] "character"
typeof(some_date)
[1] "double"
typeof(some_factor)
[1] "integer"
typeof(some_logical)
[1] "logical"

ניתן לשים לב ש-R מחשיב את המשתנה הקטגוריאלי כמספר שלם (integer) ואת התאריך כמספר רציף (double).

תרגיל: חיבור של וקטורים מסוגים שונים

באמצעות הפקודה c והפקודה typeof בדקו מה קורה כאשר מחברים משתנים מסוגים שונים אחד לשני. האם התוצאה הגיונית? מה ההיגיון? האם יש מקרים בהם התוצאה של חיבור משתנים עשויה להטעות?

בדקו את c(some_factor, some_character) ודוגמאות נוספות.

2.1.3 קריאה לתתי וקטורים

ב-R ניתן לקרוא לחלק מסוים מתוך וקטור. לדוגמה, אם אנחנו רוצים רק את שני האיברים הראשונים מתוך הוקטור some_factor או את האיבר הראשון והרביעי מתוך some_character נשתמש בכתיב:

some_factor[1:2]
[1] Apples  Oranges
Levels: Apples Mangos Oranges Paers
some_character[c(1,4)]
[1] "This"      "character"

2.2 רשימה (list)

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

my_list <- list(my_int = some_integer,
                my_double = some_double,
                my_character = some_character,
                my_factor = some_factor,
                my_date = some_date)
my_list
$my_int
[1] 1 2 3

$my_double
[1] 1.000000 2.000000 3.141593 2.718282

$my_character
[1] "This"      "is"        "a"         "character" "vector"   

$my_factor
[1] Apples  Oranges Paers   Mangos  Apples  Oranges
Levels: Apples Mangos Oranges Paers

$my_date
[1] "2024-10-06" "1993-08-01"
typeof(my_list)
[1] "list"

כדי לקרוא לוקטור מסוים מתוך רשימה ניתן להשתמש ב-$ באופן הבא:

my_list$my_int
[1] 1 2 3

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

my_data <- data.frame(name = c("Danny", "Moshe", "Iris", "Ronit"),
                      favorite_fruit = factor(c("Mango", "Apple", "Apple", "Paer")),
                      age = c(25L, 32L, 22L, 30L),
                      height = c(1.8, 1.75, 1.6, 1.68),
                      married = c(F, T, F, T))
my_data
   name favorite_fruit age height married
1 Danny          Mango  25   1.80   FALSE
2 Moshe          Apple  32   1.75    TRUE
3  Iris          Apple  22   1.60   FALSE
4 Ronit           Paer  30   1.68    TRUE
typeof(my_data)
[1] "list"

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

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

נסו להריץ את הקוד הבא, ולאחר מכן לענות על השאלות שלאחר מקטע הקוד. יש לשים לב שעל מנת להריץ את הפקודות בסוף המקטע (שקשורות ב-my_data נדרש קודם להגדיר את my_data כפי שהוגדר במקטע הקוד הקודם.

log(100)  # natural logarithm
log10(100)  # base 10 logarithm 
sin(pi)  # sin(pi) is 0 but may give you a surprising answer, why?
sqrt(4)  # square root of 4
mean(my_data$age)
sd(my_data$age)
summary(my_data)
חשיבה על דיוק

שאלה למחשבה: בחלק מהמחשבים התשובה שמתקבלת ל-sin(pi) שונה מ-0.

לדוגמה

> sin(pi)
[1] 1.224606e-16

למה לדעתך?

הפקודה summary

הפקודה האחרונה שהרצנו בדוגמה היא פקודת summary. מה עושה הפקודה summary עבור כל סוג עמודה שהיא מוצאת בdata.frame?

ככלל, הפעלת פונקציה ב-R תיראה כך:

# some code which defines the variable `bar` and then
some_result <- some_function(foo = bar)

# or simply
some_result <- some_function(bar)

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

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

# define a new function which adds a number
one_plus <- function(number){
  number + 1
}
# use the function:
one_plus(1)
[1] 2
one_plus(one_plus(1))
[1] 3

2.4 אופרטורים

אופרטורים משמשים כדי להגדיר תנאים לוגיים שונים, לדוגמה אם אנחנו רוצים לבדוק את נכונותם של שני תנאים או יותר. ב-R נשתמש בתו כפול || כדי לציין “או” לוגי (or), ונשתמש בתו כפול && על מנת לציין “וגם” לוגי (and).

a <- 5
b <- 6

(a < 3) && (b >= 3)
[1] FALSE
(a >= 5) || (b > 10)
[1] TRUE

ישנם גם פעולות לוגיות וקטוריות: כמו שניתן לחבר שני וקטורים, ניתן גם לבצע פעולות לוגיות איבר-איבר. פעולות אלו מבוצעות על ידי תו בודד: | או &.

v1 <- c(T, T, F, F)
v2 <- c(T, F, T, F)

v1 | v2
[1]  TRUE  TRUE  TRUE FALSE
v1 & v2
[1]  TRUE FALSE FALSE FALSE
אופרטור || ו-&&

נסו לבחון מה קורה במקרה של v1 || v2 או v1 && v2. מה החוקיות?

אופרטור נוסף הוא אופרטור השלילה (not), נשתמש בתו ! על מנת לייצג אותו. לדוגמה:

!c(T, F)
[1] FALSE  TRUE
!v1
[1] FALSE FALSE  TRUE  TRUE
a==5
[1] TRUE
!(a==5)
[1] FALSE

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

a == b
[1] FALSE
c(1, 2, 3) == c(2, 1 , 3)
[1] FALSE FALSE  TRUE

ישנם אופרטורים נוספים ב-R, אך בינתיים נסתפק באופרטורים שצוינו לעיל. כעת, לאחר שלמדנו על אופרטורים לוגיים, באפשרותנו ללמוד על התניות (if cluases) ועל לולאות (loops).

2.5 התניות

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

המבנה הכללי של התניות יראה כך:

if (condition1) {
  # some code which evaluates if condition1 == TRUE
} else if (condition2) {
  # some code which evaluates if condition1 == FALSE and condition2 == TRUE
} else {
  # some code which evaluates if condition1 == FALSE and condition2 == FALSE
}

על מנת להדגים, נשתמש בדוגמה הבאה: נניח שאנחנו רוצים לבדוק אם ערך מסוים הוא מספר או לא. נוכל להשתמש בשילוב של if והפונקציה is.numeric:

some_value <- 100

if (is.numeric(some_value)) {
  cat("This is indeed a numeric value!")
} else {
  cat("This is not a numeric value!")
}
This is indeed a numeric value!
some_value <- "foobar"

if (is.numeric(some_value)) {
  cat("This is indeed a numeric value!")
} else {
  cat("This is not a numeric value!")
}
This is not a numeric value!

אפשר גם להשתמש בהתניות בתוך התניות, כלומר בתוך פקודת if להגדיר פקודת if נוספת.

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

נסו להרחיב את הדוגמה הקודמת, כך שבמידה ו-some_value הוא מספר המתחלק ב-2 אז יודפס “some_value is even” ובמידה ואינו מתחלק ב-2 יודפס “some_value is odd”.

נסו למצוא יותר מדרך אחת לביצוע הרחבה זו.

2.6 לולאות

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

  • לולאות for
  • לולאות while
  • לולאות repeat break next
  • לולאות באמצעות תכנות פונקציונלי (כדוגמת פקודת lapply, או פקודות מחבילות אחרות כמו map שנמצאת בחבילת purrr)

עדיף להימנע ככל שניתן משימוש בכל סוגי הלולאות for, while, ו-repeat בעבודה עם R. לולאות אלו מאוד לא יעילות וזמן הריצה שלהן ארוך. תכנות פונקציונלי הוא יותר יעיל, אך דורש קצת יותר “התרגלות”. בכל זאת, נדגים את אופן הפעולה של לולאת for.

על מנת להגדיר לולאת for עלינו להגדיר ראשית את טווח הפעולה של הלולאה. לדוגמה, לולאה שרצה על המספרים 1 עד 100 תוגדר באופן הבא:

for (i in 1:100){
  # do some action
  # you can use i for that but don't have to
}

טווח הפעולה של הלולאה לא חייב להיות מספרי, אפשר גם להשתמש באובייקטים נוספים. לדוגמה נשתמש בלולאה על סוגי פירות. בדוגמה הבאה נשלב גם שימוש בהתניות, כפי שלמדנו בסעיף הקודם. השילוב של לולאות והתניות די נפוץ בתכנות.

fruits <- c("Mango", "Bannana", "Pineapple", "Orange", "Apple", "Prune", "Lemon", "Loquat")
my_garden <- c("Mango", "Orange", "Lemon", "Loquat")

for (current_fruit in fruits) {
  if (current_fruit %in% my_garden) {
    cat("\nI grow", current_fruit)
  } else {
    cat("\nI don't grow", current_fruit)
  }
}

I grow Mango
I don't grow Bannana
I don't grow Pineapple
I grow Orange
I don't grow Apple
I don't grow Prune
I grow Lemon
I grow Loquat
ההבדל בין %in% לבין in

בדוגמה הקודמת השתמשנו ב-in וגם ב-%in%.

in נחשבת מילת מפתח, בעוד ש-%in% נחשבת אופרטור. עמדו על ההבדלים ביניהן וכתבו מה המשמעות של כל אחת, ומה התפקיד שלה בקוד.

נדגים גם כיצד ניתן להחליף לולאה באמצעות פעולה וקטורית, ובאמצעות נוסחה מתמטית. נניח שאנחנו רוצים לחשב סכום של טור הנדסי, כלומר הסכום של \(1, q, q^2, q^3, \ldots\) כאשר \(q<1\). נציג שלוש דרכים לבצע את הפעולה הזו: לולאה (הדרך הכי פחות יעילה), פעולה וקטורית (להיעזר בוקטור, כלומר מערך של מספרים), והדרך היעילה ביותר שהיא כמובן פשוט להשתמש בנוסחה של טור הנדסי (\(\frac{1}{1-q}\)).

q <- 1/2

series_sum_loop <- 0

for (element_i in 0:50){
  series_sum_loop <- series_sum_loop + q^element_i
}

series_sum_loop
[1] 2
series_sum_vector <- sum(q^(0:50))

series_sum_vector
[1] 2
series_sum_analytic <- 1/(1-q)

series_sum_analytic
[1] 2

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

תרגיל בבניית סדרת פיבונצ’י

סדרת פיבונצ’י היא סדרה של מספרים שבה כל איבר הוא הסכום של שני האיברים שקדמו לו. שני האיברים הראשונים מקבלים את הערך 1, ולכן האיברים הראשונים של הסדרה נראים כך:

\[ 1, 1, 2, 3, 5, 8, 13, 21, 34,\ldots \] היעזרו בקוד הבא על מנת לבנות לולאה שתדפיס את 50 האיברים הראשונים של סדרת פיבונצ’י. עליכם להשלים את הקוד במקומות בהם מופיע סימן שאלה.

# Fibonachi code exercise, fill in the blanks (where you see `?`)
total <- ?

element_i_minus1 <- 1
element_i_minus2 <- 1

for (? in 3:total){
  next_element <- element_i_minus1 + ?
  element_i_minus2 <- element_i_minus1
  ? <- next_element
  
  cat("\n", ?)
}

2.7 סיכום

בפרק זה למדנו את התחביר הבסיסי של שפת R.

  • ראינו כיצד עובדת השמת משתנים

  • למדנו על סוגי משתנים שונים

  • ראינו כיצד לקרוא לתתי וקטורים (חלק מוקטור)

  • למדנו על אובייקטים מסוג רשימה (list)

  • למדנו על פונקציות שונות - כיצד להפעיל אותן וכיצד להגדיר פונקציות חדשות

  • למדנו על אופרטורים (וגם, או, לא - NOT, והשוואה)

  • למדנו על התניות (אם… אז… אחרת)

  • למדנו על לולאות (מסוג for), והזכרנו סוגים נוספים.

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