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   59

13.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(). هذا مفيد لأنه يتيح لك حساب ملخصات إحصائية أخرى في نفس الوقت:

flights |> 
  group_by(dest) |> 
  summarize(
    n = n(),
    delay = mean(arr_delay, na.rm = TRUE)
  )
#> # A tibble: 105 × 3
#>   dest      n delay
#>   <chr> <int> <dbl>
#> 1 ABQ     254  4.38
#> 2 ACK     265  4.85
#> 3 ALB     439 14.4 
#> 4 ANC       8 -2.5 
#> 5 ATL   17215 11.3 
#> 6 AUS    2439  6.02
#> # ℹ 99 more rows

تعتبر 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
  • العد الموزون هو عبارة عن مجموع. على سبيل المثال يمكنك “حساب” إجمالي الأميال التي قطعتها كل طائرة:

    flights |> 
      group_by(tailnum) |> 
      summarize(miles = sum(distance))
    #> # A tibble: 4,044 × 2
    #>   tailnum  miles
    #>   <chr>    <dbl>
    #> 1 D942DN    3418
    #> 2 N0EGMQ  250866
    #> 3 N10156  115966
    #> 4 N102UW   25722
    #> 5 N103US   24619
    #> 6 N104UW   25157
    #> # ℹ 4,038 more rows

    يُعد العد الموزون مسألة شائعة؛ لذا تحتوي الدالة count() على وسيط wt يقوم بنفس المهمة:

    flights |> count(tailnum, wt = distance)
  • يمكنك حساب القيم المفقودة عن طريق الدمج بين sum() و is.na(). في مجموعة بيانات flights تمثل هذه الحالات الرحلات التي أُلغيت:

    flights |> 
      group_by(dest) |> 
      summarize(n_cancelled = sum(is.na(dep_time))) 
    #> # A tibble: 105 × 2
    #>   dest  n_cancelled
    #>   <chr>       <int>
    #> 1 ABQ             0
    #> 2 ACK             0
    #> 3 ALB            20
    #> 4 ANC             0
    #> 5 ATL           317
    #> 6 AUS            21
    #> # ℹ 99 more rows

13.3.1 تمارين

  1. كيف يمكنك استخدام count() لحساب عدد الصفوف التي تحتوي على قيمة مفقودة لمتغير معين؟
  2. قم بتوسيع الاستدعاءات التالية للدالة count() لتستخدم بدلاً منها group_by() و summarize() و arrange():
    1. flights |> count(dest, sort = TRUE)

    2. 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 الاختلاف في الأطوال من خلال إعادة تدوير المتجه القصير، أي تكراره. يمكننا رؤية هذه العملية بوضوح إذا أنشأنا بعض المتجهات خارج إطار البيانات:

x <- c(1, 2, 10, 20)
x / 5
#> [1] 0.2 0.4 2.0 4.0
# هي اختصار لـ
x / c(5, 5, 5, 5)
#> [1] 0.2 0.4 2.0 4.0

عموماً، ستُفضل إعادة تدوير أعداد مفردة فقط (أي متجهات ذات طول 1)، لكن R ستعيد تدوير أي متجه قصير الطول. وغصباً ما تظهر لك R تحذيراً (وليس دائماً) إذا لم يكن طول المتجه الأطول مضاعفاً لطول المتجه الأقصر:

x * c(1, 2)
#> [1]  1  4 10 40
x * c(1, 2, 3)
#> Warning in x * c(1, 2, 3): longer object length is not a multiple of shorter
#> object length
#> [1]  1  4 30 20

تُطبق قواعد إعادة التدوير هذه أيضاً على المقارنات المنطقية (==, <, <=, >, >=, !=) وقد تؤدي إلى نتائج مفاجئة إذا استخدمت بالخطأ المعامل == بدلاً من %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()، واللتان ترجعان القيمة الأصغر أو الأكبر في كل صف عند تزويدهما بمتغيرين أو أكثر:

df <- tribble(
  ~x, ~y,
  1,  3,
  5,  2,
  7, NA,
)

df |> 
  mutate(
    min = pmin(x, y, na.rm = TRUE),
    max = pmax(x, y, na.rm = TRUE)
  )
#> # A tibble: 3 × 4
#>       x     y   min   max
#>   <dbl> <dbl> <dbl> <dbl>
#> 1     1     3     1     3
#> 2     5     2     2     5
#> 3     7    NA     7     7

لاحظ أن هاتين الدالتين تختلفان عن دوال التلخيص min() و max() اللتين تأخذان ملاحظات ومشاهدات متعددة وترجعان قيمة واحدة فقط. يمكنك معرفة أنك استخدمت الصيغة الخاطئة عندما تعطي جميع الحدود الدنيا والحدود القصوى نفس القيمة لكافة الصفوف:

df |> 
  mutate(
    min = min(x, y, na.rm = TRUE),
    max = max(x, y, na.rm = TRUE)
  )
#> # A tibble: 3 × 4
#>       x     y   min   max
#>   <dbl> <dbl> <dbl> <dbl>
#> 1     1     3     1     7
#> 2     5     2     1     7
#> 3     7    NA     1     7

13.4.3 الحساب النمطي (Modular arithmetic)

الحساب النمطي هو الاسم التقني للعمليات الرياضية التي كنت تجريها قبل التعرف على الخانات العشرية، أي القسمة التي تنتج عدداً صحيحاً وباقياً للقسمة. في بيئة R، يقوم المعامل %/% بالقسمة الصحيحة بينما يحسب المعامل %% باقي القسمة:

1:10 %/% 3
#>  [1] 0 0 1 1 1 2 2 2 3 3
1:10 %% 3
#>  [1] 1 2 0 1 2 0 1 2 0 1

الحساب التمطي مفيد في التعامل مع مجموعة بيانات flights، حيث يمكننا استخدامه لتفكيك متغير وقت المغادرة المجدول sched_dep_time إلى ساعه hour ودقيقة minute:

flights |> 
  mutate(
    hour = sched_dep_time %/% 100,
    minute = sched_dep_time %% 100,
    .keep = "used"
  )
#> # A tibble: 336,776 × 3
#>   sched_dep_time  hour minute
#>            <int> <dbl>  <dbl>
#> 1            515     5     15
#> 2            529     5     29
#> 3            540     5     40
#> 4            545     5     45
#> 5            600     6      0
#> 6            558     5     58
#> # ℹ 336,770 more rows

يمكننا دمج ذلك مع حيلة mean(is.na(x)) من قسم 12.4 لرؤية كيف تتغير نسبة الرحلات الملغاة على مدار اليوم. النتائج موضحة في الشكل 13.1.

flights |> 
  group_by(hour = sched_dep_time %/% 100) |> 
  summarize(prop_cancelled = mean(is.na(dep_time)), n = n()) |> 
  filter(hour > 1) |> 
  ggplot(aes(x = hour, y = prop_cancelled)) +
  geom_line(color = "grey50") + 
  geom_point(aes(size = n))
رسم بياني خطي يوضح تغير نسبة الرحلات الملغاة على مدار اليوم. تبدأ النسبة منخفضة عند حوالي 0.5% في الساعة 5 صباحاً، ثم تزداد بثبات على مدار اليوم حتى تصل إلى ذروتها عند 4% في الساعة 7 مساءً. ثم تنخفض نسبة الرحلات الملغاة بسرعة لتصل إلى حوالي 1% بحلول منتصف الليل.
الشكل 13.1: رسم بياني خطي يوضح ساعة المغادرة المجدولة على المحور الأفقي، ونسبة الرحلات الملغاة على المحور الراسي. يبدو أن الإلغاءات تتراكم على مدار اليوم حتى الساعة 8 مساءً، بينما الرحلات المتأخرة جداً تكون أقل عرضة للإلغاء.

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(123.456, 2)  # خانتان عشريتان
#> [1] 123.46
round(123.456, 1)  # خانة عشرية واحدة
#> [1] 123.5
round(123.456, -1) # تقريب لأقرب عشرة
#> [1] 120
round(123.456, -2) # تقريب لأقرب مائة
#> [1] 100

توجد غرابة واحدة في الدالة round() تبدو مفاجئة للوهلة الأولى:

round(c(1.5, 2.5))
#> [1] 2 2

تستخدم round() ما يُعرف باسم “التقريب للعدد الزوجي الأقرب” (round half to even) أو تقريب المصرفيين (Banker’s rounding): إذا كان العدد يقع في المنتصف تماماً بين عددين صحيحين، فسيتم تقريبه إلى العدد الزوجي. تعتبر هذه استراتيجية جيدة لأنها تحافظ على التقريب خالية من التحيز: فنصف حالات 0.5 تُقرب للأعلى، والنصف الآخر يُقرب للأسفل.

تقترن الدالة round() مع الدالة floor() التي تقرب دائماً للأسفل، والدالة ceiling() التي تقرب دائماً للأعلى:

x <- 123.456

floor(x)
#> [1] 123
ceiling(x)
#> [1] 124

لا تحتوي هذه الدوال على الوسيط digits؛ لذا يمكنك بدلاً من ذلك تقليل المقياس، ثم التقريب، ثم إعادة المقياس لما كان عليه:

# التقريب للأسفل لأقرب خانتين عشريتين
floor(x / 0.01) * 0.01
#> [1] 123.45
# التقريب للأعلى لأقرب خانتين عشريتين
ceiling(x / 0.01) * 0.01
#> [1] 123.46

يمكنك استخدام نفس الأسلوب إذا أردت استخدام round() للتقريب لمضاعفات عدد آخر:

# التقريب لأقرب مضاعف للعدد 4
round(x / 4) * 4
#> [1] 124

# التقريب لأقرب 0.25
round(x / 0.25) * 0.25
#> [1] 123.5

13.4.6 تقسيم الأعداد إلى فئات

استخدم cut()1 لتقسيم المتجه العددي إلى فئات ومجموعات منفصلة:

x <- c(1, 2, 5, 10, 15, 20)
cut(x, breaks = c(0, 5, 10, 15, 20))
#> [1] (0,5]   (0,5]   (0,5]   (5,10]  (10,15] (15,20]
#> Levels: (0,5] (5,10] (10,15] (15,20]

لا يلزم أن تكون نقاط التقسيم (breaks) متساوية المسافات:

cut(x, breaks = c(0, 5, 10, 100))
#> [1] (0,5]    (0,5]    (0,5]    (5,10]   (10,100] (10,100]
#> Levels: (0,5] (5,10] (10,100]

يمكنك اختيارياً تزويد الدالة بتسمياتك الخاصة عبر labels. لاحظ أنه يجب أن تكون التسميات labels أقل بواحد من عدد نقاط التقسيم breaks.

cut(x, 
  breaks = c(0, 5, 10, 15, 20), 
  labels = c("sm", "md", "lg", "xl")
)
#> [1] sm sm sm md lg xl
#> Levels: sm md lg xl

أي قيم تقع خارج نطاق نقاط التقسيم ستصبح قيم مفقودة NA:

y <- c(NA, -10, 5, 10, 30)
cut(y, breaks = c(0, 5, 10, 15, 20))
#> [1] <NA>   <NA>   (0,5]  (5,10] <NA>  
#> Levels: (0,5] (5,10] (10,15] (15,20]

راجع الوثائق لمعرفة وسائط أخرى مفيدة مثل 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 تمارين

  1. اشرح بالكلمات ما يفعله كل سطر من الأكواد المستخدمة لإنشاء الشكل 13.1.

  2. ما هي الدوال المثلثية التي توفرها R؟ خمن بعض الأسماء وابحث عنها في الوثائق. هل تستخدم الدرجات أم التقدير الدائري (راديان)؟

  3. في الوقت الحالي، يُعد المتغيران dep_time و sched_dep_time مريحين للعرض، لكن يصعب الحساب بهما لأنهما ليسا أعداداً متصلة حقيقية. يمكنك رؤية المشكلة الأساسية عن طريق تشغيل الكود أدناه: هناك فجوة بين كل ساعة وأخرى.

    flights |> 
      filter(month == 1, day == 1) |> 
      ggplot(aes(x = sched_dep_time, y = dep_delay)) +
      geom_point()

    قم بتحويلهما إلى تمثيل أكثر دقة للوقت (إما كسور الساعات أو الدقائق منذ منتصف الليل).

  4. قَرّب المتغيرين dep_time و arr_time لأقرب خمس دقائق.

13.5 التحويلات العامة

تصف الأقسام التالية بعض التحويلات العامة التي تُستخدم غالباً مع المتجهات العددية، ولكن يمكن تطبيقها على جميع أنواع الأعمدة الأخرى.

13.5.1 الرتب (Ranks)

توفر dplyr عدداً من دوال الترتيب المستوحاة من SQL، ولكن يجب أن تبدأ دائماً بالدالة dplyr::min_rank(). وهي تستخدم الطريقة التقليدية للتعامل مع التعادلات، مثل: 1st، 2nd، 2nd، 4th.

x <- c(1, 5, 5, 17, 22, NA)
min_rank(x)
#> [1]  1  2  2  4  5 NA

لاحظ أن أصغر القيم تحصل على أدنى الرتب؛ استخدم desc(x) لإعطاء أكبر القيم أدنى الرتب:

min_rank(desc(x))
#> [1]  5  3  3  2  1 NA

إذا كانت 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 rows

13.5.2 الإزاحات (Offsets)

تتيح لك الدالتان dplyr::lead() و dplyr::lag() الإشارة إلى القيم الواقعة مباشرة قبل أو بعد القيمة “الحالية”. وهما ترجعان متجهاً بنفس طول المدخلات، ومبطناً بالقيم المفقودة NA في البداية أو النهاية:

x <- c(2, 5, 11, 11, 19, 35)
lag(x)
#> [1] NA  2  5 11 11 19
lead(x)
#> [1]  5 11 11 19 35 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 من الدقائق منذ آخر نشاط. تخيل مثلاً أن لديك الأوقات التي زار فيها شخص ما موقعاً إلكترونياً:

events <- tibble(
  time = c(0, 1, 2, 3, 5, 10, 12, 15, 17, 19, 20, 27, 28, 30)
)

وقمت بحساب الوقت بين كل حدث والآخر، وحددت ما إذا كانت هناك فجوة كبيرة كافية للتأهل:

events <- events |> 
  mutate(
    diff = time - lag(time, default = first(time)),
    has_gap = diff >= 5
  )
events
#> # A tibble: 14 × 3
#>    time  diff has_gap
#>   <dbl> <dbl> <lgl>  
#> 1     0     0 FALSE  
#> 2     1     1 FALSE  
#> 3     2     1 FALSE  
#> 4     3     1 FALSE  
#> 5     5     2 FALSE  
#> 6    10     5 TRUE   
#> # ℹ 8 more rows

ولكن كيف ننتقل من ذلك المتجه المنطقي (logical vector) إلى شيء يمكننا استخدامه مع الدالة group_by()؟ هنا تأتي الدالة cumsum() من قسم 13.4.7 لإنقاذ الموقف؛ حيث إن وجود فجوة (أي عندما تكون قيمة has_gap هي TRUE) سيؤدي إلى زيادة رقم المجموعة group بمقدار واحد (قسم 12.4.2):

events |> mutate(
  group = cumsum(has_gap)
)
#> # A tibble: 14 × 4
#>    time  diff has_gap group
#>   <dbl> <dbl> <lgl>   <int>
#> 1     0     0 FALSE       0
#> 2     1     1 FALSE       0
#> 3     2     1 FALSE       0
#> 4     3     1 FALSE       0
#> 5     5     2 FALSE       0
#> 6    10     5 TRUE        1
#> # ℹ 8 more rows

طريقة أخرى لإنشاء متغيرات التجميع هي استخدام الدالة consecutive_id()، والتي تبدأ مجموعة جديدة في كل مرة تتغير فيها إحدى الوسائط (arguments) المُمررة إليها. على سبيل المثال، وبإلهام من سؤال Stack Overflow هذا، تخيل أن لديك إطار بيانات (data frame) يحتوي على مجموعة من القيم المتكررة:

df <- tibble(
  x = c("a", "a", "a", "b", "c", "c", "d", "e", "a", "a", "b", "b"),
  y = c(1, 2, 3, 2, 4, 1, 3, 9, 4, 8, 10, 199)
)

إذا أردت الاحتفاظ بالصف الأول من كل قيمة مكررة في 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 row

13.5.4 تمارين

  1. أوجد أكثر 10 رحلات تأخراً باستخدام دالة ترتيب. كيف تريد التعامل مع التعادلات؟ اقرأ وثائق min_rank() بعناية.

  2. أي طائرة حسب رقم الذيل (tailnum) لديها أسوأ سجل في الالتزام بالمواعيد؟

  3. ما هو الوقت من اليوم الذي ينبغي أن تسافر فيه إذا أردت تجنب التأخير قدر الإمكان؟

  4. ماذا يفعل التعبير flights |> group_by(dest) |> filter(row_number() < 4)؟ وماذا يفعل التعبير flights |> group_by(dest) |> filter(row_number(dep_delay) < 4)؟

  5. لكل وجهة وصول، احسب إجمالي دقائق التأخير. ولكل رحلة، احسب نسبة التأخير التي تمثلها من إجمالي التأخير لوجهتها.

  6. عادة ما تكون حالات التأخير مرتبطة زمنياً: حتى بعد حل المشكلة التي تسببت في التأخير الأولي، تتأخر الرحلات اللاحقة للسماح للرحلات السابقة بالمغادرة. باستخدام lag()، استكشف كيف يرتبط متوسط تأخير الرحلات لساعة معينة بمتوسط التأخير للساعة السابقة.

    flights |> 
      mutate(hour = dep_time %/% 100) |> 
      group_by(year, month, day, hour) |> 
      summarize(
        dep_delay = mean(dep_delay, na.rm = TRUE),
        n = n(),
        .groups = "drop"
      ) |> 
      filter(n > 5)
  7. انظر إلى كل وجهة وصول. هل يمكنك العثور على رحلات سريعة بشكل مشبوه (أي رحلات قد تمثل خطأ في إدخال البيانات)؟ احسب زمن الطيران للرحلة بالنسبة لأقصر رحلة إلى تلك الوجهة. أي الرحلات كانت الأكثر تأخراً في الجو؟

  8. أوجد جميع الوجهات التي تسير إليها شركتان للطيران على الأقل. استخدم تلك الوجهات للتوصل إلى ترتيب نسبي لشركات الطيران بناءً على أدائها لنفس الوجهة.

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()
تقع جميع النقاط أسفل خط 45 درجة، مما يعني أن الوسيط للتأخير دائماً أقل من المتوسط الحسابي. تتجمع معظم النقاط في منطقة كثيفة من المتوسط الحسابي [0, 20] والوسيط [-5, 5]. مع زيادة المتوسط الحسابي للتأخير، يزداد انتشار الوسيط أيضاً. هناك نقطتان متطرفتان مع متوسط حسابي ~60، ووسيط ~30، ومتوسط حسابي ~85، ووسيط ~55.
الشكل 13.2: رسم بياني نقطي يوضح الفروق بين تلخيص التأخير اليومي للمغادرة باستخدام الوسيط بدلاً من المتوسط الحسابي.

قد تتساءل أيضاً عن المنوال (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 rows

13.6.3 التشتت والانتشار

في بعض الأحيان لا تكون مهتماً بما يكفي بمكان تكتل الجزء الأكبر من البيانات، بل بكيفية انتشارها وتشتتها. ملخصان شائعا الاستخدام هما الانحراف المعياري sd(x) والمدى الربيعي IQR(). لن نشرح sd() هنا لأنك على دراية بها على الأرجح، لكن IQR() قد تكون جديدة بالنسبة لك — وهي تساوي quantile(x, 0.75) - quantile(x, 0.25) وتمنحك المدى الذي يحتوي على الـ 50% الوسطى من البيانات.

يمكننا استخدام هذا لكشف غرابة صغيرة في بيانات flights. قد تتوقع أن يكون انتشار وتشتت المسافة بين نقطة الانطلاق ووجهة الوصول صفراً، نظرًا لأن المطارات في نفس المكان دائماً. لكن الكود أدناه يكشف عن حالة غريبة في البيانات لمطار EGE:

flights |> 
  group_by(origin, dest) |> 
  summarize(
    distance_iqr = IQR(distance), 
    n = n(),
    .groups = "drop"
  ) |> 
  filter(distance_iqr > 0)
#> # A tibble: 2 × 4
#>   origin dest  distance_iqr     n
#>   <chr>  <chr>        <dbl> <int>
#> 1 EWR    EGE              1   110
#> 2 JFK    EGE              1   103

13.6.4 التوزيعات

من الجدير بالتذكر أن جميع الإحصاءات التلخيصية الموضحة أعلاه هي طريقة اختزال واختصار للتوزيع إلى رقم واحد. هذا يعني أنها اختزالية بشكل أساسي، وإذا اخترت المقياس التلخيصي الخاطئ، يمكنك بسهولة إغفال الفروق المهمة بين المجموعات. لهذا السبب تكون فكرة ممتازة دائماً تصور التوزيع ببيان خطي أو مدرج قبل اعتماد إحصاءاتك التلخيصية.

يوضح الشكل 13.3 التوزيع العام لتأخيرات المغادرة. التوزيع ملتوٍ لدرجة أننا نضطر إلى التكبير لرؤية معظم البيانات. هذا يشير إلى أنه من غير المرجح أن يكون المتوسط الحسابي ملخصاً جيداً وقد نفضل الوسيط بدلاً منه.

مدرجان تكراريان لـ `dep_delay`. في اليسار، من الصعب جداً رؤية أي نمط باستثناء وجود قمة كبيرة جداً حول الصفر، وتتلاشى الأعمدة بسرعة في الارتفاع، ولعظم الرسم لا يمكنك رؤية أي أعمدة قصيرة جداً. في اليمين، حيث استبعدنا التأخيرات التي تزيد عن ساعتين، يمكننا رؤية أن الذروة تحدث أسفل الصفر بقليل (أي أن معظم الرحلات مغادرة مبكراً بدقيقتين)، لكن لا يزال هناك تلاشٍ حاد جداً بعد ذلك.
الشكل 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)

توزيع `dep_delay` ملتوٍ جداً جهة اليمين مع ذروة قوية أقل بقليل من 0. تتداخل المضلعات التكرارية الـ 365 في معظمها لتشكل شريطاً أسود سميكاً.

لا تتردد في استكشاف ملخصاتك المخصصة المصممة خصيصاً للبيانات التي تعمل عليها. في هذه الحالة، قد يعني ذلك تلخيص الرحلات التي غادرت مبكرة بشكل منفصل عن الرحلات التي غادرت متأخرة، أو نظراً لأن القيم ملتوية للغاية، فقد تجرب التحويل اللوجاريتمي. أخيراً، لا تنسَ ما تعلمته في قسم 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 تمارين

  1. ابحث عن 5 طرق مختلفة على الأقل لتقييم خصائص التأخير النموذجية لمجموعة من الرحلات الجوية. متى تكون mean() مفيدة؟ متى تكون median() مفيدة؟ متى قد ترغب في استخدام شيء آخر؟ هل يجب استخدام تأخير الوصول أم تأخير المغادرة؟ لماذا قد ترغب في استخدام البيانات من جدول planes؟

  2. أي الوجهات تظهر أكبر تباين في السرعة الجوية؟

  3. أنشئ رسماً بيانياً لمزيد من استكشاف مغامرات مطار EGE. هل يمكنك العثور على أي دليل على أن المطار غير موقعه؟ هل يمكنك العثور على متغير آخر قد يفسر هذا الاختلاف؟

13.7 ملخص

أنت بالفعل على دراية بالعديد من الأدوات للتعامل مع الأعداد، وبعد قراءة هذا الفصل أصبحت تعرف الآن كيفية استخدامها في R. تعلمت أيضاً مجموعة من التحويلات العامة المفيدة التي تُطبق عادةً، ولكن ليس حصرياً، على المتجهات العددية مثل الرتب والإزاحات. أخيراً، عملت من خلال عدد من الملخصات العددية، وناقشت بعض التحديات الإحصائية التي يجب عليك مراعاتها.

عبر الفصلين القادمين، سنغوص في العمل مع النصوص السلسلية باستخدام حزمة stringr. النصوص موضوع كبير لذا يحظى بفصلين، أحدهما عن أساسيات النصوص والآخر عن التعبيرات النمطية (Regular expressions).


  1. توفر حزمة ggplot2 بعض الدوال المساعدة للحالات الشائعة مثل cut_interval() و cut_number() و cut_width(). يُعد وجود هذه الدوال داخل ggplot2 مكاناً غريباً نوعاً ما، لكنها مفيدة كجزء من حساب المدرجات التكرارية وكُتبت قبل ظهور بقية أجزاء tidyverse.↩︎

  2. تقوم الدالة mode() بمهام مختلفة تماماً في R!↩︎