عائلة 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. وللحالتين الأكثر شيوعًا تأتي R الأساسية بمساعدات مخصصة وأسرع - وهي rowSums وcolSums وrowMeans وcolMeans - فاحفظ apply للدوال التي لا مساعد مخصصًا لها، مثل apply(m, 2, max).
وثمة فخ: أن apply على إطار بيانات تحوّله بصمت إلى مصفوفة أولًا، فتكره كل الأعمدة على نوع واحد مشترك. ولإطارات البيانات، عامل الأعمدة بوصفها قائمة واستخدم 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. وهذا هو جواب R الأساسية عن سؤال "المتوسط لكل فئة" - وهو شكل السؤال نفسه الذي تجيب عنه group_by مع summarize لإطارات البيانات، فإن كنت في أرض dplyr أصلًا فاستخدمها؛ وتتألق tapply حين يكون لديك متجهان مجردان وتريد سطرًا واحدًا.
وثمة تسهيلان يعملان عبر العائلة كلها. فالوسائط الإضافية بعد الدالة تُمرَّر إليها في كل استدعاء:
وتندرج الدوال المجهولة في مكانها كلما لم تناسب دالة جاهزة - فالتعبير sapply(x, \(v) max(v) - min(v)) يحسب المدى لكل عنصر دون تسمية دالة مساعدة أولًا.
عائلة apply مقابل حلقات for
ستسمع أن حلقات for بطيئة في R وأن عائلة apply سريعة. وذلك خرافة في معظمه. فالبطيء فعلًا هو تنمية النتيجة داخل حلقة - إذ ينسخ التعبير result <- c(result, new_value) المتجه كاملًا في كل دورة. أما الحلقة التي تخصص مخرجاتها مسبقًا فأداؤها جيد:
لذا اختر بحسب قابلية القراءة لا الأداء. فنسخة apply تقول ماذا في سطر واحد - "ربّع كل عنصر" - وتتولى التخصيص نيابة عنك، ولهذا فهي الصيغة المعتمدة للعمل المباشر عنصرًا عنصرًا. وتثبت حلقة for جدارتها حين تعتمد التكرارات على نتائج سابقة، أو حين تحتاج إلى خروج مبكر بـ break، أو حين يكون الجسم طويلًا بما يكفي لتصير الدالة المجهولة ضررًا لا نفعًا. وكلاهما R مشروعة.
ما تخرج به
- العائلة كلها فكرة واحدة: شغّل دالة على كل عنصر واجمع النتائج.
- الدالة
applyلصفوف المصفوفة (MARGIN = 1) وأعمدتها (MARGIN = 2)؛ وفضّلrowSums/colMeansحين توجدان. - تعيد
lapplyقائمة دائمًا؛ وتبسّطsapplyحين تستطيع - ما يعني أن نوع ما تعيده قد يتغير مع البيانات. - تثبّت
vapplyالنوع المعاد؛ فاستخدمها في الشيفرة التي يجب ألا تفاجئك. - تضم
mapplyعدة مدخلات معًا؛ وتجمّعtapplyالقيم داخل المجموعات. - حلقات for ليست بطيئة بطبيعتها - بل تنمية المتجهات هي البطيئة. اختر الصيغة الأفضل قراءة.
التالي: الأنابيب - المعامل الذي يسلسل هذه الاستدعاءات في خطوط معالجة مقروءة خطوة بخطوة.
الأسئلة الشائعة
ما الفرق بين 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 خطأ فورًا بدل أن تسلّمك بصمت بنية غير متوقعة. فضّلها في الشيفرة التي يجب أن تستمر في العمل دون إشراف.