25 الدوال
25.1 مقدمة
إحدى أفضل الطرق لزيادة إمكانياتك وقدراتك كعالم بيانات هي كتابة الدوال (Functions). تتيح لك الدوال أتمتة المهام الشائعة بطريقة أكثر قوة وعمومية مقارنة بأسلوب النسخ واللصق. تتميز كتابة الدالة بأربع مزايا رئيسية مقارنة باستخدام النسخ واللصق:
يمكنك منح الدالة اسماً معبراً يجعل فهم كودك أسهل بكثير.
مع تغير المتطلبات، لن تحتاج سوى تحديث الكود في مكان واحد فقط، بدلاً من أماكن متعددة.
تمنع تماماً فرصة ارتكاب الأخطاء العرضية عند النسخ واللصق (مثل تحديث اسم متغير في مكان ونسيانه في مكان آخر).
تجعل إعادة استخدام عملك من مشروع لآخر أسهل، مما يرفع إنتاجيتك بمرور الوقت.
القاعدة الذهبية هي أن تفكر في كتابة دالة كلما قمت بنسخ ولصق كتل برمجية أكثر من مرتين (أي عندما يصبح لديك ثلاث نسخ من الكود نفسه). في هذا الفصل، ستتعلم عن ثلاثة أنواع مفيدة من الدوال:
- دوال المتجهات (Vector functions) تأخذ متجهاً أو أكثر كمدخلات وترجع متجهاً كمخرجات.
- دوال أطر البيانات (Data frame functions) تأخذ إطار بيانات كمدخلات وترجع إطار بيانات كمخرجات.
- دوال الرسوم البيانية (Plot functions) تأخذ إطار بيانات كمدخلات وترجع رسماً بيانياً كمخرجات.
يتضمن كل قسم من هذه الأقسام العديد من الأمثلة لمساعدتك على تعميم الأنماط التي تراها. لم تكن هذه الأمثلة لتتحقق لولا مساعدة المتابعين على منصة تويتر (X)، ونشجعك على متابعة الروابط في التعليقات للاطلاع على المصادر والأفكار الأصلية. قد ترغب أيضاً في قراءة التغريدات المُلهمة الأصلية لـ الدوال العامة ودوال الرسم البياني لرؤية المزيد من الأمثلة.
25.1.1 المتطلبات المسبقة
سنقوم بتجميع وتطبيق مجموعة متنوعة من الدوال من مختلف حزم منظومة tidyverse. سنستخدم أيضاً حزمة nycflights13 كمصدر للبيانات المألوفة لتطبيق دوالنا عليها.
25.2 دوال المتجهات
سنبدأ بدوال المتجهات: وهي الدوال التي تأخذ متجهاً أو أكثر وترجع نتيجة في صورة متجه. على سبيل المثال، ألقِ نظرة على هذا الكود، ماذا يفعل؟
df <- tibble(
a = rnorm(5),
b = rnorm(5),
c = rnorm(5),
d = rnorm(5),
)
df |> mutate(
a = (a - min(a, na.rm = TRUE)) /
(max(a, na.rm = TRUE) - min(a, na.rm = TRUE)),
b = (b - min(a, na.rm = TRUE)) /
(max(b, na.rm = TRUE) - min(b, na.rm = TRUE)),
c = (c - min(c, na.rm = TRUE)) /
(max(c, na.rm = TRUE) - min(c, na.rm = TRUE)),
d = (d - min(d, na.rm = TRUE)) /
(max(d, na.rm = TRUE) - min(d, na.rm = TRUE)),
)
#> # A tibble: 5 × 4
#> a b c d
#> <dbl> <dbl> <dbl> <dbl>
#> 1 0.339 0.387 0.291 0
#> 2 0.880 -0.613 0.611 0.557
#> 3 0 -0.0833 1 0.752
#> 4 0.795 -0.0822 0 1
#> 5 1 -0.0952 0.580 0.394قد تتمكن من استنتاج أن هذا الكود يعيد تحجيم (rescales) كل عمود لتصبح قيم نطاقه بين 0 و 1. ولكن هل لاحظت الخطأ؟ عندما كتب “هادلي” هذا الكود ارتكب خطأً أثناء النسخ واللصق ونسي تغيير الحرف a إلى b. يُعد منع هذا النوع من الأخطاء سبباً وجيهاً جداً لتعلّم كيفية كتابة الدوال.
25.2.1 كتابة الدالة
لكتابة دالة، عليك أولاً تحليل كودك المكرر لمعرفة الأجزاء الثابتة والأجزاء التي تتغير. إذا أخذنا الكود أعلاه واستخرجناه خارج دالة mutate()، فسيكون من الأسهل رؤية النمط لأن كل تكرار سيصبح في سطر واحد:
ولجعل الأمر أكثر وضوحاً، يمكننا استبدال الجزء الذي يتغير بالرمز█:
(█ - min(█, na.rm = TRUE)) / (max(█, na.rm = TRUE) - min(█, na.rm = TRUE))لتحويل هذا إلى دالة تحتاج إلى ثلاثة أشياء:
الاسم (Name). هنا سنستخدم الاسم
rescale01لأن هذه الدالة تقوم بإعادة تحجيم المتجه ليقع بين 0 و 1.المُعاملات (Arguments). المُعاملات هي الأشياء التي تتغير عبر الاستدعاءات المختلفة، وتحليلنا أعلاه يخبرنا أن لدينا مُعاملاً واحداً فقط. سنسميه
xلأنه الاسم الشائع والمعتاد للمتجهات العددية.جسم الدالة (Body). وهو الكود الذي يتكرر عبر جميع الاستدعاءات.
بعد ذلك تنشئ الدالة باتباع القالب التالي:
name <- function(arguments) {
body
}بالنسبة لحالتنا، يؤدي هذا إلى:
عند هذه النقطة، قد تختبر الدالة ببعض المدخلات البسيطة للتأكد من أنك التقطت المنطق بشكل صحيح:
ثم يمكنك إعادة كتابة الاستدعاء لـ mutate() على النحو التالي:
df |> mutate(
a = rescale01(a),
b = rescale01(b),
c = rescale01(c),
d = rescale01(d),
)
#> # A tibble: 5 × 4
#> a b c d
#> <dbl> <dbl> <dbl> <dbl>
#> 1 0.339 1 0.291 0
#> 2 0.880 0 0.611 0.557
#> 3 0 0.530 1 0.752
#> 4 0.795 0.531 0 1
#> 5 1 0.518 0.580 0.394(في الفصل 26، ستتعلم كيفية استخدام across() لتقليل التكرار بشكل أكبر بحيث يكون كل ما تحتاجه هو df |> mutate(across(a:d, rescale01))).
25.2.2 تحسين دالتنا
قد تلاحظ أن الدالة rescale01() تقوم ببعض العمل غير الضروري — فبدلاً من حساب min() مرتين و max() مرة واحدة، يمكننا حساب كل من الحد الأدنى والحد الأقصى في خطوة واحدة باستخدام range():
rescale01 <- function(x) {
rng <- range(x, na.rm = TRUE)
(x - rng[1]) / (rng[2] - rng[1])
}أو قد تجرّب هذه الدالة على متجه يحتوي على قيمة مالانهاية (infinite value):
x <- c(1:10, Inf)
rescale01(x)
#> [1] 0 0 0 0 0 0 0 0 0 0 NaNتلك النتيجة ليست مفيدة تماماً، لذا يمكننا إخبار range() بتجاهل القيم اللانهائية:
rescale01 <- function(x) {
rng <- range(x, na.rm = TRUE, finite = TRUE)
(x - rng[1]) / (rng[2] - rng[1])
}
rescale01(x)
#> [1] 0.0000000 0.1111111 0.2222222 0.3333333 0.4444444 0.5555556 0.6666667
#> [8] 0.7777778 0.8888889 1.0000000 Infتوضح هذه التغييرات فائدة مهمة للدوال: نظرًا لأننا نقلنا الكود المكرر إلى دالة، فنحن نحتاج فقط إلى إجراء التغيير في مكان واحد.
25.2.3 دوال التعديل (Mutate functions)
الآن بعد أن أصبحت لديك الفكرة الأساسية للدوال، دعنا نلقي نظرة على مجموعة كاملة من الأمثلة. سنبدأ بالنظرة إلى “دوال التعديل”، أي الدوال التي تعمل بشكل ممتاز داخل mutate() و filter() لأنها ترجع مخرجات بنفس طول المدخلات.
لنبدأ بتنويع بسيط للدالة rescale01(). ربما تريد حساب الدرجة المعيارية (Z-score)، لإعادة تحجيم متجه ليكون متوسطه صفراً وانحرافه المعياري واحداً:
أو ربما تريد تغليف استخدام مباشر للدالة case_when() وإعطائها اسماً مفيداً. على سبيل المثال، تضمن دالة clamp() هذه أن جميع قيم المتجه تقع بين حد أدنى وحد أقصى:
clamp <- function(x, min, max) {
case_when(
x < min ~ min,
x > max ~ max,
.default = x
)
}
clamp(1:10, min = 3, max = 7)
#> [1] 3 3 3 4 5 6 7 7 7 7بالطبع لا تقتصر الدوال على التعامل مع المتغيرات العددية فقط. قد ترغب في إجراء بعض المعالجة المكررة للنصوص. ربما تحتاجه لجعل الحرف الأول كبيراً (uppercase):
first_upper <- function(x) {
str_sub(x, 1, 1) <- str_to_upper(str_sub(x, 1, 1))
x
}
first_upper("hello")
#> [1] "Hello"أو ربما تريد تجريد علامات النسبة المئوية، الفواصل، وعلامات الدولار من سلسلة نصية قبل تحويلها إلى رقم:
# https://twitter.com/NVlabormarket/status/1571939851922198530
clean_number <- function(x) {
is_pct <- str_detect(x, "%")
num <- x |>
str_remove_all("%") |>
str_remove_all(",") |>
str_remove_all(fixed("$")) |>
as.numeric()
if_else(is_pct, num / 100, num)
}
clean_number("$12,300")
#> [1] 12300
clean_number("45%")
#> [1] 0.45في بعض الأحيان تكون دوالك متخصصة للغاية لخطوة تحليل بيانات واحدة. على سبيل المثال، إذا كان لديك مجموعة من المتغيرات التي تسجل القيم المفقودة كـ 997، 998، أو 999، فقد ترغب في كتابة دالة لاستبدالها بـ NA:
لقد ركزنا على الأمثلة التي تأخذ متجهاً واحداً لأننا نعتقد أنها الأكثر شيوعاً. ولكن لا يوجد سبب يمنع دالتك من أخذ مدخلات متجهات متعددة.
25.2.4 دوال التلخيص (Summary functions)
عائلة أخرى مهمة من دوال المتجهات هي دوال التلخيص، وهي الدوال التي ترجع قيمة واحدة لاستخدامها داخل summarize(). في بعض الأحيان قد يكون الأمر مجرد تحديد مُعامل افتراضي أو اثنين:
commas <- function(x) {
str_flatten(x, collapse = ", ", last = " and ")
}
commas(c("cat", "dog", "pigeon"))
#> [1] "cat, dog and pigeon"أو قد تقوم بتغليف حساب بسيط، كما هو الحال بالنسبة لمعامل الاختلاف (coefficient of variation)، والذي يقسم الانحراف المعياري على المتوسط:
أو ربما تريد فقط جعل نمط شائع أسهل في التذكر من خلال منحه اسماً لا يُنسى:
يمكنك أيضاً كتابة دوال بمدخلات متجهات متعددة. على سبيل المثال، ربما تريد حساب متوسط نسبة الخطأ المطلقة (mean absolute percentage error) لمساعدتك في مقارنة توقعات النموذج بالقيم الفعلية:
بمجرد البدء في كتابة الدوال، هناك اختصاران مفيدان للغاية في RStudio:
للعثور على تعريف دالة قمت بكتابتها، ضع المؤشر على اسم الدالة واضغط على
F2.للانتقال السريع إلى دالة، اضغط على
Ctrl + .لفتح أداة البحث عن الملفات والدوال واكتب الحروف الأولى من اسم دالتك. يمكنك أيضاً التنقل إلى الملفات وأقسام Quarto والمزيد، مما يجعلها أداة تنقل مفيدة للغاية.
25.2.5 تمارين
- تدرب على تحويل قصاصات الكود التالية إلى دوال. فكر في ما تفعله كل دالة. ماذا ستسميها؟ كم عدد المُعاملات التي تحتاجها؟
في المتغير الثاني من
rescale01()، تُترك القيم اللانهائية دون تغيير. هل يمكنك إعادة كتابةrescale01()بحيث يتم تعيين-Infإلى 0، وInfإلى 1؟بمعلومية متجه لتوارخ الميلاد، اكتب دالة لحساب العمر بالسنين.
اكتب دوالك الخاصة لحساب التباين (variance) والالتواء (skewness) لمتجه عددي. يمكنك البحث عن التعاريف في ويكيبيديا أو في أي مكان آخر.
اكتب
both_na()، وهي دالة تلخيص تأخذ متجهين بنفس الطول وترجع عدد المواقع التي تحتوي علىNAفي كلا المتجهين.اقرأ التوثيق لمعرفة ما تفعله الدوال التالية. لماذا تعد مفيدة على الرغم من أنها قصيرة جداً؟
is_directory <- function(x) {
file.info(x)$isdir
}
is_readable <- function(x) {
file.access(x, 4) == 0
}25.3 دوال أطر البيانات (Data frame functions)
دوال المتجهات مفيدة لاستخراج الأكواد المكررة داخل أفعال dplyr. ولكنك غالباً ما ستكرر الأفعال نفسها أيضاً، خاصة داخل خطوط الأنابيب/معالجة البيانات (pipelines) الكبيرة. عندما تلاحظ أنك تقوم بنسخ ولصق أفعال متعددة عدة مرات، قد تفكر في كتابة دالة لإطار البيانات. تعمل دوال أطر البيانات مثل أفعال dplyr: حيث تأخذ إطار البيانات كمُعامل أول، وبعض المُعاملات إضافية التي تحدد ما يجب فعله بها، وترجع إطار بيانات أو متجهاً.
لتتمكن من كتابة دالة تستخدم أفعال dplyr، سنعرفك أولاً بتحدي “التوجيه غير المباشر” (indirection) وكيف يمكنك التغلب عليه باستخدام التضمين/الأقواس المزدوجة {{ }}. مع استيعاب هذه النظرية، سنعرض لك بعد ذلك مجموعة من الأمثلة لتوضيح ما يمكنك القيام به منها.
25.3.1 التوجيه غير المباشر والتقييم الأنيق
عندما تبدأ في كتابة دوال تستخدم أفعال dplyr، فإنك تصطدم بسرعة بمشكلة التوجيه غير المباشر. دعنا نوضح المشكلة بدالة بسيطة جداً: grouped_mean(). الهدف من هذه الدالة هو حساب متوسط المتغير mean_var مجمعاً حسب group_var:
إذا حاولنا استخدامها، فسنحصل على خطأ:
diamonds |> grouped_mean(cut, carat)
#> Error in `group_by()`:
#> ! Must group by variables found in `.data`.
#> ✖ Column `group_var` is not found.ولجعل المشكلة أكثر وضوحاً، يمكننا استخدام إطار بيانات اختباري:
df <- tibble(
mean_var = 1,
group_var = "g",
group = 1,
x = 10,
y = 100
)
df |> grouped_mean(group, x)
#> # A tibble: 1 × 2
#> group_var `mean(mean_var)`
#> <chr> <dbl>
#> 1 g 1
df |> grouped_mean(group, y)
#> # A tibble: 1 × 2
#> group_var `mean(mean_var)`
#> <chr> <dbl>
#> 1 g 1بغض النظر عن كيفية استدعاء grouped_mean()، فإنها تقوم دائماً بـ df |> group_by(group_var) |> summarize(mean(mean_var))، بدلاً من df |> group_by(group) |> summarize(mean(x)) أو df |> group_by(group) |> summarize(mean(y)). هذه هي مشكلة التوجيه غير المباشر، وتنشأ لأن dplyr تستخدم التقييم الأنيق (tidy evaluation) للسماح لك بالإشارة إلى أسماء المتغيرات داخل إطار البيانات الخاص بك دون أي معاملة خاصة.
التقييم الأنيق ممتاز في 95% من الحالات لأنه يجعل تحليلات البيانات موجزة للغاية حيث لا يتعين عليك أبداً تحديد إطار البيانات الذي يأتي منه المتغير؛ فهو واضح من السياق. الجانب السلبي للتقييم الأنيق يظهر عندما نريد تغليف أكواد tidyverse المكررة داخل دالة. هنا نحتاج إلى طريقة لإخبار group_by() و summarize() بعدم معاملة group_var و mean_var كاسم حرفي للمتغيرات، بل النظر بداخلها للحصول على المتغير الذي نريد استخدامه بالفعل.
يتضمن التقييم الأنيق حلاً لهذه المشكلة يسمى التضمين / الاحتضان (embracing) 🤗. تضمين المتغير يعني إحاطته بأقواس معقوفة مزدوجة مثل var يصبح {{var}}. يخبر التضمين دالة dplyr باستخدام القيمة المخزنة داخل المُعامل، وليس اسم المُعامل كاسم حرفي للمتغير. إحدى الطرق لتذكر ما يحدث هي التفكير في {{ }} على أنه النظر عبر نفق — حيث سيجعل {{var}} دالة dplyr تنظر داخل var بدلاً من البحث عن متغير يسمى var.
لذا لجعل grouped_mean() تعمل، نحتاج إلى إحاطة group_var و mean_var بـ {{ }}:
نجاح!
25.3.2 متى نستخدم التضمين؟
التحدي الرئيسي في كتابة دوال أطر البيانات هو معرفة المُعاملات التي تحتاج إلى تضمين. لحسن الحظ، هذا أمر سهل لأنه يمكنك معرفته من التوثيق (documentation) 😄. هناك مصطلحان يجب البحث عنهما في التوثيق واللذان يقابلان أكثر نوعين فرعيين شائعين للتقييم الأنيق:
قناع البيانات (Data-masking): يُستخدم في الدوال مثل
arrange()، وfilter()، وsummarize()التي تجري حسابات مع المتغيرات.التحديد الأنيق (Tidy-selection): يُستخدم للدوال مثل
select()، وrelocate()، وrename()التي تختار المتغيرات.
حدسك حول المُعاملات التي تستخدم التقييم الأنيق يجب أن يكون جيداً للعديد من الدوال الشائعة — فقط فكر فيما إذا كنت تستطيع الحساب (مثل x + 1) أو التحديد (مثل a:x).
في الأقسام التالية، سنستكشف أنواع الدوال المفيدة التي قد تكتبها بمجرد فهمك للتضمين.
25.3.3 الاستخدامات الشائعة
إذا كنت تقوم بانتظام بتنفيذ نفس مجموعة الملخصات عند إجراء استكشاف أولي للبيانات، فقد تفكر في تغليفها داخل دالة مساعدة:
summary6 <- function(data, var) {
data |> summarize(
min = min({{ var }}, na.rm = TRUE),
mean = mean({{ var }}, na.rm = TRUE),
median = median({{ var }}, na.rm = TRUE),
max = max({{ var }}, na.rm = TRUE),
n = n(),
n_miss = sum(is.na({{ var }})),
.groups = "drop"
)
}
diamonds |> summary6(carat)
#> # A tibble: 1 × 6
#> min mean median max n n_miss
#> <dbl> <dbl> <dbl> <dbl> <int> <int>
#> 1 0.2 0.798 0.7 5.01 53940 0(كلما قمت بتغليف summarize() داخل دالة مساعدة، فإننا نرى أنه من الممارسات الجيدة تعيين .groups = "drop" لتجنب رسائل التنبيه ولترك البيانات في حالة غير مجمعة).
الشيء الرائع في هذه الدالة هو أنه نظراً لأنها تغلف summarize(), يمكنك استخدامها على البيانات المجمعة:
diamonds |>
group_by(cut) |>
summary6(carat)
#> # A tibble: 5 × 7
#> cut min mean median max n n_miss
#> <ord> <dbl> <dbl> <dbl> <dbl> <int> <int>
#> 1 Fair 0.22 1.05 1 5.01 1610 0
#> 2 Good 0.23 0.849 0.82 3.01 4906 0
#> 3 Very Good 0.2 0.806 0.71 4 12082 0
#> 4 Premium 0.2 0.892 0.86 4.01 13791 0
#> 5 Ideal 0.2 0.703 0.54 3.5 21551 0علاوة على ذلك، بما أن مُعاملات summarize تعتمد على قناع البيانات (data-masking), فإن مُعامل var في summary6() يفعل ذلك أيضاً. هذا يعني أنه يمكنك أيضاً تلخيص المتغيرات المحسوبة:
diamonds |>
group_by(cut) |>
summary6(log10(carat))
#> # A tibble: 5 × 7
#> cut min mean median max n n_miss
#> <ord> <dbl> <dbl> <dbl> <dbl> <int> <int>
#> 1 Fair -0.658 -0.0273 0 0.700 1610 0
#> 2 Good -0.638 -0.133 -0.0862 0.479 4906 0
#> 3 Very Good -0.699 -0.164 -0.149 0.602 12082 0
#> 4 Premium -0.699 -0.125 -0.0655 0.603 13791 0
#> 5 Ideal -0.699 -0.225 -0.268 0.544 21551 0لتلخيص عدة متغيرات، سيتعين عليك الانتظار حتى قسم 26.2, حيث ستتعلم كيفية استخدام across().
دالة مساعدة أخرى شائعة لـ summarize() هي نسخة من count() تقوم أيضاً بحساب النسب المئوية:
# https://twitter.com/Diabb6/status/1571635146658402309
count_prop <- function(df, var, sort = FALSE) {
df |>
count({{ var }}, sort = sort) |>
mutate(prop = n / sum(n))
}
diamonds |> count_prop(clarity)
#> # A tibble: 8 × 3
#> clarity n prop
#> <ord> <int> <dbl>
#> 1 I1 741 0.0137
#> 2 SI2 9194 0.170
#> 3 SI1 13065 0.242
#> 4 VS2 12258 0.227
#> 5 VS1 8171 0.151
#> 6 VVS2 5066 0.0939
#> # ℹ 2 more rowsتحتوي هذه الدالة على ثلاثة مُعاملات: df و var و sort, ويحتاج var وحده إلى التضمين لأنه يُمرر إلى count() التي تستخدم قناع البيانات لجميع المتغيرات. لاحظ أننا نستخدم قيمة افتراضية لـ sort بحيث إذا لم يُقدم المستخدم قيمته الخاصة, فسيكون افتراضياً FALSE.
أو ربما تريد العثور على القيم الفريدة والمرتبة لمتجه ما لعينة فرعية من البيانات. بدلاً من توفير متغير وقيمة لإجراء التصفية, سنسمح للمستخدم بتوفير شرط (condition):
unique_where <- function(df, condition, var) {
df |>
filter({{ condition }}) |>
distinct({{ var }}) |>
arrange({{ var }})
}
# العثور على جميع الوجهات في شهر ديسمبر
flights |> unique_where(month == 12, dest)
#> # A tibble: 96 × 1
#> dest
#> <chr>
#> 1 ABQ
#> 2 ALB
#> 3 ATL
#> 4 AUS
#> 5 AVL
#> 6 BDL
#> # ℹ 90 more rowsهنا نقوم بتضمين condition لأنه يُمرر إلى filter() و var لأنه يُمرر إلى distinct() و arrange().
لقد أنشأنا كل هذه الأمثلة لتأخذ إطار بيانات كمعامل أول, ولكن إذا كنت تعمل مراراً وتكراراً مع نفس البيانات, فقد يكون من المباشر ترميزها ضمنياً (hardcode). على سبيل المثال، تعمل الدالة التالية دائماً مع مجموعة بيانات الرحلات وتختار دائماً time_hour و carrier و flight نظراً لأنها تشكل المفتاح الأساسي المركب الذي يتيح لك تحديد الصف:
25.3.4 قناع البيانات مقابل التحديد الأنيق (Data-masking vs. tidy-selection)
أحياناً ترغب في تحديد متغيرات داخل دالة تستخدم قناع البيانات. على سبيل المثال، تخيل أنك تريد كتابة count_missing() التي تحسب عدد الملاحظات المفقودة في الصفوف. قد تحاول كتابة شيء كالتالي:
count_missing <- function(df, group_vars, x_var) {
df |>
group_by({{ group_vars }}) |>
summarize(
n_miss = sum(is.na({{ x_var }})),
.groups = "drop"
)
}
flights |>
count_missing(c(year, month, day), dep_time)
#> Error in `group_by()`:
#> ℹ In argument: `c(year, month, day)`.
#> Caused by error:
#> ! `c(year, month, day)` must be size 336776 or 1, not 1010328.هذا لا يعمل لأن group_by() تستخدم قناع البيانات وليس التحديد الأنيق. يمكننا التغلب على هذه المشكلة باستخدام الدالة المفيدة pick(), والتي تتيح لك استخدام التحديد الأنيق داخل دالات قناع البيانات:
count_missing <- function(df, group_vars, x_var) {
df |>
group_by(pick({{ group_vars }})) |>
summarize(
n_miss = sum(is.na({{ x_var }})),
.groups = "drop"
)
}
flights |>
count_missing(c(year, month, day), dep_time)
#> # A tibble: 365 × 4
#> year month day n_miss
#> <int> <int> <int> <int>
#> 1 2013 1 1 4
#> 2 2013 1 2 8
#> 3 2013 1 3 10
#> 4 2013 1 4 6
#> 5 2013 1 5 3
#> 6 2013 1 6 1
#> # ℹ 359 more rowsمن الاستخدامات الملائمة الأخرى لـ pick() إنشاء جدول عد ثنائي الأبعاد (2d table). هنا نقوم بالعد باستخدام جميع المتغيرات في rows و cols, ثم نستخدم pivot_wider() لإعادة ترتيب العد في شبكة:
# https://twitter.com/pollicipes/status/1571606508944719876
count_wide <- function(data, rows, cols) {
data |>
count(pick(c({{ rows }}, {{ cols }}))) |>
pivot_wider(
names_from = {{ cols }},
values_from = n,
names_sort = TRUE,
values_fill = 0
)
}
diamonds |> count_wide(c(clarity, color), cut)
#> # A tibble: 56 × 7
#> clarity color Fair Good `Very Good` Premium Ideal
#> <ord> <ord> <int> <int> <int> <int> <int>
#> 1 I1 D 4 8 5 12 13
#> 2 I1 E 9 23 22 30 18
#> 3 I1 F 35 19 13 34 42
#> 4 I1 G 53 19 16 46 16
#> 5 I1 H 52 14 12 46 38
#> 6 I1 I 34 9 8 24 17
#> # ℹ 50 more rowsفي حين أن أمثلتنا ركزت في الغالب على dplyr, فإن التقييم الأنيق يدعم أيضاً tidyr, وإذا نظرت إلى وثائق pivot_wider() يمكنك أن ترى أن names_from يستخدم التحديد الأنيق.
25.3.5 تمارين
-
باستخدام مجموعات البيانات من حزمة nycflights13، اكتب دالة تقوم بما يلي:
-
تجد جميع الرحلات التي تم إلغاؤها (أي
is.na(arr_time)) أو تأخرت لأكثر من ساعة.flights |> filter_severe() -
تحسب عدد الرحلات الملغاة وعدد الرحلات المتأخرة لأكثر من ساعة.
flights |> group_by(dest) |> summarize_severe() -
تجد جميع الرحلات التي تم إلغاؤها أو تأخرت لأكثر من عدد ساعات يحدده المستخدم:
flights |> filter_severe(hours = 2) -
تلخص حالة الطقس لحساب الحد الأدنى، والمتوسط، والحد الأقصى لمتغير يحدده المستخدم:
weather |> summarize_weather(temp) -
تحول المتغير الذي يحدده المستخدم والذي يستخدم نظام التوقيت الزمني (مثل
dep_timeوarr_timeوغيرها) إلى توقيت عشري (أي: الساعات + (الدقائق / 60)).flights |> standardize_time(sched_dep_time)
-
بالنسبة لكل دالة من الدوال التالية، اذكر جميع المعاملات (Arguments) التي تستخدم التقييم الأنيق (Tidy evaluation)، ووضح ما إذا كانت تستخدم قناع البيانات (Data-masking) أم التحديد الأنيق (Tidy-selection):
distinct()، وcount()، وgroup_by()، وrename_with()، وslice_min()، وslice_sample().عمّم الدالة التالية بحيث يمكنك إمدادها بأي عدد من المتغيرات لحساب تكراراتها:
25.4 دوال الرسوم البيانية
بدلاً من إرجاع إطار بيانات، قد ترغب في إرجاع رسم بياني. ولحسن الحظ، يمكنك استخدام نفس التقنيات مع ggplot2, لأن aes() هي دالة قناع بيانات. على سبيل المثال، تخيل أنك تقوم بعمل الكثير من الرسوم البيانية التكرارية (histograms):
diamonds |>
ggplot(aes(x = carat)) +
geom_histogram(binwidth = 0.1)
diamonds |>
ggplot(aes(x = carat)) +
geom_histogram(binwidth = 0.05)أليس من الرائع لو كان بإمكانك تغليف هذا في دالة للرسم التكراري؟ هذا سهل للغاية بمجرد أن تعلم أن aes() هي دالة قناع بيانات وتحتاج إلى التضمين:
histogram <- function(df, var, binwidth = NULL) {
df |>
ggplot(aes(x = {{ var }})) +
geom_histogram(binwidth = binwidth)
}
diamonds |> histogram(carat, 0.1)
لاحظ أن histogram() تُرجع رسم ggplot2, مما يعني أنه لا يزال بإمكانك إضافة مكونات إضافية إذا أردت. فقط تذكر التحول من |> إلى +:
diamonds |>
histogram(carat, 0.1) +
labs(x = "الحجم (بالقيراط)", y = "عدد الماسات")25.4.1 المزيد من المتغيرات
من السهل إضافة المزيد من المتغيرات. على سبيل المثال، ربما تريد طريقة سهلة لمعرفة ما إذا كانت مجموعة البيانات خطية أم لا عن طريق تراكب خط ناعم وخط مستقيم:
# https://twitter.com/tyler_js_smith/status/1574377116988104704
linearity_check <- function(df, x, y) {
df |>
ggplot(aes(x = {{ x }}, y = {{ y }})) +
geom_point() +
geom_smooth(method = "loess", formula = y ~ x, color = "red", se = FALSE) +
geom_smooth(method = "lm", formula = y ~ x, color = "blue", se = FALSE)
}
starwars |>
filter(mass < 1000) |>
linearity_check(mass, height)
أو ربما تريد بديلاً لـ scatterplots الملونة لمجموعات البيانات الكبيرة جداً حيث يمثل التداخل مشكلة:
# https://twitter.com/ppaxisa/status/1574398423175921665
hex_plot <- function(df, x, y, z, bins = 20, fun = "mean") {
df |>
ggplot(aes(x = {{ x }}, y = {{ y }}, z = {{ z }})) +
stat_summary_hex(
aes(color = after_scale(fill)), # جعل الحدود نفس لون التعبئة
bins = bins,
fun = fun,
)
}
diamonds |> hex_plot(carat, price, depth)
25.4.2 الدمج مع حزم tidyverse الأخرى
بعض الدوال المساعدة الأكثر فائدة تجمع بين لمسة من معالجة البيانات و ggplot2. على سبيل المثال، قد ترغب في عمل مخطط أشرطة عمودي حيث تقوم تلقائياً بترتيب الأشرطة بترتيب التردد باستخدام fct_infreq(). بما أن مخطط الأشرطة عمودي، فنحن بحاجة أيضاً إلى عكس الترتيب المعتاد للحصول على أعلى القيم في الأعلى:

يجب أن نستخدم مُعاملاً جديداً هنا، := (ويُعرف عادة بـ “معامل الفظ - walrus operator”), لأننا نولد اسم المتغير بناءً على بيانات يوفرها المستخدم. توضع أسماء المتغيرات على الجانب الأيسر من =, لكن صياغة R لا تسمح بأي شيء على يسار = إلا اسم حرفي واحد. وللتغلب على هذه المشكلة، نستخدم المعامل الخاص := والذي يتعامل معه التقييم الأنيق بنفس طريقة =.
أو ربما تريد تسهيل رسم مخطط أشرطة فقط لعينة فرعية من البيانات:

يمكنك أيضاً الإبداع وعرض ملخصات البيانات بطرق أخرى. يمكنك العثور على تطبيق رائع على https://gist.github.com/GShotwell/b19ef520b6d56f61a830fabb3454965b؛ حيث يستخدم تسميات المحاور لعرض أعلى قيمة. كلما تعلمت المزيد عن ggplot2, ستستمر قوة دالتك في الازدياد.
سنختتم بحالة أكثر تعقيداً: تسمية الرسوم البيانية التي تنشئها.
25.4.3 التسمية (Labeling)
أتذكر دالة الرسم التكراري التي عرضناها عليك سابقاً؟
histogram <- function(df, var, binwidth = NULL) {
df |>
ggplot(aes(x = {{ var }})) +
geom_histogram(binwidth = binwidth)
}أليس من الرائع لو تمكنا من تسمية المخرجات بالمتغير وعرض الفئة (bin width) المستخدمة؟ لقيام بذلك، سيتعين علينا الغوص تحت غطاء التقييم الأنيق واستخدام دالة من حزمة لم نتحدث عنها بعد: rlang. rlang هي حزمة منخفضة المستوى تُستخدم من قبل جميع الحزم الأخرى في tidyverse تقريباً لأنها تنفذ التقييم الأنيق (وكذلك العديد من الأدوات المفيدة الأخرى).
لحل مشكلة التسمية، يمكننا استخدام rlang::englue(). يعمل هذا بشكل مشابه لـ str_glue(), لذلك سيتم إدراج أي قيمة مغلفة في { } في السلسلة النصية. ولكنه يفهم أيضاً {{ }}, والتي تدرج تلقائياً اسم المتغير المناسب:
histogram <- function(df, var, binwidth) {
label <- rlang::englue("A histogram of {{var}} with binwidth {binwidth}")
df |>
ggplot(aes(x = {{ var }})) +
geom_histogram(binwidth = binwidth) +
labs(title = label)
}
diamonds |> histogram(carat, 0.1)
يمكنك استخدام نفس النهج في أي مكان آخر تريد فيه توفير سلسلة نصية في رسم ggplot2.
25.4.4 تمارين
قم بإنشاء دالة رسم غنية من خلال تنفيذ كل خطوة من الخطوات أدناه تدريجياً:
ارسم مخطط انتشار (scatterplot) بناءً على مجموعة بيانات ومتغيرات
xوy.أضف خط أفضل ملاءمة (a line of best fit) (أي نموذج خطي بدون أخطاء معيارية).
أضف عنواناً.
25.5 الأسلوب (Style)
لا يهم R ما سميت دالتك أو معاملاتها، لكن الأسماء تحدث فرقاً كبيراً للبشر. من الناحية المثالية، سيكون اسم دالتك قصيراً، ولكنه يوضح بوضوح ما تفعله الدالة. هذا صعب! لكن من الأفضل أن تكون واضحاً بدلاً من أن تكون قصيراً، حيث تجعل ميزة الإكمال التلقائي في RStudio من السهل كتابة الأسماء الطويلة.
عموماً، يجب أن تكون أسماء الدوال أفعالاً، والمعاملات أسماءً (nouns). هناك بعض الاستثناءات: الأسماء مقبولة إذا كانت الدالة تحسب اسماً معروفاً جداً (أي mean() أفضل من compute_mean()), أو الوصول إلى بعض خصائص كائن ما (أي coef() أفضل من get_coefficients()). استخدم أفضل تقدير لديك ولا تخف من إعادة تسمية الدالة إذا اكتشفت اسماً أفضل لاحقاً.
# قصير جداً
f()
# ليست فعلاً، أو غير وصفية
my_awesome_function()
# طويلة، ولكنها واضحة
impute_missing()
collapse_years()لا يهتم R أيضاً بكيفية استخدام المسافات البيضاء في دالاتك، لكن القراء المستقبليين سيهتمون بذلك. استمر في اتباع القواعد من الفصل 4. بالإضافة إلى ذلك، يجب أن تتبع function() دائماً الأقواس المتعرجة ({}), ويجب إزاحة المحتوى بمقدار بمسافتين إضافيتين. هذا يسهل رؤية التسلسل الهرمي في كودك من خلال تصفح الهامش الأيسر.
# مفقود مسافتان إضافيتان
density <- function(color, facets, binwidth = 0.1) {
diamonds |>
ggplot(aes(x = carat, y = after_stat(density), color = {{ color }})) +
geom_freqpoly(binwidth = binwidth) +
facet_wrap(vars({{ facets }}))
}
# تم إزاحة الأنبوب بشكل غير صحيح
density <- function(color, facets, binwidth = 0.1) {
diamonds |>
ggplot(aes(x = carat, y = after_stat(density), color = {{ color }})) +
geom_freqpoly(binwidth = binwidth) +
facet_wrap(vars({{ facets }}))
}كما ترى، نوصي بوضع مسافات إضافية داخل {{ }}. هذا يجعل من الواضح جداً أن شيئاً غير معتاد يحدث.
25.5.1 تمارين
- اقرأ الكود المصدري لكل من الدالتين الآتيتين، واكتشف ما تفعلانه، ثم فكر في أسماء أفضل.
f1 <- function(string, prefix) {
str_sub(string, 1, str_length(prefix)) == prefix
}
f3 <- function(x, y) {
rep(y, length.out = length(x))
}25.6 ملخص
في هذا الفصل، تعلمت كيفية كتابة الدوال لثلاثة سيناريوهات مفيدة: إنشاء متجه، إنشاء إطار بيانات، أو إنشاء رسم بياني. على طول الطريق رأيت العديد من الأمثلة، والتي نأمل أن تبدأ في تحفيز أفكارك، ومنحتك بعض الأفكار حول الأماكن التي قد تساعد فيها الدوال كود التحليل الخاص بك.
لقد أظهرنا لك الحد الأدنى للبدء في الدوال وهناك الكثير لتتعلمه. بعض الأماكن لمعرفة المزيد هي:
- لمعرفة المزيد حول البرمجة باستخدام التقييم الأنيق، راجع الوصفات المفيدة في programming with dplyr و programming with tidyr وتعرف على المزيد حول النظرية في ?}} What is data-masking and why do I need.
- لمعرفة المزيد حول تقليل التكرار في كود ggplot2 الخاص بك، اقرأ فصل Programming with ggplot2 من كتاب ggplot2.
- لمزيد من النصائح حول أسلوب الدوال، راجع tidyverse style guide.
في الفصل التالي، سنغوص في التكرار (iteration) والذي يمنحك أدوات إضافية لتقليل تكرار الكود.