משפחת apply במשפט אחד
כל חברי משפחת apply עושים את אותה עבודה: לוקחים פונקציה, מריצים אותה על כל איבר במבנה כלשהו ואוספים את התוצאות. מה שמשתנה הוא צורת הקלט וצורת הפלט. המפה:
apply(m, MARGIN, FUN): שורות או עמודות של מטריצה.lapply(x, FUN): כל איבר ברשימה או בווקטור; תמיד מחזירה רשימה.sapply(x, FUN): כמוlapply, ואז מפשטת לווקטור או למטריצה כשאפשר.vapply(x, FUN, FUN.VALUE):sapplyעם סוג החזרה מוצהר ונאכף.mapply(FUN, x, y, ...): עוברת על כמה קלטים במקביל, איבר אחר איבר.tapply(values, groups, FUN): מפעילה אתFUNבתוך כל קבוצה: תוצאה אחת לכל קבוצה.
כולן מקבלות פונקציות שאתם כותבים בעצמכם, כולל פונקציות אנונימיות, ומשם מגיע הכוח של המשפחה.
apply(): שורות ועמודות של מטריצה
apply מקבלת מטריצה, MARGIN (1 לשורות, 2 לעמודות) ופונקציה:
המטריצה מתמלאת עמודה אחר עמודה, ולכן השורות הן 1 3 5 ו-2 4 6: סכומי השורות מודפסים כ-9 12 וסכומי העמודות כ-3 7 11. לשני המקרים הנפוצים ביותר יש ב-base R פונקציות עזר ייעודיות ומהירות יותר, rowSums, colSums, rowMeans, colMeans, אז שמרו את apply לפונקציות שאין להן גרסה כזו, כמו apply(m, 2, max).
מלכודת אחת: apply על data frame ממירה אותו בשקט למטריצה קודם, וזה כופה על כל העמודות סוג משותף אחד. ב-data frames התייחסו לעמודות כרשימה והשתמשו ב-lapply/sapply.
lapply() תמיד נותנת רשימה, sapply() מפשטת
lapply מפעילה פונקציה על כל איבר ומחזירה רשימה באותו אורך, לא משנה מה:
אותו חישוב, שתי צורות: lapply מחזירה רשימה שמכילה 85 ו-80; sapply שמה לב שלכל תוצאה אורך 1 ומפשטת לווקטור מספרי עם שמות, הרבה יותר נוח לקריאה ולהמשך חישובים.
אבל לנוחות הזו יש קצה חד: סוג ההחזרה של sapply תלוי בנתונים. אם אפילו איבר אחד מייצר אורך שונה, הפישוט נכשל ובשקט מקבלים שוב רשימה:
בעבודה אינטראקטיבית זה לא נורא; בתוך סקריפט זה אומר שקוד שעבד כל השנה נשבר ביום שבו צורת הנתונים משתנה. זו כל הסיבה ש-vapply קיימת.
vapply(): גרסת sapply בטוחה מבחינת סוגים
vapply מוסיפה ארגומנט שלישי, FUN.VALUE: תבנית שמצהירה איך תוצאה אחת צריכה להיראות. integer(1) פירושו "כל קריאה מחזירה בדיוק מספר שלם אחד":
מקבלים וקטור של מספרים שלמים עם שמות, 5 6 6, וחשוב מכך, התחייבות: אם nchar הייתה מחזירה אי פעם שני ערכים או תו, vapply הייתה עוצרת עם שגיאה בנקודת הקריאה במקום לתת לתוצאה בצורה שגויה לזלוג הלאה. ההצהרה משמשת גם כתיעוד:
vapply(words, nchar, character(1))
# Error in vapply(words, nchar, character(1)) : values must be type 'character'
כלל אצבע: sapply בקונסולה, vapply בפונקציות ובסקריפטים שצריכים לרוץ בלי השגחה.
mapply() ו-tapply(): קלטים מקבילים וקבוצות
lapply עוברת על מבנה אחד. כשכל קריאה צריכה איבר מכמה מבנים באותו מיקום, השתמשו ב-mapply. שימו לב שהפונקציה באה ראשונה:
כל איבר בפלט מצמיד את הערכים הראשונים, אחר כך את הערכים השניים, וכן הלאה: 10 200 3000 40000.
tapply היא החברה במשפחה שעובדת לפי קבוצות: היא מפצלת את values לפי groups ומפעילה את הפונקציה בתוך כל קבוצה, עם תוצאה אחת לכל קבוצה:
הממוצע של קבוצה a הוא 30, של קבוצה b הוא 40. זו התשובה של base R ל"ממוצע לכל קטגוריה", אותה שאלה ש-group_by + summarize עונה עליה עבור data frames. אם אתם כבר בעולם של dplyr השתמשו בזה; tapply מצטיינת כשיש לכם שני וקטורים פשוטים ואתם רוצים שורה אחת.
שתי נוחויות עובדות בכל המשפחה. ארגומנטים נוספים אחרי הפונקציה מועברים אליה בכל קריאה:
ופונקציות אנונימיות נכנסות בכל מקום שבו אין פונקציה מוכנה שמתאימה: sapply(x, \(v) max(v) - min(v)) מחשבת טווח לכל איבר בלי לתת קודם שם לפונקציית עזר.
משפחת apply מול לולאות for
תשמעו שלולאות for איטיות ב-R ושמשפחת apply מהירה. זה בעיקר מיתוס. מה שבאמת איטי הוא הגדלת תוצאה בתוך לולאה: result <- c(result, new_value) מעתיקה את כל הווקטור בכל סבב. לולאה שמקצה את הפלט שלה מראש מתפקדת מצוין:
אז בחרו לפי קריאוּת, לא לפי ביצועים. גרסת ה-apply אומרת מה בשורה אחת, "העלו כל איבר בריבוע", ומטפלת בהקצאה בשבילכם, ולכן היא הדרך המקובלת לעבודה פשוטה איבר אחר איבר. לולאת for מצדיקה את עצמה כשסבבים תלויים בתוצאות קודמות, כשצריך יציאה מוקדמת עם break, או כשגוף הלולאה ארוך מספיק כדי שפונקציית lambda תזיק יותר משתועיל. שתיהן R לגיטימי.
מה לקחת מכאן
- כל המשפחה היא רעיון אחד: להריץ פונקציה על כל איבר ולאסוף את התוצאות.
applyמיועדת לשורות מטריצה (MARGIN = 1) ולעמודות (MARGIN = 2); העדיפוrowSums/colMeansכשהן קיימות.lapplyתמיד מחזירה רשימה;sapplyמפשטת כשאפשר, כלומר סוג ההחזרה שלה יכול להשתנות עם הנתונים.vapplyנועלת את סוג ההחזרה; השתמשו בה בקוד שאסור לו להפתיע אתכם.mapplyמצמידה כמה קלטים יחד;tapplyמסכמת ערכים בתוך קבוצות.- לולאות for לא איטיות מטבען, הגדלת וקטורים היא האיטית. בחרו בכתיב שהכי קל לקרוא.
הבא בתור: pipes, האופרטור שמשרשר את הקריאות האלה לצינורות קריאים, שלב אחר שלב.
שאלות נפוצות
מה ההבדל בין lapply ל-sapply ב-R?
lapply תמיד מחזירה רשימה, בלי יוצאים מן הכלל. sapply מבצעת את אותו חישוב ואז מנסה לפשט את התוצאה: לווקטור אם לכל איבר אורך 1, למטריצה אם לכולם אותו אורך, וחזרה לרשימה אם זה לא מסתדר. sapply נוחה יותר לעבודה אינטראקטיבית; lapply (או vapply) בטוחה יותר בסקריפטים, כי סוג ההחזרה שלה לעולם לא משתנה.
מה עושה apply() ב-R?
apply(m, MARGIN, FUN) מריצה את FUN על מטריצה: MARGIN = 1 מפעילה אותה על כל שורה, MARGIN = 2 על כל עמודה. apply(m, 1, sum) נותנת סכומי שורות, apply(m, 2, mean) נותנת ממוצעי עמודות. היא מיועדת למטריצות ומערכים; לרשימות ולווקטורים השתמשו ב-lapply/sapply.
האם משפחת apply מהירה יותר מלולאת for ב-R?
בדרך כלל לא בהרבה, זה מיתוס. לולאת for כתובה היטב עם וקטור תוצאה שהוקצה מראש מתפקדת בצורה דומה. הלולאות שהוציאו ל-for שם רע מגדילות את התוצאה איבר אחד בכל פעם ומעתיקות הכול בכל סבב. בחרו בפונקציות ממשפחת apply בשביל תמציתיות וכוונה ברורה, לא בשביל מהירות.
למה משמשת vapply ב-R?
vapply היא sapply עם חוזה: מצהירים על הסוג והאורך של כל תוצאה, למשל vapply(x, nchar, integer(1)). אם הפונקציה מחזירה משהו אחר, R זורקת שגיאה מיד במקום להגיש לכם בשקט מבנה לא צפוי. העדיפו אותה בקוד שצריך להמשיך לעבוד בלי השגחה.