library(nycflights13)
library(tidyverse)
#> ── Attaching core tidyverse packages ───────────────────── tidyverse 2.0.0 ──
#> ✔ dplyr 1.2.1 ✔ readr 2.2.0
#> ✔ forcats 1.0.1 ✔ stringr 1.6.0
#> ✔ ggplot2 4.0.3 ✔ tibble 3.3.1
#> ✔ lubridate 1.9.5 ✔ tidyr 1.3.2
#> ✔ purrr 1.2.2
#> ── Conflicts ─────────────────────────────────────── tidyverse_conflicts() ──
#> ✖ dplyr::filter() masks stats::filter()
#> ✖ dplyr::lag() masks stats::lag()
#> ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors3 تحويل البيانات
3.1 مقدمة
يُعد التمثيل البصري (Visualization) أداةً بالغة الأهمية لتوليد الأفكار والرؤى التفسيرية، ولكن نادراً ما تأتي البيانات بالصيغة الصحيحة والمناسبة تماماً لبناء الرسم البياني الذي ترغب فيه. ففي كثير من الأحيان، ستكون بحاجة إلى إنشاء متغيرات جديدة أو استخراج ملخصات إحصائية للإجابة عن أسئلتك البحثية باستخدام بياناتك، أو ربما ترغب فقط في إعادة تسمية المتغيرات، أو إعادة ترتيب المشاهدات (Observations) لتسهيل التعامل مع البيانات. وسوف تتعلم كيفية القيام بكل ذلك (وأكثر!) في هذا الفصل، والذي سيقدم لك مبادئ تحويل البيانات باستخدام حزمة dplyr، مستعينين بمجموعة بيانات جديدة خاصة بالرحلات الجوية التي أقلعت من مدينة نيويورك عام 2013.
إن الهدف من هذا الفصل هو تزويدك بنظرة عامة وشاملة على جميع الأدوات الأساسية المستخدمة لتحويل الإطارات البياناتية (Data frames). وسوف نبدأ بالدوال التي تتعامل مع صفوف الإطار البياناتي ثم أعمدته، لينعطف مسارنا بعد ذلك للحديث بتفصيل أعمق عن أداة التمرير والربط (The pipe)، وهي أداة محورية تُستخدم للدمج بين الدوال الإجرائية. بعد ذلك، سنعرض القدرة على التعامل مع المجموعات (Groups). ونختتم الفصل بدراسة حالة تطبيقية تعرض هذه الدوال سياقياً أثناء العمل. وفي فصول لاحقة، سنعود إلى هذه الدوال بمزيد من التفصيل لتفكيك كيفية التعامل مع أنواع محددة من البيانات (مثل: الأعداد، والنصوص، والتواريخ).
3.1.1 المتطلبات المسبقة
سينصب تركيزنا في هذا الفصل على حزمة dplyr، وهي عضو أساسي آخر في منظومة tidyverse. وسوف نوضح الأفكار الرئيسية للميزات البرمجية بالاعتماد على البيانات الواردة في حزمة nycflights13 مع الاستعانة بحزمة ggplot2 لمساعدتنا في فهم طبيعة البيانات واستكشافها.
يرجى الانتباه بدقة إلى رسالة “التعارضات” (Conflicts message) التي تُطبع عند تحميل حزمة tidyverse ؛ حيث تخبرك بأن حزمة dplyr تحل محل (تتجاوز) بعض الدوال الأساسية الموجودة في بيئة R الأساسية (Base R). فإذا كنت ترغب في استخدام النسخة الأساسية من هذه الدوال بعد تحميل dplyr، فستحتاج إلى استدعائها بأسمائها الكاملة والمحددة: stats::filter و stats::lag. وحتى الآن، كنا نتجاهل غالباً الحزمة التي تنتمي إليها الدالة نظراً لأن ذلك لا يشكل فارقاً في المعتاد. ومع ذلك، فإن معرفة الحزمة الأصلية يساعدك في الوصول إلى ملفات المساعدة والعثور على الدوال ذات الصلة؛ لذا، عندما نكون بحاجة إلى الدقة وتحديد الحزمة المصدر للدالة، سنستخدم نفس الصيغة البرمجية المعتمدة في R وهو: packagename::functionname().
3.1.2 nycflights13
لاستكشاف الدوال الإجرائية الأساسية في حزمة dplyr، سوف نستخدم الإطار البياناتي nycflights13::flights. تحتوي مجموعة البيانات هذه على كافة الرحلات الجوية البالغ عددها 336,776 رحلة والتي أقلعت من مدينة نيويورك في عام 2013. تم الحصول على هذه البيانات من مكتب إحصاءات النقل الأمريكي (Bureau of Transportation Statistics)، ويمكنك الاطلاع على توثيقها البرمجي عبر الأمر ?flights.
flights
#> # A tibble: 336,776 × 19
#> year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time
#> <int> <int> <int> <int> <int> <dbl> <int> <int>
#> 1 2013 1 1 517 515 2 830 819
#> 2 2013 1 1 533 529 4 850 830
#> 3 2013 1 1 542 540 2 923 850
#> 4 2013 1 1 544 545 -1 1004 1022
#> 5 2013 1 1 554 600 -6 812 837
#> 6 2013 1 1 554 558 -4 740 728
#> # ℹ 336,770 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …يُعد الكائن flights إطاراً بياناتياً من النوع tibble، وهو نوع خاص من الإطارات البياناتية يُعتمد عليه داخل منظومة tidyverse لتجنب بعض المشكلات البرمجية الشائعة. ويكمن الاختلاف الأكثر أهمية بين إطارات tibble والإطارات البياناتية التقليدية في طريقة استعراضها وطباعتها؛ حيث تم تصميمها للتعامل مع مجموعات البيانات الضخمة، ولذلك فهي لا تعرض سوى الأسطر القليلة الأولى والأعمدة التي تتسع لها شاشة العرض الواحدة فقط. وهناك خيارات متعددة لمعاينة البيانات بأكملها؛ فإذا كنت تستخدم بيئة RStudio، فإن الطريقة الأكثر ملاءمة هي استخدام الدالة View(flights)، والتي تفتح لك واجهة تفاعلية قابلة للتمرير والتصفية. وخلافاً لذلك، يمكنك استخدام الأمر print(flights, width = Inf) لعرض كافة الأعمدة, أو استخدام دالة اللمحة السريعة glimpse():
glimpse(flights)
#> Rows: 336,776
#> Columns: 19
#> $ year <int> 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013…
#> $ month <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
#> $ day <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
#> $ dep_time <int> 517, 533, 542, 544, 554, 554, 555, 557, 557, 558, 55…
#> $ sched_dep_time <int> 515, 529, 540, 545, 600, 558, 600, 600, 600, 600, 60…
#> $ dep_delay <dbl> 2, 4, 2, -1, -6, -4, -5, -3, -3, -2, -2, -2, -2, -2,…
#> $ arr_time <int> 830, 850, 923, 1004, 812, 740, 913, 709, 838, 753, 8…
#> $ sched_arr_time <int> 819, 830, 850, 1022, 837, 728, 854, 723, 846, 745, 8…
#> $ arr_delay <dbl> 11, 20, 33, -18, -25, 12, 19, -14, -8, 8, -2, -3, 7,…
#> $ carrier <chr> "UA", "UA", "AA", "B6", "DL", "UA", "B6", "EV", "B6"…
#> $ flight <int> 1545, 1714, 1141, 725, 461, 1696, 507, 5708, 79, 301…
#> $ tailnum <chr> "N14228", "N24211", "N619AA", "N804JB", "N668DN", "N…
#> $ origin <chr> "EWR", "LGA", "JFK", "JFK", "LGA", "EWR", "EWR", "LG…
#> $ dest <chr> "IAH", "IAH", "MIA", "BQN", "ATL", "ORD", "FLL", "IA…
#> $ air_time <dbl> 227, 227, 160, 183, 116, 150, 158, 53, 140, 138, 149…
#> $ distance <dbl> 1400, 1416, 1089, 1576, 762, 719, 1065, 229, 944, 73…
#> $ hour <dbl> 5, 5, 5, 5, 6, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 6, 6…
#> $ minute <dbl> 15, 29, 40, 45, 0, 58, 0, 0, 0, 0, 0, 0, 0, 0, 0, 59…
#> $ time_hour <dttm> 2013-01-01 05:00:00, 2013-01-01 05:00:00, 2013-01-0…وفي كلا العرضين، تتبع أسماء المتغيرات اختصارات نصية تُبين نوع كل متغير: فالاختصار <int> هو دلالة على العدد الصحيح (Integer)، والاختصار <dbl> دلالة على العدد الحقيقي المزدوج أو ما يُعرف بالأعداد الحقيقية (Double)، والاختصار <chr> دلالة على المتغير النصي أو الرمزي (Character)، بينما يشير <dttm> إلى التاريخ والوقت معاً (Date-time). وتكتسب هذه التسميات أهمية قصوى لأن العمليات الإحصائية والرياضية التي يمكنك إجراؤها على أي عمود تعتمد كلياً على تحديد “نوعه”.
3.1.3 أساسيات حزمة dplyr
أنت على وشك تعلم الدوال الإجرائية الأساسية لحزمة dplyr، والتي ستمكنك من حل الغالبية العظمى من تحديات معالجة وتشكيل البيانات. ولكن قبل أن نناقش الاختلافات الفردية لكل دالة، يجدر بنا توضيح الخصائص البرمجية المشتركة بينها:
المعامل أو الوسيط الأول (First argument) دائماً ما يكون إطاراً بياناتياً (Data frame).
المتغيرات والوسائط اللاحقة تصف عادةً الأعمدة التي سيتم إجراء العمليات عليها باستخدام أسماء المتغيرات مباشرة (دون الحاجة لوضعها بين علامات اقتباس).
المخرج الناتج (Output) يكون دائماً إطاراً بياناتياً جديداً.
ونظراً لأن كل دالة إجرائية تؤدي مهمة واحدة محددة بكفاءة عالية، فإن حل المشكلات المعقدة يتطلب عادةً الجمع والدمج بين دالات متعددة، وسنقوم بذلك باستخدام أداة التمرير والربط |>، سنناقش هذه الأداة بمزيد من التفصيل في قسم 3.4، ولكن باختصار، تأخذ أداة التمرير الكائن الموجود على يسارها وتمرره كمعامل أول للدالة الموجودة على يمينها؛ بحيث يصبح التعبير البرمجي x |> f(y) مكافئاً تماماً للتعبير f(x, y)، وبالمثل فإن التعبير x |> f(y) |> g(z) يكافئ تماماً التعبير g(f(x, y), z)، والطريقة الأسهل لنطق وقراءة أداة التمرير هي كلمة “ثم”. وهذا يتيح لك فهم المنطق العام للكود التالي حتى وإن لم تكن قد درست تفاصيله الإجرائية بعد:
تنقسم دوال حزمة dplyr إلى أربع مجموعات أساسية بناءً على الكيان الذي تجري العمليات عليه: الصفوف، أو الأعمدة، أو المجموعات، أو الجداول الكاملة، وفي الأقسام التالية، سوف تتعلم الدوال الأكثر أهمية للتعامل مع الصفوف والأعمدة والمجموعات. ومن ثم، سنعود لاحقاً لدوال الربط والدمج التي تعمل على الجداول في الفصل 19. فلنبدأ معاً!
3.2 الصفوف (Rows)
إن الدوال الإجرائية الأكثر أهمية والتي تتعامل مع صفوف مجموعة البيانات هي الدالة filter()، والتي تغير الصفوف الموجودة في العرض دون تعديل ترتيبها، والدالة arrange()، والتي تغير ترتيب الصفوف دون تعديل ماهية الصفوف الموجودة بالفعل. وتؤثر كلتا الدالتين على الصفوف فقط، بينما تظل الأعمدة دون أي تغيير. وسوف نناقش أيضاً الدالة distinct() التي تبحث عن الصفوف ذات القيم الفريدة وغير المكررة. وعلى العكس من الدالتين arrange() و filter()، يمكن للدالة distinct() أيضاً تعديل الأعمدة اختيارياً.
3.2.1 الدالة filter()
تتيح لك الدالة filter() الاحتفاظ بالصفوف بناءً على قيم الأعمدة1. والمعامل الأول للدالة هو الإطار البياناتي. أما المعامل الثاني والمعاملات اللاحقة فهي الشروط التي يجب أن تتحقق (أن تكون صائبة TRUE) للاحتفاظ بالصف. على سبيل المثال، يمكننا العثور على جميع الرحلات الجوية التي تأخرت في الإقلاع لأكثر من 120 دقيقة (ساعتين):
flights |>
filter(dep_delay > 120)
#> # A tibble: 9,723 × 19
#> year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time
#> <int> <int> <int> <int> <int> <dbl> <int> <int>
#> 1 2013 1 1 848 1835 853 1001 1950
#> 2 2013 1 1 957 733 144 1056 853
#> 3 2013 1 1 1114 900 134 1447 1222
#> 4 2013 1 1 1540 1338 122 2020 1825
#> 5 2013 1 1 1815 1325 290 2120 1542
#> 6 2013 1 1 1842 1422 260 1958 1535
#> # ℹ 9,717 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …إلى جانب أداة المقارنة > (أكبر من)، يمكنك استخدام >= (أكبر من أو يساوي)، و < (أقل من)، و <= (أقل من أو يساوي)، و == (يساوي)، و != (لا يساوي). كما يمكنك دمج الشروط باستخدام الأداة & أو الفاصلة , للإشارة إلى الرابط المنطقي “و” (التحقق من تحقق كلا الشرطين معاً)، أو استخدام الأداة | للإشارة إلى الرابط المنطقي “أو” (التحقق من تحقق أي من الشرطين):
# الرحلات الجوية التي أقلعت في الأول من يناير
flights |>
filter(month == 1 & day == 1)
#> # A tibble: 842 × 19
#> year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time
#> <int> <int> <int> <int> <int> <dbl> <int> <int>
#> 1 2013 1 1 517 515 2 830 819
#> 2 2013 1 1 533 529 4 850 830
#> 3 2013 1 1 542 540 2 923 850
#> 4 2013 1 1 544 545 -1 1004 1022
#> 5 2013 1 1 554 600 -6 812 837
#> 6 2013 1 1 554 558 -4 740 728
#> # ℹ 836 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …
# الرحلات الجوية التي أقلعت في شهر يناير أو فبراير
flights |>
filter(month == 1 | month == 2)
#> # A tibble: 51,955 × 19
#> year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time
#> <int> <int> <int> <int> <int> <dbl> <int> <int>
#> 1 2013 1 1 517 515 2 830 819
#> 2 2013 1 1 533 529 4 850 830
#> 3 2013 1 1 542 540 2 923 850
#> 4 2013 1 1 544 545 -1 1004 1022
#> 5 2013 1 1 554 600 -6 812 837
#> 6 2013 1 1 554 558 -4 740 728
#> # ℹ 51,949 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …وهناك اختصار مفيد للغاية عند دمج الأداة | مع أداة التساوي == وهو استخدام المعامل %in%؛ حيث يحتفظ بالصفوف التي يساوى فيها المتغير إحدى القيم الموجودة في الجانب الأيمن:
# طريقة أقصر لاختيار الرحلات الجوية التي أقلعت في شهر يناير أو فبراير
flights |>
filter(month %in% c(1, 2))
#> # A tibble: 51,955 × 19
#> year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time
#> <int> <int> <int> <int> <int> <dbl> <int> <int>
#> 1 2013 1 1 517 515 2 830 819
#> 2 2013 1 1 533 529 4 850 830
#> 3 2013 1 1 542 540 2 923 850
#> 4 2013 1 1 544 545 -1 1004 1022
#> 5 2013 1 1 554 600 -6 812 837
#> 6 2013 1 1 554 558 -4 740 728
#> # ℹ 51,949 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …وسوف نعود إلى هذه المقارنات والمعاملات المنطقية بمزيد من التفصيل في الفصل 12.
عند تشغيل الدالة filter()، تقوم حزمة dplyr بتنفيذ عملية التصفية، وإنشاء إطار بياناتي جديد، ثم طباعته. وهي لا تقوم بتعديل مجموعة بيانات flights الأصلية الحالية لأن دوال حزمة dplyr لا تغير مدخلاتها أبداً. ولحفظ النتيجة، تحتاج إلى استخدام معامل التعيين والوسم <-:
jan1 <- flights |>
filter(month == 1 & day == 1)3.2.2 أخطاء شائعة
عند بدء التعامل مع بيئة R، فإن الخطأ الأسهل في الوقوع هو استخدام علامة مساواة واحدة = بدلاً من علامتي مساواة == عند اختبار التساوي. وستقوم الدالة filter() بتنبيهك وإخطارك فور حدوث ذلك:
flights |>
filter(month = 1)
#> Error in `filter()`:
#> ! We detected a named input.
#> ℹ This usually means that you've used `=` instead of `==`.
#> ℹ Did you mean `month == 1`?ومن الأخطاء الأخرى صياغة عبارات الشرط المنطقي “أو” بنفس طريقة كتابتها باللغة الإنجليزية الجارية:
flights |>
filter(month == 1 | 2)هذا الكود “يعمل” من الناحية الإجرائية بمعنى أنه لا يظهر خطأ برمجياً، ولكنه لا يفعل ما تريده بدقة؛ لأن الأداة | تتحقق أولاً من الشرط month == 1 ثم تتحقق بعد ذلك من الشرط 2، وهو ليس شرطاً منطقياً معقولاً للتحقق منه. وسنتعلم المزيد عما يحدث هنا وأسبابه في قسم 12.3.2.
3.2.3 الدالة arrange()
تقوم الدالة arrange() بتغيير ترتيب الصفوف بناءً على قيم الأعمدة. وتأخذ الدالة إطاراً بياناتياً ومجموعة من أسماء الأعمدة (أو تعبيرات برمجية أكثر تعقيداً) للترتيب بناءً عليها. وإذا قمت بتقديم أكثر من اسم عمود واحد، فسيتم استخدام كل عمود إضافي لكسر التعادلات (Ties) في قيم الأعمدة السابقة. على سبيل المثال، يقوم الكود التالي بالفرز بناءً على وقت الإقلاع، والموزع على أربعة أعمدة؛ حيث نحصل على السنوات الأولى أولاً، ثم الأشهر الأولى داخل نفس السنة، وهكذا:
flights |>
arrange(year, month, day, dep_time)
#> # A tibble: 336,776 × 19
#> year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time
#> <int> <int> <int> <int> <int> <dbl> <int> <int>
#> 1 2013 1 1 517 515 2 830 819
#> 2 2013 1 1 533 529 4 850 830
#> 3 2013 1 1 542 540 2 923 850
#> 4 2013 1 1 544 545 -1 1004 1022
#> 5 2013 1 1 554 600 -6 812 837
#> 6 2013 1 1 554 558 -4 740 728
#> # ℹ 336,770 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …يمكنك استخدام الدالة العكسية desc على عمود ما داخل الدالة arrange() لإعادة ترتيب الإطار البياناتي بناءً على هذا العمود بترتيب تنازلي (من الأكبر إلى الأصغر). على سبيل المثال، يقوم هذا الكود بترتيب الرحلات الجوية من الأكثر تأخراً إلى الأقل تأخراً:
flights |>
arrange(desc(dep_delay))
#> # A tibble: 336,776 × 19
#> year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time
#> <int> <int> <int> <int> <int> <dbl> <int> <int>
#> 1 2013 1 9 641 900 1301 1242 1530
#> 2 2013 6 15 1432 1935 1137 1607 2120
#> 3 2013 1 10 1121 1635 1126 1239 1810
#> 4 2013 9 20 1139 1845 1014 1457 2210
#> 5 2013 7 22 845 1600 1005 1044 1815
#> 6 2013 4 10 1100 1900 960 1342 2211
#> # ℹ 336,770 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …لاحظ أن عدد الصفوف لم يتغير؛ فنحن نقوم فقط بإعادة ترتيب البيانات وتنسيقها، ولا نقوم بتصفيتها أو حذف شيء منها.
3.2.4 الدالة distinct()
تبحث الدالة distinct() عن جميع الصفوف الفريدة وغير المكررة في مجموعة البيانات، لذا فهي من الناحية التقنية تتعامل أساساً مع الصفوف. ومع ذلك، فإنك في معظم الأوقات ستكون بحاجة إلى استخراج التوليفات الفريدة لبعض المتغيرات المحددة، ولذلك يمكنك أيضاً تمرير أسماء الأعمدة اختيارياً:
# إزالة الصفوف المكررة، إن وجدت
flights |>
distinct()
#> # A tibble: 336,776 × 19
#> year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time
#> <int> <int> <int> <int> <int> <dbl> <int> <int>
#> 1 2013 1 1 517 515 2 830 819
#> 2 2013 1 1 533 529 4 850 830
#> 3 2013 1 1 542 540 2 923 850
#> 4 2013 1 1 544 545 -1 1004 1022
#> 5 2013 1 1 554 600 -6 812 837
#> 6 2013 1 1 554 558 -4 740 728
#> # ℹ 336,770 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …
# العثور على جميع ثنائيات المطارات الأصلية ومطارات الوصول الفريدة
flights |>
distinct(origin, dest)
#> # A tibble: 224 × 2
#> origin dest
#> <chr> <chr>
#> 1 EWR IAH
#> 2 LGA IAH
#> 3 JFK MIA
#> 4 JFK BQN
#> 5 LGA ATL
#> 6 EWR ORD
#> # ℹ 218 more rowsوبخلاف ذلك، إذا كنت ترغب في الاحتفاظ بالأعمدة الأخرى عند التصفية بحثاً عن الصفوف الفريدة، يمكنك استخدام خيار ضبط المعامل .keep_all = TRUE:
flights |>
distinct(origin, dest, .keep_all = TRUE)
#> # A tibble: 224 × 19
#> year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time
#> <int> <int> <int> <int> <int> <dbl> <int> <int>
#> 1 2013 1 1 517 515 2 830 819
#> 2 2013 1 1 533 529 4 850 830
#> 3 2013 1 1 542 540 2 923 850
#> 4 2013 1 1 544 545 -1 1004 1022
#> 5 2013 1 1 554 600 -6 812 837
#> 6 2013 1 1 554 558 -4 740 728
#> # ℹ 218 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …وليس من قبيل المصادفة أن تكون جميع هذه الرحلات الفريدة المستخرجة مصادفة لتاريخ الأول من يناير؛ فالدالة distinct() تبحث عن أول ظهور للصف الفريد في مجموعة البيانات وتتخلص من الباقي تلقائياً.
وإذا كنت ترغب في معرفة عدد مرات التكرار والظهور بدلاً من ذلك، فمن الأفضل استبدال الدالة distinct() بالدالة count(). وباستخدام وسيط الضبط sort = TRUE، يمكنك ترتيبها تنازلياً بناءً على عدد مرات الظهور والتكرار. وسوف تتعلم المزيد عن العد والتكرارات في قسم 13.3.
flights |>
count(origin, dest, sort = TRUE)
#> # A tibble: 224 × 3
#> origin dest n
#> <chr> <chr> <int>
#> 1 JFK LAX 11262
#> 2 LGA ATL 10263
#> 3 LGA ORD 8857
#> 4 JFK SFO 8204
#> 5 LGA CLT 6168
#> 6 EWR ORD 6100
#> # ℹ 218 more rows3.2.5 تمارين
-
في أنبوب برمي واحد (Pipeline) مستقل لكل شرط مما يلي، أوجد جميع الرحلات الجوية التي تستوفي المعايير التالية:
- الرحلات التي واجهت تأخراً في الوصول بمقدار ساعتين أو أكثر.
- الرحلات التي توجهت إلى هيوستن Houston (
IAHأوHOU). - الرحلات التي تم تشغيلها بواسطة شركات الطيران: يونايتد (United)، أو أمريكان (American)، أو ديلتا (Delta).
- الرحلات التي أقلعت في فصل الصيف (أشهر يوليو، وأغسطس، وسبتمبر).
- الرحلات التي وصلت متأخرة بأكثر من ساعتين ولكنها لم تفرز تأخراً في الإقلاع.
- الرحلات التي تأخرت بمقدار ساعة على الأقل، ولكنها عوضت أكثر من 30 دقيقة أثناء الطيران.
قم بفرز بيانات
flightsللعثور على الرحلات الجوية التي سجلت أطول فترات تأخير في الإقلاع. وأوجد الرحلات التي غادرت في وقت مبكر جداً من الصباح.قم بفرز بيانات
flightsللعثور على أسرع الرحلات الجوية. (تلميح: جرب إدراج عملية حسابية رياضية داخل الدالة).هل كانت هناك رحلة جوية في كل يوم من أيام سنة 2013؟
ما هي الرحلات الجوية التي قطعت أطول مسافة سفر؟ وما هي الرحلات التي قطعت أقل مسافة؟
هل يشكل الترتيب الذي استخدمت به الدالتين
filter()وarrange()فارقاً إذا كنت تستخدم كلتيهما معاً؟ ولماذا أو لم لا؟ فكر في النتائج المستخرجة وحجم العمل البرمجي الحسابي الذي يتعين على الدوال القيام به في كلتا الحالتين.
3.3 الأعمدة (Columns)
هناك أربع دوال إجرائية مهمة تؤثر على الأعمدة دون تغيير الصفوف: الدالة mutate() وتعمل على إنشاء أعمدة جديدة يتم اشتقاقها من الأعمدة الحالية، والدالة select() وتغير الأعمدة التي تظهر في العرض، والدالة rename() وتغير أسماء الأعمدة، والدالة relocate() وتغير مواضع الأعمدة وترتيبها الحيزي.
3.3.1 الدالة mutate()
تتمثل وظيفة الدالة mutate() في إضافة أعمدة جديدة يتم حساب قيمها بناءً على الأعمدة الحالية بالفعل. وفي الفصول الخاصة بتحويل وتشكيل البيانات، ستتعرف على مجموعة واسعة من الدوال التي يمكنك استخدامها لمعالجة الأنواع المختلفة من المتغيرات. أما في الوقت الحالي، سنلتزم بالعمليات الجبرية الأساسية، والتي تتيح لنا حساب مقدار المكسب في الوقت gain (أي مقدار الوقت الذي عوضته الرحلة المتأخرة أثناء طيرانها في الجو)، وحساب السرعة speed مقدرة بالمسافة ميل/ساعة:
flights |>
mutate(
gain = dep_delay - arr_delay,
speed = distance / air_time * 60
)
#> # A tibble: 336,776 × 21
#> year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time
#> <int> <int> <int> <int> <int> <dbl> <int> <int>
#> 1 2013 1 1 517 515 2 830 819
#> 2 2013 1 1 533 529 4 850 830
#> 3 2013 1 1 542 540 2 923 850
#> 4 2013 1 1 544 545 -1 1004 1022
#> 5 2013 1 1 554 600 -6 812 837
#> 6 2013 1 1 554 558 -4 740 728
#> # ℹ 336,770 more rows
#> # ℹ 13 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …بشكل افتراضي، تقوم الدالة mutate() بإضافة الأعمدة الجديدة في الجانب الأيمن (الطرف النهائي) لمجموعة البيانات الخاصة بك، مما يجعل من الصعب رؤية ما يحدث هنا بدقة. ويمكننا استخدام وسيط الضبط .before لإضافة هذه المتغيرات الجديدة في الجانب الأيسر (الطرف البداياتي) بدلاً من ذلك2:
flights |>
mutate(
gain = dep_delay - arr_delay,
speed = distance / air_time * 60,
.before = 1
)
#> # A tibble: 336,776 × 21
#> gain speed year month day dep_time sched_dep_time dep_delay arr_time
#> <dbl> <dbl> <int> <int> <int> <int> <int> <dbl> <int>
#> 1 -9 370. 2013 1 1 517 515 2 830
#> 2 -16 374. 2013 1 1 533 529 4 850
#> 3 -31 408. 2013 1 1 542 540 2 923
#> 4 17 517. 2013 1 1 544 545 -1 1004
#> 5 19 394. 2013 1 1 554 600 -6 812
#> 6 -16 288. 2013 1 1 554 558 -4 740
#> # ℹ 336,770 more rows
#> # ℹ 12 more variables: sched_arr_time <int>, arr_delay <dbl>, …تشير النقطة . المسبقة إلى أن الكائن .before هو وسيط برمي تابع للدالة نفسها، وليس اسماً لمتغير جديد ثالث نقوم بإنشائه. كما يمكنك أيضاً استخدام الوسيط البرمجي .after لإضافة الأعمدة بعد متغير معين، وفي كلتا حالتي الضبط التموضعي .before و .after يمكنك استخدام اسم المتغير مباشرةً بدلاً من الإشارة إلى ترتيبه الرقمي. على سبيل المثال، يمكننا إضافة المتغيرات الجديدة مباشرةً بعد عمود اليوم day:
flights |>
mutate(
gain = dep_delay - arr_delay,
speed = distance / air_time * 60,
.after = day
)وبدلاً من ذلك، يمكنك التحكم في المتغيرات التي يتم الاحتفاظ بها في المخرج النهائي باستخدام وسيط التحكم .keep. ويُعد المعامل اللفظي "used" خياراً مفيداً للغاية؛ حيث يحدد الاقتصار فقط على الاحتفاظ بالأعمدة التي شاركت في العمليات الحسابية أو التي تم إنشاؤها خلال خطوة استدعاء الدالة mutate(). على سبيل المثال، لن يحتوي المخرج البرمجي التالي إلا على المتغيرات المحددة التالية: dep_delay و arr_delay و air_time و gain و hours و gain_per_hour:
flights |>
mutate(
gain = dep_delay - arr_delay,
hours = air_time / 60,
gain_per_hour = gain / hours,
.keep = "used"
)لاحظ أنه نظراً لأننا لم نقم بتعيين وإسناد نتيجة الحسابات البرمجية أعلاه مجدداً إلى الكائن flights، فإن المتغيرات الجديدة gain و hours و gain_per_hour ستُطبع فقط في العرض المعروض ولكن لن يتم تخزينها داخل إطار البيانات المستديم. وإذا كنا نرغب في جعلها متاحة للاستخدام المستقبلي في إطار بياناتي، فيتعين علينا التفكير بعناية فيما إذا كنا نريد إسناد النتيجة مجدداً إلى flights (وبالتالي استبدال الإطار البياناتي الأصلي بإطار آخر يحتوي على متغيرات كثيرة إضافية)، أم إسنادها إلى كائن برمجي جديد مستقل. وغالباً ما تكون الإجابة الصحيحة هي إنشاء كائن جديد وتسميته باسم دال وواضح يشير إلى محتوياته بدقة، مثل delay_gain، ورغم ذلك قد تكون لديك أسباب منهجية وجيهة تدعوك لاستبدال الكائن الأصلي flights وتعديله.
3.3.2 الدالة select()
ليس من الغريب أو النادر الحصول على مجموعات بيانات تشتمل على مئات أو حتى آلاف المتغيرات. وفي مثل هذه المواقف الإحصائية، غالباً ما يتمثل التحدي الأول في التركيز فقط على المتغيرات التي تقع في دائرة اهتمامك البحثي. تتيح لك الدالة select() إمكانية التقريب والتركيز السريع (Zoom in) على مجموعة فرعية مفيدة ومحددة من البيانات باستخدام عمليات تعتمد على أسماء المتغيرات كالتالي:
-
اختيار الأعمدة بناءً على أسمائها المحددة:
flights |> select(year, month, day) -
اختيار جميع الأعمدة الواقعة بين عمود السنة
yearوعمود اليومday(بما في ذلك العمودين الطرفيين):flights |> select(year:day) -
اختيار جميع الأعمدة باستثناء الأعمدة الممتدة من السنة
yearإلى اليومday(بما في ذلك العمودين الطرفيين):flights |> select(!year:day)تاريخياً، كانت هذه العملية تُجرى باستخدام الإشارة الحسابية
-بدلاً من أداة النفي المنطقية!، ولذا فمن المحتمل جداً أن تصادف تلك الصيغة القديمة في الأكواد المنتشرة. ويؤدي كلا المعاملين نفس الغرض البرمجي ولكن مع وجود اختلافات طفيفة ودقيقة في سلوك المعالجة. ونحن نوصي باستخدام الأداة المنطقية!لأنها تُقرأ صراحة بمعنى “النفي/ليس” (Not)، فضلاً عن مرونتها العالية في الاندماج والربط مع الأدوات المنطقية الأخرى مثل&و|. -
اختيار جميع الأعمدة ذات الطبيعة النصية أو الرمزية (Characters):
وهناك عدد من الدوال المساعدة والمساندة (Helper functions) التي يمكنك توظيفها واستخدامها بمرونة داخل الدالة select()، ومن أبرزها:
-
starts_with("abc"): لمطابقة واختيار الأسماء التي تبدأ بالمقطع النصي “abc”. -
ends_with("xyz"): لمطابقة واختيار الأسماء التي تنتهي بالمقطع النصي “xyz”. -
contains("ijk"): لمطابقة واختيار الأسماء التي تحتوي في أي من أجزائها على المقطع النصي “ijk”. -
num_range("x", 1:3): لمطابقة الأعمدة ذات التسلسل الرقمي الممنهج مثلx1وx2وx3.
ولمزيد من التفاصيل الإجرائية، يمكنك مراجعة دليل المساعدة عبر الأمر ?select. وبمجرد تمكنك من فهم التعبيرات النمطية المنتظمة (Regular expressions) (وهو الموضوع المحوري الذي سيتم تناوله في الفصل 15)، ستصبح قادراً أيضاً على استخدام الدالة الذكية matches لاختيار المتغيرات التي تطابق نمطاً بنيوياً محدداً.
ويمكنك أيضاً إعادة تسمية المتغيرات والأعمدة بالتزامن مع عملية اختيارها داخل الدالة select() باستخدام علامة التساوي =. حيث يظهر الاسم الجديد المقترح في الجانب الأيسر من علامة المساواة =، بينما يوضع اسم المتغير الأصلي القديم في الجانب الأيمن منها:
flights |>
select(tail_num = tailnum)
#> # A tibble: 336,776 × 1
#> tail_num
#> <chr>
#> 1 N14228
#> 2 N24211
#> 3 N619AA
#> 4 N804JB
#> 5 N668DN
#> 6 N39463
#> # ℹ 336,770 more rows
3.3.3 الدالة rename()
إذا كنت ترغب في الاحتفاظ بجميع المتغيرات الحالية والرغبة فقط في إعادة تسمية عدد قليل منها، يمكنك استخدام الدالة rename() بدلاً من select():
flights |>
rename(tail_num = tailnum)
#> # A tibble: 336,776 × 19
#> year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time
#> <int> <int> <int> <int> <int> <dbl> <int> <int>
#> 1 2013 1 1 517 515 2 830 819
#> 2 2013 1 1 533 529 4 850 830
#> 3 2013 1 1 542 540 2 923 850
#> 4 2013 1 1 544 545 -1 1004 1022
#> 5 2013 1 1 554 600 -6 812 837
#> 6 2013 1 1 554 558 -4 740 728
#> # ℹ 336,770 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …وإذا كانت لديك مجموعة من الأعمدة ذات الأسماء غير المتسقة والمبعثرة وكان من المرهق تعديلها يدوياً بالكامل، فننصحك بمراجعة حزمة janitor واستخدام الدالة janitor::clean_names التي توفر آليات تنظيف وتنسيق مؤتمتة ومفيدة للغاية.
3.3.4 الدالة relocate()
تُستخدم الدالة relocate() لتغيير المواضع المكانية للمتغيرات ونقلها. قد ترغب في تجميع المتغيرات ذات الصلة ببعضها البعض أو نقل المتغيرات المهمة إلى الصدارة البداياتية للإطار البياناتي. وبشكل افتراضي، تقوم الدالة relocate() بنقل المتغيرات المحددة إلى الصدارة الأولى:
flights |>
relocate(time_hour, air_time)
#> # A tibble: 336,776 × 19
#> time_hour air_time year month day dep_time sched_dep_time
#> <dttm> <dbl> <int> <int> <int> <int> <int>
#> 1 2013-01-01 05:00:00 227 2013 1 1 517 515
#> 2 2013-01-01 05:00:00 227 2013 1 1 533 529
#> 3 2013-01-01 05:00:00 160 2013 1 1 542 540
#> 4 2013-01-01 05:00:00 183 2013 1 1 544 545
#> 5 2013-01-01 06:00:00 116 2013 1 1 554 600
#> 6 2013-01-01 05:00:00 150 2013 1 1 554 558
#> # ℹ 336,770 more rows
#> # ℹ 12 more variables: dep_delay <dbl>, arr_time <int>, …ويمكنك أيضاً تحديد المكان الدقيق لوضعها باستخدام وسيطي الضبط التموضعي .before و .after، تماماً كما رأينا في الدالة mutate():
flights |>
relocate(year:dep_time, .after = time_hour)
flights |>
relocate(starts_with("arr"), .before = dep_time)3.3.5 تمارين
قارن بين المتغيرات التالية:
dep_timeوsched_dep_timeوdep_delay. كيف تتوقع أن تكون العلاقة الرياضية بين هذه الأعداد الثلاثة؟اعصف ذهنك لابتكار أكبر عدد ممكن من الطرق البرمجية المختلفة لاختيار المتغيرات
dep_timeوdep_delayوarr_timeوarr_delayمن الإطار البياناتيflights.ماذا يحدث إذا قمت بتحديد اسم نفس المتغير عدة مرات مكررة داخل استدعاء الدالة
select()؟ما الوظيفة البرمجية التي تؤديها الدالة المساعدة
any_of()؟ ولماذا قد تكون مفيدة للغاية عند دمجها مع المتجه النصي التالي؟
variables <- c("year", "month", "day", "dep_delay", "arr_delay")- هل تسببت نتيجة تشغيل الكود التالي في مفاجأتك؟ وكيف تتعامل الدوال المساعدة لعملية الاختيار (Select helpers) مع حالة الأحرف (الكبيرة والصغيرة Upper and Lower case) بشكل افتراضي؟ وكيف يمكنك تغيير هذا السلوك الافتراضي؟
أعد تسمية المتغير
air_timeليصبحair_time_minللإشارة الصريحة إلى وحدة القياس المستخدمة (الدقائق)، ثم انقله إلى الصدارة البداياتية للإطار البياناتي.لماذا لا يعمل الكود التالي، وما المفهوم الإجرائي الذي تعنيه رسالة الخطأ الظاهرة؟
3.4 أداة التمرير والربط (The pipe)
لقد عرضنا لك أمثلة بسيطة لأداة التمرير والربط في الأجزاء السابقة، ولكن القوة الحقيقية الطاغية لهذه الأداة تبرز بوضوح عندما تبدأ في الدمج المتسلسل بين دالات إجرائية متعددة. على سبيل المثال، تخيل أنك تريد العثور على أسرع الرحلات الجوية المتوجهة إلى مطار هيوستن (IAH): هنا ستحتاج إلى الدمج المنظم بين الدوال التالية: filter() و mutate() و select() و arrange():
flights |>
filter(dest == "IAH") |>
mutate(speed = distance / air_time * 60) |>
select(year:day, dep_time, carrier, flight, speed) |>
arrange(desc(speed))
#> # A tibble: 7,198 × 7
#> year month day dep_time carrier flight speed
#> <int> <int> <int> <int> <chr> <int> <dbl>
#> 1 2013 7 9 707 UA 226 522.
#> 2 2013 8 27 1850 UA 1128 521.
#> 3 2013 8 28 902 UA 1711 519.
#> 4 2013 8 28 2122 UA 1022 519.
#> 5 2013 6 11 1628 UA 1178 515.
#> 6 2013 8 27 1017 UA 333 515.
#> # ℹ 7,192 more rowsوعلى الرغم من أن هذا الأنبوب البرمجي (Pipeline) يشتمل على أربع خطوات متتالية، إلا أنه من السهل جداً تصفحه ومتابعته بصرياً نظراً لأن الدوال الإجرائية تأتي صراحة في مطلع كل سطر برمي: ابدأ ببيانات الرحلات flights، ثم قم بالتصفية، ثم التحوير والإنشاء، ثم الاختيار، ثم الترتيب.
ماذا كان سيحدث لو لم نكن نملك أداة التمرير والربط؟ كان سيتعين علينا تعشيق (Nest) كل دالة مستدعاة داخل الدالة السابقة لها مباشرة كالتالي:
أو كان سيتعين علينا إنشاء واستخدام مجموعة ضخمة ومربكة من الكائنات والوسائط المتغيرة الوسيطة (Intermediate objects):
ورغم أن لكلتا الصيغتين السابقتين سياقاتها وأسبابها المنهجية الخاصة أحياناً، إلا أن أداة التمرير تُنتج عموماً كوداً لتحليل البيانات يكون أسهل بكثير في الكتابة والقراءة والمتابعة.
ولإدراج أداة التمرير والربط داخل كودك البرمجي، نوصيك باستخدام اختصار لوحة المفاتيح المدمج: Ctrl/Cmd + Shift + M، وستحتاج إلى إجراء تغيير واحد بسيط في خيارات وإعدادات بيئة RStudio لتفعيل أداة التمرير الأصيلة |> بدلاً من الأداة التقليدية %>% كما هو موضح في الشكل 3.1؛ وسنتحدث عن %>% بعد قليل.
|>، تأكد من تفعيل خيار “Use native pipe operator”.
إذا كنت تستخدم منظومة tidyverse لفترة من الوقت، فقد تكون مألوفاً لديك أداة التمرير والربط %>% التي توفرها حزمة magrittr. تُعد حزمة magrittr مدمجة تلقائياً داخل النواة الأساسية لمنظومة tidyverse، ولذلك يمكنك استخدام %>% مباشرة بمجرد تحميل حزمة tidyverse:
في الحالات البرمجية البسيطة، تتطابق أداة التمرير الأصيلة |> وأداة التمرير %>% تماماً في السلوك الإجرائي. إذن، لماذا نوصي بشدة باستخدام أداة التمرير الأصيلة (Base pipe)؟ أولاً، لأنها جزء لا يتجزأ من البيئة الأساسية للغة R الأساسية (Base R)، فهي متاحة دائماً للاستخدام الفوري، حتى عندما لا تقوم بتحميل منظومة tidyverse. ثانياً، لأن الأداة |> أبسط بكثير من الأداة %>%؛ فخلال الفترة الزمنية الممتدة بين ابتكار %>% عام 2014 وإدراج |> في إصدار R 4.1.0 عام 2021، اكتسب المجتمع البرمجي فهماً أعمق وأدق لآلية عمل أنابيب التمرير، وهو ما سمح للتضمين الأصيل داخل لغة R بالتخلص من الميزات النادرة الاستخدام والأقل أهمية لتقديم أداة رشيقة وعالية الكفاءة.
3.5 المجموعات (Groups)
تعلمت حتى الآن الدوال الإجرائية التي تعمل مباشرة مع الصفوف والأعمدة. ولكن حزمة dplyr تكتسب قوة إحصائية وحسابية أكبر بكثير عندما تضيف إليها القدرة على التعامل مع المجموعات وتجزئة البيانات. وسنركز في هذا القسم على الدوال الأكثر أهمية وهي: group_by() و summarize() وعائلة دوال القطع الحيزي slice_.
3.5.1 الدالة group_by()
تُستخدم الدالة group_by() لتقسيم وتجزئة مجموعة البيانات الخاصة بك إلى مجموعات ذات دلالة ومعنى تخدم أهدافك التحليلية:
flights |>
group_by(month)
#> # A tibble: 336,776 × 19
#> # Groups: month [12]
#> year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time
#> <int> <int> <int> <int> <int> <dbl> <int> <int>
#> 1 2013 1 1 517 515 2 830 819
#> 2 2013 1 1 533 529 4 850 830
#> 3 2013 1 1 542 540 2 923 850
#> 4 2013 1 1 544 545 -1 1004 1022
#> 5 2013 1 1 554 600 -6 812 837
#> 6 2013 1 1 554 558 -4 740 728
#> # ℹ 336,770 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …لا تقوم الدالة group_by() بتغيير البيانات أو تعديل قيمها، ولكن إذا نظرت بعناية إلى المخرج المطبوع، ستلاحظ أن النتيجة تشير صراحة إلى أنها “مجمعة بناءً على” الأشهر (Groups: month [12]). وهذا يعني بنيوياً أن العمليات الإجرائية اللاحقة ستعمل الآن “لكل شهر على حدة”. تقوم الدالة group_by() بإضافة هذه الخاصية التجميعية (والتي تُعرف برمجياً بالـ Class) إلى الإطار البياناتي، مما يغير تماماً من سلوك الدوال الإجرائية التالية المطبقة على البيانات.
3.5.2 الدالة summarize()
تُعد عملية التلخيص واستخراج الإحصاءات التجميعية هي العملية الأكثر أهمية عند التعامل مع المجموعات؛ فإذا تم استخدامها لحساب معلَمة إحصائية ملخصة واحدة، فإنها تختزل الإطار البياناتي ليحتوي على صف واحد فريد لكل مجموعة. وفي حزمة dplyr، يتم تنفيذ هذه العملية بواسطة الدالة summarize()3, كما يظهر في المثال التالي الذي يحسب متوسط تأخر الإقلاع مقسماً حسب الأشهر:
عفواً! لقد حدث خطأ ما، وظهرت جميع نتائجنا على هيئة قيم مفقودة NA (والتي تُنطق اختصاراً لقيم غير متاحة Missing values)، وهي الرمز المعتمد في لغة R للإشارة إلى البيانات المفقودة. حدث هذا لأن بعض الرحلات الجوية المرصودة تشتمل على بيانات مفقودة في عمود التأخير، وبالتالي فعندما قمنا بحساب المتوسط الحسابي متضمناً تلك القيم، حصلنا حتماً على نتيجة مفقودة NA. وسوف نعود لمناقشة البيانات المفقودة بالتفصيل في الفصل 18، ولكن في الوقت الحالي، سنوجه دالة المتوسط mean() لتجاهل كافة القيم المفقودة عن طريق ضبط وسيط الاستبعاد na.rm = TRUE:
ويمكنك إنشاء أي عدد تريده من الملخصات الإحصائية داخل استدعاء واحد للدالة summarize(). وستتعلم ملخصات ودوال إحصائية متنوعة ومفيدة في الفصول القادمة، ولكن من أبرز الدوال المفيدة للغاية هي الدالة n()، والتي تعيد عدد الصفوف (التكرارات) الموجودة في كل مجموعة:
إن المتوسطات الحسابية وحساب التكرارات والعد كفيلان بنقلك إلى آفاق بعيدة وعميقة جداً في ميدان علم البيانات!
3.5.3 عائلة دوال القطع والاستخلاص slice_
هناك خمس دالات عملية وذكية تتيح لك استخلاص صفوف محددة بدقة من داخل كل مجموعة تجميعية:
-
df |> slice_head(n = 1): لاستخلاص الصف الأول من كل مجموعة. -
df |> slice_tail(n = 1): لاستخلاص الصف الأخير من كل مجموعة. -
df |> slice_min(x, n = 1): لاستخلاص الصف الذي يحتوي على أقل قيمة في العمودx. -
df |> slice_max(x, n = 1): لاستخلاص الصف الذي يحتوي على أكبر قيمة في العمودx. -
df |> slice_sample(n = 1): لاستخلاص صف عشوائي واحد من كل مجموعة.
ويمكنك تغيير قيمة المعامل n لاختيار أكثر من صف واحد، أو يمكنك بدلاً من استخدام التحديد العددي n = أن تستخدم تحديد النسبة المئوية مثل prop = 0.1 لاختيار (على سبيل المثال) 10% من الصفوف داخل كل مجموعة. على سبيل المثال، يقوم الكود التالي بالعثور على الرحلات الجوية التي سجلت أطول فترات تأخير عند الوصول لكل وجهة مطار مقصودة:
flights |>
group_by(dest) |>
slice_max(arr_delay, n = 1) |>
relocate(dest)
#> # A tibble: 108 × 19
#> # Groups: dest [105]
#> dest year month day dep_time sched_dep_time dep_delay arr_time
#> <chr> <int> <int> <int> <int> <int> <dbl> <int>
#> 1 ABQ 2013 7 22 2145 2007 98 132
#> 2 ACK 2013 7 23 1139 800 219 1250
#> 3 ALB 2013 1 25 123 2000 323 229
#> 4 ANC 2013 8 17 1740 1625 75 2042
#> 5 ATL 2013 7 22 2257 759 898 121
#> 6 AUS 2013 7 10 2056 1505 351 2347
#> # ℹ 102 more rows
#> # ℹ 11 more variables: sched_arr_time <int>, arr_delay <dbl>, …لاحظ أن هناك 105 وجهة مطار مقصودة ولكننا حصلنا في المخرج على 108 صفاً! ما السبب وراء ذلك؟ السبب هو أن الدالتين slice_min() و slice_max() تحتفظان بالقيم المتعادلة (Tied values) تلقائياً، وبالتالي فإن ضبط المعامل n = 1 يعني: أعطنا جميع الصفوف التي تتشارك أعلى قيمة عظمى دون استبعاد. وإذا كنت ترغب في الحصول على صف واحد فريد تماماً لكل مجموعة، يمكنك إيقاف هذا السلوك عبر ضبط وسيط التعادلات with_ties = FALSE.
هذا الإجراء البرمجي يشبه إلى حد كبير حساب التأخير الأقصى باستخدام الدالة summarize()، ولكن الميزة هنا هي الحصول على الصف بأكمله بكافة متغيراته (أو الصفوف المتعددة في حالة وجود تعادلات) بدلاً من الحصول على قيمة إحصائية ملخصة منفردة.
3.5.4 التجميع بناءً على متغيرات متعددة
يمكنك إنشاء مجموعات تجميعية بالاعتماد على أكثر من متغير واحد بالتزامن. على سبيل المثال، يمكننا إنشاء مجموعات تجميعية فرعية لكل تاريخ يومي مستقل:
daily <- flights |>
group_by(year, month, day)
daily
#> # A tibble: 336,776 × 19
#> # Groups: year, month, day [365]
#> year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time
#> <int> <int> <int> <int> <int> <dbl> <int> <int>
#> 1 2013 1 1 517 515 2 830 819
#> 2 2013 1 1 533 529 4 850 830
#> 3 2013 1 1 542 540 2 923 850
#> 4 2013 1 1 544 545 -1 1004 1022
#> 5 2013 1 1 554 600 -6 812 837
#> 6 2013 1 1 554 558 -4 740 728
#> # ℹ 336,770 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …عندما تقوم بتلخيص إطار بياناتي من النوع tibble مجمع بناءً على أكثر من متغير واحد، فإن كل عملية تلخيص تقوم تلقائياً بتقشير (Peel off) الطبقة التجميعية الأخيرة وإزالتها. وبأثر رجعي، لم تكن هذه الطريقة هي المثالية المطلقة لتصميم عمل الدالة، ولكن من الصعب جداً تعديل هذا السلوك الآن دون كسر وإعطاب الأكواد البرمجية القائمة والمنتشرة عالمياً. ولجعل ما يحدث واضحاً تماماً أمام عينيك، تعرض حزمة dplyr رسالة تنبيهية تخبرك بكيفية تعديل هذا السلوك والتحكم فيه:
daily_flights <- daily |>
summarize(n = n())
#> `summarise()` has regrouped the output.
#> ℹ Summaries were computed grouped by year, month, and day.
#> ℹ Output is grouped by year and month.
#> ℹ Use `summarise(.groups = "drop_last")` to silence this message.
#> ℹ Use `summarise(.by = c(year, month, day))` for per-operation grouping
#> (`?dplyr::dplyr_by`) instead.وإذا كنت راضياً تماماً عن هذا السلوك الافتراضي، يمكنك طلبه صراحةً وبشكل مباشر من أجل كتم وإخفاء ظهور رسالة التنبيه البرمجية كالتالي:
وبدلاً من ذلك، يمكنك تغيير السلوك الافتراضي بأكمله عن طريق تعيين قيمة مختلفة لمعامل المجموعات، مثل ضبط الخيار "drop" لإلغاء وإسقاط كافة المستويات التجميعية تماماً، أو ضبط الخيار "keep" للحفاظ على نفس المجموعات التجميعية الأصلية دون أي تقشير.
3.5.5 إلغاء التجميع (Ungrouping)
قد ترغب أيضاً في إزالة وإسقاط الخصائص التجميعية تماماً من الإطار البياناتي دون الحاجة لتمريره عبر دالة التلخيص summarize(). ويمكنك القيام بذلك بمنتهى السهولة باستخدام الدالة ungroup():
daily |>
ungroup()
#> # A tibble: 336,776 × 19
#> year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time
#> <int> <int> <int> <int> <int> <dbl> <int> <int>
#> 1 2013 1 1 517 515 2 830 819
#> 2 2013 1 1 533 529 4 850 830
#> 3 2013 1 1 542 540 2 923 850
#> 4 2013 1 1 544 545 -1 1004 1022
#> 5 2013 1 1 554 600 -6 812 837
#> 6 2013 1 1 554 558 -4 740 728
#> # ℹ 336,770 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …والآن، دعنا نرى ماذا يحدث عندما نقوم بتطبيق دالة التلخيص على إطار بياناتي تم إلغاء تجميعه بالفعل:
ستحصل في المخرج على صف واحد فريد فقط؛ لأن حزمة dplyr تتعامل مع كافة الصفوف في الإطار البياناتي غير المجمع بصفتها تنتمي بأكملها إلى مجموعة تجميعية واحدة كبرى.
3.5.6 المعامل والوسيط المباشر .by
تتضمن حزمة dplyr بدءاً من الإصدار 1.1.0 صيغة برمجية جديدة وتجريبية لإجراء التجميع المؤقت والمباشر المخصص لكل عملية على حدة، وذلك باستخدام الوسيط البرمجي .by. وللعلم، فإن الدالتين group_by() و ungroup() باقيتان ولن تختفيا، ولكن أصبح بمقدورك الآن استخدام وسيط الضبط المباشر .by للتجميع ضمن عملية واحدة محددة تنتهى بانتهاء السطر البرمجي:
أو إذا كنت ترغب في التجميع الفوري بناءً على متغيرات متعددة معاً:
يعمل المعامل الذكي .by بمرونة عالية مع جميع الدوال الإجرائية (Verbs)، ويتميز بميزة بنيوية قوية وهي عدم حاجتك نهائياً لاستخدام وسيط التحكم .groups لكتم رسالة التجميع، كما يغنيك تماماً عن استدعاء الدالة ungroup() عند انتهائك من التحليل.
ولم نقم بالتركيز المكثف على هذه الصيغة البرمجية في ثنايا هذا الفصل نظراً لأنها كانت حديثة العهد للغاية أثناء تأليف هذا الكتاب، ومع ذلك حرصنا على الإشارة إليها لعلمنا اليقين بأنها تحمل آفاقاً واعدة جداً وتطوراً كبيراً، ومن المتوقع جداً أن تحظى بشعبية طاغية بين محللي البيانات. ويمكنك التوسع والاطلاع على المزيد من تفاصيلها الإجرائية عبر المقال المخصص لإطلاق تحديث dplyr 1.1.0 blog post.
3.5.7 تمارين
أي من شركات الطيران (Carriers) تمتلك أسوأ متوسط فترات تأخير؟ تحدٍّ: هل يمكنك الفصل وتفكيك التداخل بين تأثيرات المطارات السيئة مقابل شركات الطيران السيئة؟ ولماذا أو لم لا؟ (تلميح: فكر في صياغة الكود التالي:
flights |> group_by(carrier, dest) |> summarize(n()))أوجد الرحلات الجوية التي سجلت أطول فترات تأخير عند الإقلاع لكل وجهة مطار مقصودة.
كيف تتغير وتتفاوت فترات التأخير على مدار ساعات اليوم الكامل؟ وضح إجابتك واستدل عليها برسم بياني توضيحي.
ماذا يحدث إذا قمت بتمرير قيمة سالبة للمعامل
nداخل الدالةslice_min()والدوال الشقيقة التابعة لعائلتها؟اشرح الآلية الإجرائية وما تقوم به الدالة
count()صراحةً بدلالة دوال حزمة dplyr الإجرائية التي تعلمتها للتو. وما هي الوظيفة التي يؤديها وسيط الضبطsortداخل الدالةcount()؟لنفترض أن لدينا الإطار البياناتي الصغير جداً التالي:
أ. اكتب تصورك وتوقعك لما سيبدو عليه شكل المخرج البرمجي، ثم تحقق بعد ذلك عملياً لمعرفة ما إذا كنت مصيباً، واشرح بدقة ماذا تفعل الدالة group_by().
df |>
group_by(y)ب. اكتب تصورك وتوقعك لما سيبدو عليه شكل المخرج البرمجي، ثم تحقق بعد ذلك عملياً لمعرفة ما إذا كنت مصيباً، واشرح بدقة ماذا تفعل الدالة arrange(). وعلق أيضاً على طبيعة اختلافها عن عملية التجميع group_by() التي أجريت في الفقرة (أ).
df |>
arrange(y)ج. اكتب تصورك وتوقعك لما سيبدو عليه شكل المخرج البرمجي، ثم تحقق بعد ذلك عملياً لمعرفة ما إذا كنت مصيباً، واشرح بدقة ماذا يفعل هذا الأنبوب البرمجي المتسلسل (Pipeline).
د. اكتب تصورك وتوقعك لما سيبدو عليه شكل المخرج البرمجي، ثم تحقق بعد ذلك عملياً لمعرفة ما إذا كنت مصيباً، واشرح بدقة ماذا يفعل هذا الأنبوب البرمجي المتسلسل. وعلق بعد ذلك على ما تفيده الرسالة التنبيهية الظاهرة.
هـ. اكتب تصورك وتوقعك لما سيبدو عليه شكل المخرج البرمجي، ثم تحقق بعد ذلك عملياً لمعرفة ما إذا كنت مصيباً، واشرح بدقة ماذا يفعل هذا الأنبوب البرمجي المتسلسل. وكيف يختلف هذا المخرج عن المخرج المستخرج في الفقرة (د)؟
و. اكتب تصورك وتوقعك لما سيبدو عليه شكل المخرجات البرمجية، ثم تحقق بعد ذلك عملياً لمعرفة ما إذا كنت مصيباً، واشرح بدقة وظيفة كل أنبوب برمجي متسلسل منهما. وكيف تختلف المخرجات الناتجة بين هذين الأنبوبين البرمجيين؟
3.6 دراسة حالة: التلخيصات الإحصائية وحجم العينة
كلما قمت بإجراء أي عملية تلخيص إحصائي أو تجميعي، فمن القواعد المنهجية السديدة دائماً تضمين حساب التكرارات والعد (n())، فبهذه الطريقة، يمكنك التوثق والضمان التام بأنك لا تبني استنتاجاتك وتعميماتك العلمية بناءً على كميات ضئيلة جداً ومحدودة من البيانات. وسوف نبرهن على هذه الفكرة التحليلية باستخدام بعض البيانات الخاصة برياضة البيسبول والمستمدة من حزمة Lahman، وتحديداً، سوف نقوم بالمقارنة بين النسبة الإحصائية لعدد المرات التي يحقق فيها اللاعب ضربة ناجحة (H) مقسوماً على إجمالي عدد المرات التي حاول فيها إدخال الكرة في سياق اللعب (AB):
batters <- Lahman::Batting |>
group_by(playerID) |>
summarize(
performance = sum(H, na.rm = TRUE) / sum(AB, na.rm = TRUE),
n = sum(AB, na.rm = TRUE)
)
batters
#> # A tibble: 24,011 × 3
#> playerID performance n
#> <chr> <dbl> <int>
#> 1 aardsda01 0 4
#> 2 aaronha01 0.305 12364
#> 3 aaronto01 0.229 944
#> 4 aasedo01 0 5
#> 5 abadan01 0.0952 21
#> 6 abadfe01 0.111 9
#> # ℹ 24,005 more rowsعندما نقوم برسم المهارة الفنية للاعب البيسبول (والتي تقاس بمعدل الضربات الناجحة performance) في مقابل إجمالي عدد الفرص المتاحة له لضرب الكرة (والتي تقاس بعدد مرات الوقوف للضرب n)، سوف تلمح وتلاحظ بوضوح نمطين بنيويين محددين:
إن التباين والتفاوت في الأداء
performanceيكون أكبر بكثير وبشكل ملحوظ بين اللاعبين الذين حظوا بفرص وقوف قليلة للضرب. ويُعد شكل هذا الرسم البياني نموذجياً ومألوفاً للغاية في الإحصاء؛ فكلما قمت برسم متوسط حسابي (أو أي مؤشرات إحصائية ملخصة أخرى) في مقابل حجم المجموعة التجميعية، ستلاحظ دائماً أن التباين يضيق وينخفض تدريجياً كلما زاد حجم العينة (Sample size)4.هناك ارتباط إيجابي طردي واضح بين مهارة اللاعب (
performance) وفرص ضرب الكرة المتاحة له (n)؛ والسبب المنهجي في ذلك يرجع إلى أن الفرق الرياضية ترغب بطبيعة الحال في منح أفضل لاعبيها المهرة أكبر عدد ممكن من الفرص والمحاولات لضرب الكرة.
batters |>
filter(n > 100) |>
ggplot(aes(x = n, y = performance)) +
geom_point(alpha = 1 / 10) +
geom_smooth(se = FALSE)
تأمل جيداً هذا النمط البرمجي العملي للغاية الذي يدمج بين حزمتي ggplot2 و dplyr معاً؛ حيث لا يتطلب الأمر منك سوى تذكر التحول الحذر من استخدام أداة التمرير |> (المخصصة لمعالجة وتجهيز مجموعة البيانات) إلى استخدام إشارة الجمع + (المخصصة لإضافة الطبقات والملامح الرسومية إلى مخططك البياني).
ولهذه الظاهرة الإحصائية أيضاً انعكاسات بالغة الأهمية على عمليات الترتيب والمفاضلة (Ranking). فإذا قمت بالفرز السطحي المباشر بناءً على الترتيب التنازلي للأداء desc(performance) دون مراعاة لحجم العينة، فإن الأشخاص الذين سيتصدرون القائمة بأفضل معدلات أداء هم حتماً أولئك الذين خاضوا محاولات قليلة جداً لإدخال الكرة في اللعب ونجحوا فيها بالصدفة المحضة، وليسوا بالضرورة اللاعبين الأكثر مهارة واستدامة في الأداء الفعلي:
ويمكنك العثور على شرح تفصيلي وعميق لهذه المشكلة المنهجية وكيفية التغلب عليها ومعالجتها إحصائياً عبر الرابطين التاليين: http://varianceexplained.org/r/empirical_bayes_baseball/ و https://www.evanmiller.org/how-not-to-sort-by-average-rating.html.
3.7 ملخص
لقد تعلمت في ثنايا هذا الفصل الأدوات البرمجية المتكاملة التي تتيحها حزمة dplyr للتعامل بمرونة مع الإطارات البياناتية وتشكيلها. وتنقسم هذه الأدوات بشكل عام إلى ثلاث فئات رئيسية أساسية: الأدوات التي تتعامل مع الصفوف وتتحكم فيها (مثل الدالتين filter() و arrange())، والأدوات التي تتعامل مع الأعمدة وتغيرها (مثل الدالتين select() و mutate())، وأخيراً الأدوات المخصصة للتعامل مع المجموعات الإحصائية وتجزئتها (مثل الدالتين group_by() و summarize()). ولقد ركزنا كل اهتمامنا في هذا الفصل على هذه الأدوات الشاملة التي تؤثر على “كامل الإطار البياناتي”، ولكنك لم تتعمق بعد في معرفة ما يمكنك القيام به بالتفصيل على مستوى المتغيرات الفردية المستقلة. وسوف نعود إلى هذا المبحث بالتفصيل في قسم “التحويل” (Transform) القادم من هذا الكتاب، حيث سيفرد كل فصل أدوات تخصصية ونوعية للتعامل مع نوع محدد من المتغيرات.
وفي الفصل القادم، سوف ننعطف مجدداً للحديث عن مسارات وسياقات العمل (Workflow) لمناقشة الأهمية البالغة لأسلوب كتابة الكود وتنسيقه (Code style)، والمحافظة على تنظيم كودك البرمجي بطريقة منهجية تسهل عليك وعلى الآخرين قراءته وفهمه بوضوح.
ستتعرف لاحقاً على عائلة الدوال
slice_*، والتي تتيح لك اختيار الصفوف بناءً على مواقعها وترتيبها الرقمي.↩︎تذكر أنه في بيئة RStudio، فإن الطريقة الأكثر ملاءمة لمعاينة مجموعة بيانات تحتوي على أعمدة كثيرة هي استخدام الدالة
View.↩︎أو
summarise()بالرسم البريطاني إذا كنت تفضل ذلك.↩︎تنحنح قانون الأعداد الكبيرة (The law of large numbers) تنحنح.↩︎