13 الأعداد
13.1 مقدمة
تُمثل المتجهات العددية العمود الفقري لعلم البيانات، وقد استخدمتها بالفعل عدة مرات في أجزاء سابقة من هذا الكتاب. حان الوقت الآن لاستعراض ما يمكنك القيام به معها في بيئة R بشكل منهجي، لضمان استعدادك التام للتعامل مع أي مسألة مستقبلية تتضمن متجهات عددية.
سنبدأ بتزويدك ببعض الأدوات لتحويل النصوص إلى أعداد، ثم ننتقل إلى تفاصيل أكثر حول الدالة count(). بعد ذلك، سننتقل إلى التحويلات العددية المختلفة التي تتكامل بفاعلية مع الدالة mutate()، بما في ذلك تحويلات أكثر عمومية يمكن تطبيقها على أنواع أخرى من المتجهات لكنها تُستخدم غالباً مع المتجهات العددية. وسنختم بتغطية دوال التلخيص التي تتكامل مع الدالة summarize() ونوضح كيف يمكن استخدامها أيضاً مع mutate().
13.1.1 المتطلبات المسبقة
يستخدم هذا الفصل في الغالب دوالاً من حزمة R الأساسية (base R)، والتي تتوفر دون الحاجة لتحميل أي حزم إضافية. لكننا نزال بحاجة إلى منظومة tidyverse لأننا سنستخدم هذه الدوال الأساسية داخل دوال tidyverse مثل mutate() و filter(). وكما في الفصل السابق، سنستخدم أمثلة حقيقية من مجموعة بيانات nycflights13، بالإضافة إلى أمثلة توضيحية بسيطة مصممة باستخدام c() و tribble().
13.2 إنشاء الأعداد
في معظم الحالات، ستحصل على الأعداد مسجلة بالفعل في أحد الأنواع العددية في R: أعداد صحيحة (integer) أو أعداد مزدوجة الدقة (double). ومع ذلك، ستواجهها في بعض الحالات كقيم نصية، ربما لأنك أنشأتها عن طريق تحويل العناوين (pivoting) أو بسبب حدث خطأ ما أثناء عملية استيراد البيانات.
توفر حزمة readr دالتين مفيدتين لتحليل النصوص وتحويلها إلى أعداد: parse_double() و parse_number(). استخدم parse_double() عندما تكون لديك أعداد مكتوبة كقيم نصية:
x <- c("1.2", "5.6", "1e3")
parse_double(x)
#> [1] 1.2 5.6 1000.0واستخدم parse_number() عندما يحتوي النص على رمُوز أو أحرف غير عددية تريد تجاهُلها. هذا مفيد بشكل خاص عند التعامل مع بيانات العملات والنسب المئوية:
x <- c("$1,234", "USD 3,513", "59%")
parse_number(x)
#> [1] 1234 3513 5913.3 التكرارات أو الأعداد (Counts)
من المدهش حجم التحليلات التي يمكنك القيام بها في علم البيانات باستخدام التكرارات والحسابات الأساسية فقط؛ لذا تسعى dplyr إلى جعل عملية العد والتكرار أسهل ما يمكن باستخدام الدالة count(). تعد هذه الدالة ممتازة الاستخدام للاستكشاف السريع والمراجعة أثناء التحليل:
flights |> count(dest)
#> # A tibble: 105 × 2
#> dest n
#> <chr> <int>
#> 1 ABQ 254
#> 2 ACK 265
#> 3 ALB 439
#> 4 ANC 8
#> 5 ATL 17215
#> 6 AUS 2439
#> # ℹ 99 more rows(على الرغم من النصيحة المذكورة في الفصل 4، نضع عادةً count() في سطر واحد لأنها تُستخدم غالباً في لوحة التحكم للحصول على تحقق سريع من عمل الحسابات كما هو متوقع.)
إذا أردت رؤية القيم الأكثر تكراراً وشيوعاً، أضف الوسيط sort = TRUE:
flights |> count(dest, sort = TRUE)
#> # A tibble: 105 × 2
#> dest n
#> <chr> <int>
#> 1 ORD 17283
#> 2 ATL 17215
#> 3 LAX 16174
#> 4 BOS 15508
#> 5 MCO 14082
#> 6 CLT 14064
#> # ℹ 99 more rowsوتذكر أنه إذا أردت عرض جميع القيم، يمكنك استخدام |> View() أو |> print(n = Inf).
يمكنك إجراء نفس الحسابات خطوة بخطوة باستخدام group_by() و summarize() و n(). هذا مفيد لأنه يتيح لك حساب ملخصات إحصائية أخرى في نفس الوقت:
تعتبر n() دالة تلخيص خاصة لا تأخذ أي وسائط، وتكتفي بدلاً من ذلك بالوصول إلى معلومات المجموعة “الحالية”. هذا يعني أنها تعمل فقط داخل أفعال وحزم dplyr:
n()
#> Error in `n()`:
#> ! Must only be used inside data-masking verbs like `mutate()`,
#> `filter()`, and `group_by()`.توجد صيغتان متنوّعتان من n() و count() قد تجدهما مفيدتين:
-
تحسب
n_distinct(x)عدد القيم المميزة (الفريدة) لمتغير واحد أو أكثر. على سبيل المثال، يمكننا معرفة وجهات الوصول التي تخدمها أكثر شركات الطيران:flights |> group_by(dest) |> summarize(carriers = n_distinct(carrier)) |> arrange(desc(carriers)) #> # A tibble: 105 × 2 #> dest carriers #> <chr> <int> #> 1 ATL 7 #> 2 BOS 7 #> 3 CLT 7 #> 4 ORD 7 #> 5 TPA 7 #> 6 AUS 6 #> # ℹ 99 more rows -
العد الموزون هو عبارة عن مجموع. على سبيل المثال يمكنك “حساب” إجمالي الأميال التي قطعتها كل طائرة:
يُعد العد الموزون مسألة شائعة؛ لذا تحتوي الدالة
count()على وسيطwtيقوم بنفس المهمة:flights |> count(tailnum, wt = distance) -
يمكنك حساب القيم المفقودة عن طريق الدمج بين
sum()وis.na(). في مجموعة بياناتflightsتمثل هذه الحالات الرحلات التي أُلغيت:
13.3.1 تمارين
- كيف يمكنك استخدام
count()لحساب عدد الصفوف التي تحتوي على قيمة مفقودة لمتغير معين؟ - قم بتوسيع الاستدعاءات التالية للدالة
count()لتستخدم بدلاً منهاgroup_by()وsummarize()وarrange():flights |> count(dest, sort = TRUE)flights |> count(tailnum, wt = distance)
13.4 التحويلات العددية
تعمل دوال التحويل بفاعلية ممتازة مع mutate() لأن طول مخرجاتها يكون مساوياً لطول المدخلات. الغالبية العظمى من دوال التحويل مدمجة بالفعل في R الأساسية. وليس من العملي حصرها جميعاً، لذا سيعرض هذا القسم أهمها وأكثرها فائدة. على سبيل المثال، توفر R جميع الدوال المثلثية، لكننا لا ندرجها هنا لأنها نادرة الاستخدام في علم البيانات.
13.4.1 العمليات الحسابية وقواعد إعادة التدوير
استعرضنا أساسيات العمليات الحسابية (+, -, *, /, ^) في الفصل 2 واستخدمناها كثيراً منذ ذلك الحين. لا تحتاج هذه الدوال إلى شرح موسع لأنها تطبق الحسابات الأساسية المتعارف عليها. ولكننا بحاجة للتحدث بإيجاز عن قواعد إعادة التدوير (recycling rules) التي تحدد ما يحدث عندما تكون للطرفين الأيمن والأيسر أطوال مختلفة. هذا مهم لعمليات مثل flights |> mutate(air_time = air_time / 60) لأن هناك 336,776 عدداً على يسار المعامل / بينما يوجد عدد واحد فقط على يمينه.
تعالج R الاختلاف في الأطوال من خلال إعادة تدوير المتجه القصير، أي تكراره. يمكننا رؤية هذه العملية بوضوح إذا أنشأنا بعض المتجهات خارج إطار البيانات:
عموماً، ستُفضل إعادة تدوير أعداد مفردة فقط (أي متجهات ذات طول 1)، لكن R ستعيد تدوير أي متجه قصير الطول. وغصباً ما تظهر لك R تحذيراً (وليس دائماً) إذا لم يكن طول المتجه الأطول مضاعفاً لطول المتجه الأقصر:
تُطبق قواعد إعادة التدوير هذه أيضاً على المقارنات المنطقية (==, <, <=, >, >=, !=) وقد تؤدي إلى نتائج مفاجئة إذا استخدمت بالخطأ المعامل == بدلاً من %in% وكان لإطار البيانات عدد غير متوافق من الصفوف. على سبيل المثال، تأمل هذا الكود الذي يحاول العثور على جميع الرحلات الجوية في شهري يناير وفبراير:
flights |>
filter(month == c(1, 2))
#> # A tibble: 25,977 × 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 542 540 2 923 850
#> 3 2013 1 1 554 600 -6 812 837
#> 4 2013 1 1 555 600 -5 913 854
#> 5 2013 1 1 557 600 -3 838 846
#> 6 2013 1 1 558 600 -2 849 851
#> # ℹ 25,971 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …يعمل الكود دون أخطاء برمجية، لكنه لا يرجع ما تريده بالفعل. بسبب قواعد إعادة التدوير، أوجد الكود الرحلات المغادرة في شهر يناير والموجودة في الصفوف الفردية، والرحلات المغادرة في شهر فبراير والموجودة في الصفوف الزوجية. وللأسف لا يظهر أي تحذير لأن إطار البيانات flights يحتوي على عدد زوجي من الصفوف.
لحمايتك من هذا النوع من الفشل غير الظاهر، تستخدم معظم دوال tidyverse نمطاً أكثر صرامة من إعادة التدوير يقتصر على تدوير القيم المفردة فقط. ولكن هذا لا يساعد في هذه الحالة أو في حالات أخرى، لأن العملية الأساسية تُنفذ بواسطة دالة R الأساسية == وليس بواسطة filter().
13.4.2 الحد الأدنى والحد الأقصى
تعمل الدوال الحسابية مع أزواج المتغيرات. وهناك دالتان وثيقتا الصلة بهما هما pmin() و pmax()، واللتان ترجعان القيمة الأصغر أو الأكبر في كل صف عند تزويدهما بمتغيرين أو أكثر:
لاحظ أن هاتين الدالتين تختلفان عن دوال التلخيص min() و max() اللتين تأخذان ملاحظات ومشاهدات متعددة وترجعان قيمة واحدة فقط. يمكنك معرفة أنك استخدمت الصيغة الخاطئة عندما تعطي جميع الحدود الدنيا والحدود القصوى نفس القيمة لكافة الصفوف:
13.4.3 الحساب النمطي (Modular arithmetic)
الحساب النمطي هو الاسم التقني للعمليات الرياضية التي كنت تجريها قبل التعرف على الخانات العشرية، أي القسمة التي تنتج عدداً صحيحاً وباقياً للقسمة. في بيئة R، يقوم المعامل %/% بالقسمة الصحيحة بينما يحسب المعامل %% باقي القسمة:
الحساب التمطي مفيد في التعامل مع مجموعة بيانات flights، حيث يمكننا استخدامه لتفكيك متغير وقت المغادرة المجدول sched_dep_time إلى ساعه hour ودقيقة minute:
يمكننا دمج ذلك مع حيلة mean(is.na(x)) من قسم 12.4 لرؤية كيف تتغير نسبة الرحلات الملغاة على مدار اليوم. النتائج موضحة في الشكل 13.1.
13.4.4 اللوجاريتمات
تعد اللوجاريتمات تحويلاً مفيداً للغاية للتعامل مع البيانات التي تمتد عبر عدة رتب من المقدار (orders of magnitude) ولتحويل النمو الأسي إلى نمو خطي. في بيئة R، لديك خيار من ثلاثة لوجاريتمات: log() (اللوجاريتم الطبيعي، ذو الأساس e)، و log2() (ذو الأساس 2)، و log10() (ذو الأساس 10). نوصي باستخدام log2() أو log10(). يتميز log2() بسهولة التفسير لأن الفرق بمقدار 1 على المقياس اللوجاريتمي يمثل المضاعفة على المقياس الأصلي، بينما الفرق بمقدار -1 يمثل النصف؛ في حين أن log10() يسهل عكس تحويله لأن 3 مثلاً تعادل 10^3 = 1000. المعكوس للدالة log() هو exp()؛ ولحساب المعكوس لـ log2() أو log10() ستنحتاج إلى استخدام 2^ أو 10^.
13.4.5 التقريب
استخدم round(x) لتقريب عدد إلى أقرب عدد صحيح:
round(123.456)
#> [1] 123يمكنك التحكم في دقة التقريب باستخدام الوسيط الثاني digits. تقوم round(x, digits) بالتقريب إلى أقرب 10^-n؛ لذا فإن digits = 2 سوف تقرب إلى أقرب 0.01. هذا التعريف مفيد لأنه يعني أن round(x, -3) ستُقرب إلى أقرب ألف، وهو ما تفعله بالفعل:
توجد غرابة واحدة في الدالة round() تبدو مفاجئة للوهلة الأولى:
تستخدم round() ما يُعرف باسم “التقريب للعدد الزوجي الأقرب” (round half to even) أو تقريب المصرفيين (Banker’s rounding): إذا كان العدد يقع في المنتصف تماماً بين عددين صحيحين، فسيتم تقريبه إلى العدد الزوجي. تعتبر هذه استراتيجية جيدة لأنها تحافظ على التقريب خالية من التحيز: فنصف حالات 0.5 تُقرب للأعلى، والنصف الآخر يُقرب للأسفل.
تقترن الدالة round() مع الدالة floor() التي تقرب دائماً للأسفل، والدالة ceiling() التي تقرب دائماً للأعلى:
لا تحتوي هذه الدوال على الوسيط digits؛ لذا يمكنك بدلاً من ذلك تقليل المقياس، ثم التقريب، ثم إعادة المقياس لما كان عليه:
يمكنك استخدام نفس الأسلوب إذا أردت استخدام round() للتقريب لمضاعفات عدد آخر:
13.4.6 تقسيم الأعداد إلى فئات
استخدم cut()1 لتقسيم المتجه العددي إلى فئات ومجموعات منفصلة:
لا يلزم أن تكون نقاط التقسيم (breaks) متساوية المسافات:
يمكنك اختيارياً تزويد الدالة بتسمياتك الخاصة عبر labels. لاحظ أنه يجب أن تكون التسميات labels أقل بواحد من عدد نقاط التقسيم breaks.
أي قيم تقع خارج نطاق نقاط التقسيم ستصبح قيم مفقودة NA:
راجع الوثائق لمعرفة وسائط أخرى مفيدة مثل right و include.lowest اللذين يتحكمان فيما إذا كانت الفترات مغلقة من اليمين (a, b] أم اليسار [a, b) وما إذا كانت أدنى فترة يجب أن تكون شاملة للطرفين [a, b].
13.4.7 المجمعات التراكمية والمتحركة
توفر R الأساسية الدوال cumsum() و cumprod() و cummin() و cummax() للحصول على المجموع والتراكمي والجداءات (حواصل الضرب) والأدنى والأقصى التراكمي. بينما توفر dplyr الدالة cummean() للمتوسط التراكمي. وعادة ما تظهر المجاميع التراكمية بشكل أكبر في الممارسة العملية:
x <- 1:10
cumsum(x)
#> [1] 1 3 6 10 15 21 28 36 45 55إذا كنت بحاجة إلى مجمعات تحريك أو انزلاق أكثر تعقيداً، فجرّب حزمة slider.
13.4.8 تمارين
اشرح بالكلمات ما يفعله كل سطر من الأكواد المستخدمة لإنشاء الشكل 13.1.
ما هي الدوال المثلثية التي توفرها R؟ خمن بعض الأسماء وابحث عنها في الوثائق. هل تستخدم الدرجات أم التقدير الدائري (راديان)؟
-
في الوقت الحالي، يُعد المتغيران
dep_timeوsched_dep_timeمريحين للعرض، لكن يصعب الحساب بهما لأنهما ليسا أعداداً متصلة حقيقية. يمكنك رؤية المشكلة الأساسية عن طريق تشغيل الكود أدناه: هناك فجوة بين كل ساعة وأخرى.flights |> filter(month == 1, day == 1) |> ggplot(aes(x = sched_dep_time, y = dep_delay)) + geom_point()قم بتحويلهما إلى تمثيل أكثر دقة للوقت (إما كسور الساعات أو الدقائق منذ منتصف الليل).
قَرّب المتغيرين
dep_timeوarr_timeلأقرب خمس دقائق.
13.5 التحويلات العامة
تصف الأقسام التالية بعض التحويلات العامة التي تُستخدم غالباً مع المتجهات العددية، ولكن يمكن تطبيقها على جميع أنواع الأعمدة الأخرى.
13.5.1 الرتب (Ranks)
توفر dplyr عدداً من دوال الترتيب المستوحاة من SQL، ولكن يجب أن تبدأ دائماً بالدالة dplyr::min_rank(). وهي تستخدم الطريقة التقليدية للتعامل مع التعادلات، مثل: 1st، 2nd، 2nd، 4th.
لاحظ أن أصغر القيم تحصل على أدنى الرتب؛ استخدم desc(x) لإعطاء أكبر القيم أدنى الرتب:
إذا كانت min_rank() لا تؤدي الغرض المطلوب، فراجع الصيغ الأخرى مثل dplyr::row_number() و dplyr::dense_rank() و dplyr::percent_rank() و dplyr::cume_dist(). راجع الوثائق للتفاصيل.
df <- tibble(x = x)
df |>
mutate(
row_number = row_number(x),
dense_rank = dense_rank(x),
percent_rank = percent_rank(x),
cume_dist = cume_dist(x)
)
#> # A tibble: 6 × 5
#> x row_number dense_rank percent_rank cume_dist
#> <dbl> <int> <int> <dbl> <dbl>
#> 1 1 1 1 0 0.2
#> 2 5 2 2 0.25 0.6
#> 3 5 3 2 0.25 0.6
#> 4 17 4 3 0.75 0.8
#> 5 22 5 4 1 1
#> 6 NA NA NA NA NAيمكنك تحقيق العديد من نفس النتائج لاختيار الوسيط المناسب ties.method في الدالة الأساسية rank() ببيئة R؛ وربما ترغب أيضاً في ضبط na.last = "keep" للإبقاء على القيم المفقودة NA كما هي.
يمكن أيضاً استخدام row_number() بدون أي وسائط عندما تكون داخل أفعال dplyr. في هذه الحالة، ستعطي رقم الصف “الحالي”. وعند دمجها مع %/% أو %% يمكن أن تكون أداة مفيدة لتقسيم البيانات إلى مجموعات متساوية الحجم:
df <- tibble(id = 1:10)
df |>
mutate(
row0 = row_number() - 1,
three_groups = row0 %% 3,
three_in_each_group = row0 %/% 3
)
#> # A tibble: 10 × 4
#> id row0 three_groups three_in_each_group
#> <int> <dbl> <dbl> <dbl>
#> 1 1 0 0 0
#> 2 2 1 1 0
#> 3 3 2 2 0
#> 4 4 3 0 1
#> 5 5 4 1 1
#> 6 6 5 2 1
#> # ℹ 4 more rows13.5.2 الإزاحات (Offsets)
تتيح لك الدالتان dplyr::lead() و dplyr::lag() الإشارة إلى القيم الواقعة مباشرة قبل أو بعد القيمة “الحالية”. وهما ترجعان متجهاً بنفس طول المدخلات، ومبطناً بالقيم المفقودة NA في البداية أو النهاية:
-
يمنحك التعبير
x - lag(x)الفرق بين القيمة الحالية والقيمة السابقة.x - lag(x) #> [1] NA 3 6 0 8 16 -
يخبرك التعبير
x == lag(x)عندما تتغير القيمة الحالية.x == lag(x) #> [1] NA FALSE FALSE TRUE FALSE FALSE
يمكنك الإزاحة للأمام أو الخلف بأكثر من خانة واحدة باستخدام الوسيط الثاني n.
13.5.3 المعرفات المتتابعة (Consecutive identifiers)
في بعض الأحيان ترغب في بدء مجموعة جديدة كلما حدث حدث معين. على سبيل المثال، عند النظر إلى بيانات موقع إلكتروني، يُعد خياراً شائعاً أن ترغب في تقسيم الأحداث إلى جلسات (sessions)، حيث تبدأ جلسة جديدة بعد فجوة زمنية تتجاوز x من الدقائق منذ آخر نشاط. تخيل مثلاً أن لديك الأوقات التي زار فيها شخص ما موقعاً إلكترونياً:
وقمت بحساب الوقت بين كل حدث والآخر، وحددت ما إذا كانت هناك فجوة كبيرة كافية للتأهل:
ولكن كيف ننتقل من ذلك المتجه المنطقي (logical vector) إلى شيء يمكننا استخدامه مع الدالة group_by()؟ هنا تأتي الدالة cumsum() من قسم 13.4.7 لإنقاذ الموقف؛ حيث إن وجود فجوة (أي عندما تكون قيمة has_gap هي TRUE) سيؤدي إلى زيادة رقم المجموعة group بمقدار واحد (قسم 12.4.2):
طريقة أخرى لإنشاء متغيرات التجميع هي استخدام الدالة consecutive_id()، والتي تبدأ مجموعة جديدة في كل مرة تتغير فيها إحدى الوسائط (arguments) المُمررة إليها. على سبيل المثال، وبإلهام من سؤال Stack Overflow هذا، تخيل أن لديك إطار بيانات (data frame) يحتوي على مجموعة من القيم المتكررة:
إذا أردت الاحتفاظ بالصف الأول من كل قيمة مكررة في x، يمكنك استخدام group_by() و consecutive_id() و slice_head():
df |>
group_by(id = consecutive_id(x)) |>
slice_head(n = 1)
#> # A tibble: 7 × 3
#> # Groups: id [7]
#> x y id
#> <chr> <dbl> <int>
#> 1 a 1 1
#> 2 b 2 2
#> 3 c 4 3
#> 4 d 3 4
#> 5 e 9 5
#> 6 a 4 6
#> # ℹ 1 more row13.5.4 تمارين
أوجد أكثر 10 رحلات تأخراً باستخدام دالة ترتيب. كيف تريد التعامل مع التعادلات؟ اقرأ وثائق
min_rank()بعناية.أي طائرة حسب رقم الذيل (
tailnum) لديها أسوأ سجل في الالتزام بالمواعيد؟ما هو الوقت من اليوم الذي ينبغي أن تسافر فيه إذا أردت تجنب التأخير قدر الإمكان؟
ماذا يفعل التعبير
flights |> group_by(dest) |> filter(row_number() < 4)؟ وماذا يفعل التعبيرflights |> group_by(dest) |> filter(row_number(dep_delay) < 4)؟لكل وجهة وصول، احسب إجمالي دقائق التأخير. ولكل رحلة، احسب نسبة التأخير التي تمثلها من إجمالي التأخير لوجهتها.
-
عادة ما تكون حالات التأخير مرتبطة زمنياً: حتى بعد حل المشكلة التي تسببت في التأخير الأولي، تتأخر الرحلات اللاحقة للسماح للرحلات السابقة بالمغادرة. باستخدام
lag()، استكشف كيف يرتبط متوسط تأخير الرحلات لساعة معينة بمتوسط التأخير للساعة السابقة. انظر إلى كل وجهة وصول. هل يمكنك العثور على رحلات سريعة بشكل مشبوه (أي رحلات قد تمثل خطأ في إدخال البيانات)؟ احسب زمن الطيران للرحلة بالنسبة لأقصر رحلة إلى تلك الوجهة. أي الرحلات كانت الأكثر تأخراً في الجو؟
أوجد جميع الوجهات التي تسير إليها شركتان للطيران على الأقل. استخدم تلك الوجهات للتوصل إلى ترتيب نسبي لشركات الطيران بناءً على أدائها لنفس الوجهة.
13.6 الملخصات العددية
مجرد استخدام التكرارات والمعدلات والمجاميع التي قدمناها بالفعل يمكن أن يقودك إلى قطع شوط طويل، لكن R توفر العديد من دوال التلخيص المفيدة الأخرى. إليك تشكيلة مختارة قد تجدها مفيدة.
13.6.1 النزعة المركزية
حتى الآن، استخدمنا غالباً mean() لتلخيص النزعة المركزية لمتجه من القيم. وكما رأينا في قسم 3.6، ونظراً لأن المتوسط الحسابي هو المجموع مقسوماً على العدد، فإنه حساس للغاية حتى للعدد القليل جداً من القيم العالية أو المنخفضة بشكل غير عادي. البديل هو استخدام الوسيط median()، الذي يجد القيمة التي تقع في “منتصف” المتجه، أي أن 50% من القيم أعلى منها و 50% أصل منها. اعتماداً على شكل توزيع المتغير الذي تهتم به، قد يكون المتوسط الحسابي أو الوسيط هو المقياس الأفضل للنزعة المركزية. على سبيل المثال، بالنسبة للتوزيعات المتماثلة يُفضل عادةً التقرير عن المتوسط الحسابي، بينما بالنسبة للتوزيعات الملتوية نورد عادةً الوسيط.
يقارن الشكل 13.2 بين المتوسط الحسابي والوسيط لتأخير المغادرة (بالدقائق) لكل وجهة وصول. يكون الوسيط دائماً أصغر من المتوسط الحسابي لأن الرحلات قد تغادر أحياناً متأخرة بعدة ساعات، لكنها لا تغادر أبداً مبكرة بعدة ساعات.
flights |>
group_by(year, month, day) |>
summarize(
mean = mean(dep_delay, na.rm = TRUE),
median = median(dep_delay, na.rm = TRUE),
n = n(),
.groups = "drop"
) |>
ggplot(aes(x = mean, y = median)) +
geom_abline(slope = 1, intercept = 0, color = "white", linewidth = 2) +
geom_point()
قد تتساءل أيضاً عن المنوال (mode)، أو القيمة الأكثر تكراراً وشيوعاً. هذا ملخص يعمل فقط وبشكل جيد مع الحالات البسيطة جداً (وهو السبب في أنك ربما تعلمته في المدرسة الثانوية)، ولكنه لا يعمل بفاعلية مع معظم مجموعات البيانات الحقيقية. إذا كانت البيانات منفصلة، فقد تكون هناك قيم متعددة هي الأكثر تكراراً، وإذا كانت البيانات متصلة، فقد لا توجد قيمة أكثر تكراراً لأن كل قيمة تختلف عن الأخرى ولو بنسبة طفيفة جداً. لهذه الأسباب، يميل الإحصائيون إلى عدم استخدام المنوال، ولا تتضمن R الأساسية دالة لحساب المنوال2.
13.6.2 الحد الأدنى والحد الأقصى والمئينات
ماذا لو كنت مهتماً بالمواقع بخلاف المركز؟ ستعطيك min() و max() أكبر وأصغر القيم. أداة قوية أخرى هي quantile() وهي تعميم للوسيط: حيث تجد quantile(x, 0.25) القيمة التي تُعد أكبر من 25% من القيم، و quantile(x, 0.5) تكافئ الوسيط، و quantile(x, 0.95) تجد القيمة التي تزيد عن 95% من القيم.
بالنسبة لبيانات flights، قد ترغب في النظر إلى المئين 95% للتأخيرات بدلاً من الحد الأقصى، لأنه سيتجاهل 5% من الرحلات الأكثر تأخراً والتي قد تكون متطرفة للغاية.
flights |>
group_by(year, month, day) |>
summarize(
max = max(dep_delay, na.rm = TRUE),
q95 = quantile(dep_delay, 0.95, na.rm = TRUE),
.groups = "drop"
)
#> # A tibble: 365 × 5
#> year month day max q95
#> <int> <int> <int> <dbl> <dbl>
#> 1 2013 1 1 853 70.1
#> 2 2013 1 2 379 85
#> 3 2013 1 3 291 68
#> 4 2013 1 4 288 60
#> 5 2013 1 5 327 41
#> 6 2013 1 6 202 51
#> # ℹ 359 more rows13.6.3 التشتت والانتشار
في بعض الأحيان لا تكون مهتماً بما يكفي بمكان تكتل الجزء الأكبر من البيانات، بل بكيفية انتشارها وتشتتها. ملخصان شائعا الاستخدام هما الانحراف المعياري sd(x) والمدى الربيعي IQR(). لن نشرح sd() هنا لأنك على دراية بها على الأرجح، لكن IQR() قد تكون جديدة بالنسبة لك — وهي تساوي quantile(x, 0.75) - quantile(x, 0.25) وتمنحك المدى الذي يحتوي على الـ 50% الوسطى من البيانات.
يمكننا استخدام هذا لكشف غرابة صغيرة في بيانات flights. قد تتوقع أن يكون انتشار وتشتت المسافة بين نقطة الانطلاق ووجهة الوصول صفراً، نظرًا لأن المطارات في نفس المكان دائماً. لكن الكود أدناه يكشف عن حالة غريبة في البيانات لمطار EGE:
13.6.4 التوزيعات
من الجدير بالتذكر أن جميع الإحصاءات التلخيصية الموضحة أعلاه هي طريقة اختزال واختصار للتوزيع إلى رقم واحد. هذا يعني أنها اختزالية بشكل أساسي، وإذا اخترت المقياس التلخيصي الخاطئ، يمكنك بسهولة إغفال الفروق المهمة بين المجموعات. لهذا السبب تكون فكرة ممتازة دائماً تصور التوزيع ببيان خطي أو مدرج قبل اعتماد إحصاءاتك التلخيصية.
يوضح الشكل 13.3 التوزيع العام لتأخيرات المغادرة. التوزيع ملتوٍ لدرجة أننا نضطر إلى التكبير لرؤية معظم البيانات. هذا يشير إلى أنه من غير المرجح أن يكون المتوسط الحسابي ملخصاً جيداً وقد نفضل الوسيط بدلاً منه.
من الفكرة الجيدة أيضاً التحقق من أن التوزيعات للمجموعات الفرعية تشبه الكل. في الرسم البياني التالي، تم تراكب 365 مضلعاً تكرارياً لـ dep_delay، مضلع واحد لكل يوم. يبدو أن التوزيعات تتبع نمطاً مشتركاً، مما يشير إلى أنه من المناسب استخدام نفس الملخص لكل يوم.
flights |>
filter(dep_delay < 120) |>
ggplot(aes(x = dep_delay, group = interaction(day, month))) +
geom_freqpoly(binwidth = 5, alpha = 1/5)
لا تتردد في استكشاف ملخصاتك المخصصة المصممة خصيصاً للبيانات التي تعمل عليها. في هذه الحالة، قد يعني ذلك تلخيص الرحلات التي غادرت مبكرة بشكل منفصل عن الرحلات التي غادرت متأخرة، أو نظراً لأن القيم ملتوية للغاية، فقد تجرب التحويل اللوجاريتمي. أخيراً، لا تنسَ ما تعلمته في قسم 3.6: عند إنشاء ملخصات عددية، من الفكرة الجيدة دائماً تضمين عدد المشاهدات والملاحظات في كل مجموعة.
13.6.5 المواقع والترتيب الموقعي
هناك نوع أخير من الملخصات المفيدة للمتجهات العددية، ولكنه يعمل أيضاً مع كل أنواع القيم الأخرى: استخراج قيمة في موقع محدد: first(x) و last(x) و nth(x, n).
على سبيل المثال، يمكننا العثور على وقت المغادرة الأول والخامس والأخير لكل يوم:
flights |>
group_by(year, month, day) |>
summarize(
first_dep = first(dep_time, na_rm = TRUE),
fifth_dep = nth(dep_time, 5, na_rm = TRUE),
last_dep = last(dep_time, na_rm = TRUE)
)
#> `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.
#> # A tibble: 365 × 6
#> # Groups: year, month [12]
#> year month day first_dep fifth_dep last_dep
#> <int> <int> <int> <int> <int> <int>
#> 1 2013 1 1 517 554 2356
#> 2 2013 1 2 42 535 2354
#> 3 2013 1 3 32 520 2349
#> 4 2013 1 4 25 531 2358
#> 5 2013 1 5 14 534 2357
#> 6 2013 1 6 16 555 2355
#> # ℹ 359 more rows(ملاحظة: نظراً لأن دوال dplyr تستخدم _ للفصل بين المكونات وأسماء الوسائط، فإن هذه الدوال تستخدم na_rm بدلاً من na.rm.)
إذا كنت على دراية بمعامل الأقواس [، والذي سنعود إليه في قسم 27.2، فقد تتساءل عما إذا كنت بحاجة إلى هذه الدوال مطلقاً. هناك ثلاثة أسباب: الوسيط default يتيح لك تقديم قيمة افتراضية إذا كان الموقع المحدد غير موجود، والوسيط order_by يتيح لك إعادة ترتيب الصفوف محلياً، والوسيط na_rm يتيح لك حذف القيم المفقودة.
يعد استخراج القيم في مواقع معينة مكملاً للتصفية بناءً على الرتب. تمنحك التصفية جميع المتغيرات، مع وجود كل مشاهدة في صف منفصل:
flights |>
group_by(year, month, day) |>
mutate(r = min_rank(sched_dep_time)) |>
filter(r %in% c(1, max(r)))
#> # A tibble: 1,195 × 20
#> # 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 2353 2359 -6 425 445
#> 3 2013 1 1 2353 2359 -6 418 442
#> 4 2013 1 1 2356 2359 -3 425 437
#> 5 2013 1 2 42 2359 43 518 442
#> 6 2013 1 2 458 500 -2 703 650
#> # ℹ 1,189 more rows
#> # ℹ 12 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …
13.6.6 الاستخدام مع mutate()
كما توحي الأسماء، تقترن دوال التلخيص عادةً مع summarize(). ومع ذلك، وبسبب قواعد إعادة التدوير التي ناقشناها في قسم 13.4.1، يمكن أيضاً إقرانها بشكل مفيد مع mutate()، خاصة عندما تريد إجراء نوع من المعايرة للمجموعات. على سبيل المثال:
- يحسب التعبير
x / sum(x)نسبة من الإجمالي. - يحسب التعبير
(x - mean(x)) / sd(x)الدرجة المعيارية Z-score (المعايرة لمتوسط 0 وانحراف معياري 1). - يعاير التعبير
(x - min(x)) / (max(x) - min(x))البيانات للمدى [0, 1]. - يحسب التعبير
x / first(x)مؤشراً استناداً إلى المشاهدة الأولى.
13.6.7 تمارين
ابحث عن 5 طرق مختلفة على الأقل لتقييم خصائص التأخير النموذجية لمجموعة من الرحلات الجوية. متى تكون
mean()مفيدة؟ متى تكونmedian()مفيدة؟ متى قد ترغب في استخدام شيء آخر؟ هل يجب استخدام تأخير الوصول أم تأخير المغادرة؟ لماذا قد ترغب في استخدام البيانات من جدولplanes؟أي الوجهات تظهر أكبر تباين في السرعة الجوية؟
أنشئ رسماً بيانياً لمزيد من استكشاف مغامرات مطار EGE. هل يمكنك العثور على أي دليل على أن المطار غير موقعه؟ هل يمكنك العثور على متغير آخر قد يفسر هذا الاختلاف؟
13.7 ملخص
أنت بالفعل على دراية بالعديد من الأدوات للتعامل مع الأعداد، وبعد قراءة هذا الفصل أصبحت تعرف الآن كيفية استخدامها في R. تعلمت أيضاً مجموعة من التحويلات العامة المفيدة التي تُطبق عادةً، ولكن ليس حصرياً، على المتجهات العددية مثل الرتب والإزاحات. أخيراً، عملت من خلال عدد من الملخصات العددية، وناقشت بعض التحديات الإحصائية التي يجب عليك مراعاتها.
عبر الفصلين القادمين، سنغوص في العمل مع النصوص السلسلية باستخدام حزمة stringr. النصوص موضوع كبير لذا يحظى بفصلين، أحدهما عن أساسيات النصوص والآخر عن التعبيرات النمطية (Regular expressions).
توفر حزمة ggplot2 بعض الدوال المساعدة للحالات الشائعة مثل
cut_interval()وcut_number()وcut_width(). يُعد وجود هذه الدوال داخل ggplot2 مكاناً غريباً نوعاً ما، لكنها مفيدة كجزء من حساب المدرجات التكرارية وكُتبت قبل ظهور بقية أجزاء tidyverse.↩︎