12  المتجهات المنطقية

12.1 مقدمة

ستتعلم في هذا الفصل الأدوات المخصصة للتعامل مع المتجهات المنطقية (logical vectors). تُعد المتجهات المنطقية أبسط أنواع المتجهات؛ لأن كل عنصر فيها لا يمكن أن يتخذ سوى واحدة من ثلاث قيم ممكنة: TRUE (صحيح)، وFALSE (خاطئ)، وNA (قيمة مفقودة). ومن النادر نسبياً أن تجد متجهات منطقية جاهزة في بياناتك الخام، لكنك ستنشئها وتتعامل معها وتجري عليها العمليات في كل تحليل بيانات تقوم به تقريباً.

سنبدأ بمناقشة الطريقة الأكثر شيوعاً لإنشاء المتجهات المنطقية: استخدام المقارنات العددية (numeric comparisons). ثم ستتعلم كيف يمكنك استخدام الجبر البولياني (Boolean algebra) لدمج متجهات منطقية مختلفة، بالإضافة إلى بعض المقاييس التلخيصية المفيدة. وسنختتم الفصل بشرح الدالتين if_else() وcase_when()، وهما دالتان نافعتان لإجراء تغييرات شرطية تعتمد على المتجهات المنطقية.

12.1.1 المتطلبات المسبقة

معظم الدوال التي ستتعلمها في هذا الفصل توفرها البيئة الأساسية لـ R (base R)، لذا لسنا بحاجة ماسة لحزمة tidyverse، لكننا سنقوم بتحميلها حتى نتمكن من استخدام mutate() وfilter() والدوال الشقيقة للتعامل مع إطارات البيانات. كما سنواصل استعراض الأمثلة التطبيقية باستخدام مجموعة بيانات الرحلات الجوية nycflights13::flights.

ومع ذلك، كلما بدأنا في تغطية المزيد من الأدوات، لن يتوفر دائماً مثال واقعي مثالي لكل حالة. لذا سنبدأ بإنشاء بعض البيانات الافتراضية باستخدام الدالة c():

x <- c(1, 2, 3, 5, 7, 11, 13)
x * 2
#> [1]  2  4  6 10 14 22 26

يُسهل هذا أسلوب شرح الدوال المنفردة، لكنه يجعل رؤية كيفية تطبيقها على مشكلات البيانات الواقعية أشد صعوبة نوعاً ما. تذكر دائماً أن أي عملية نُجريها على متجه حر ومستقل، يمكنك تطبيقها على متغير داخل إطار بيانات باستخدام mutate() والدوال المماثلة.

df <- tibble(x)
df |> 
  mutate(y = x * 2)
#> # A tibble: 7 × 2
#>       x     y
#>   <dbl> <dbl>
#> 1     1     2
#> 2     2     4
#> 3     3     6
#> 4     5    10
#> 5     7    14
#> 6    11    22
#> # ℹ 1 more row

12.2 المقارنات

تعد المقارنات العددية باستخدام الأدوات <, <=, >, >=, !=, و == طريقة شائعة جداً لإنشاء المتجهات المنطقية. وحتى الآن، كنا ننشئ المتغيرات المنطقية بشكل مؤقت داخل الدالة filter()؛ حيث يُحسب المتغير، يُستخدم، ثم يُتخلص منه. على سبيل المثال، يحدد الفلتر (filter) التالي جميع الرحلات المغادرة نهاراً والتي وصلت في الموعد المحدد تقريباً:

flights |> 
  filter(dep_time > 600 & dep_time < 2000 & abs(arr_delay) < 20)
#> # A tibble: 172,286 × 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      601            600         1      844            850
#> 2  2013     1     1      602            610        -8      812            820
#> 3  2013     1     1      602            605        -3      821            805
#> 4  2013     1     1      606            610        -4      858            910
#> 5  2013     1     1      606            610        -4      837            845
#> 6  2013     1     1      607            607         0      858            915
#> # ℹ 172,280 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …

من المفيد أن تعلم أن هذه الطريقة الاختصارية يمكن الاستعاضة عنها بإنشاء المتغيرات المنطقية الكامنة صراحة باستخدام mutate():

flights |> 
  mutate(
    daytime = dep_time > 600 & dep_time < 2000,
    approx_ontime = abs(arr_delay) < 20,
    .keep = "used"
  )
#> # A tibble: 336,776 × 4
#>   dep_time arr_delay daytime approx_ontime
#>      <int>     <dbl> <lgl>   <lgl>        
#> 1      517        11 FALSE   TRUE         
#> 2      533        20 FALSE   FALSE        
#> 3      542        33 FALSE   FALSE        
#> 4      544       -18 FALSE   TRUE         
#> 5      554       -25 FALSE   FALSE        
#> 6      554        12 FALSE   TRUE         
#> # ℹ 336,770 more rows

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

وبشكل عام، فإن الفلتر المبدئي يعادل تماماً الكود التالي:

flights |> 
  mutate(
    daytime = dep_time > 600 & dep_time < 2000,
    approx_ontime = abs(arr_delay) < 20,
  ) |> 
  filter(daytime & approx_ontime)

12.2.1 مقارنة الأعداد ذات الفاصلة العائمة

احذر من استخدام معامل المساواة == مع الأعداد. على سبيل المثال، يبدو للوهلة الأولى أن هذا المتجه يحتوي على العددين 1 و 2:

x <- c(1 / 49 * 49, sqrt(2) ^ 2)
x
#> [1] 1 2

لكن إذا اختبرت مساواتهما، ستكون النتيجة FALSE:

x == c(1, 2)
#> [1] FALSE FALSE

ما الذي يحدث هنا؟ تخزن أجهزة الحاسوب الأعداد بعدد ثابت من الخانات العشرية، لذا لا توجد طريقة لتمثيل الكسر 1/49 أو sqrt(2) بشكل دقيق مطلق، وتكون الحسابات اللاحقة غير دقيقة بفارق ضئيل جداً. يمكننا رؤية القيم الدقيقة تماماً باستدعاء الدالة print() مع تحديد الوسيط digits1:

print(x, digits = 16)
#> [1] 0.9999999999999999 2.0000000000000004

يتضح لك الآن السبب في أن R تقوم بتقريب هذه الأعداد افتراضياً؛ لأنها في الحقيقة قريبة جداً مما تتوقعه.

والآن بعد أن رأيت سبب فشل المعامل ==، ما الذي يمكنك فعله حيال ذلك؟ أحد الخيارات المتاحة هو استخدام الدالة dplyr::near() والتي تتجاهل الفروق الضئيلة جداً:

near(x, c(1, 2))
#> [1] TRUE TRUE

12.2.2 القيم المفقودة

تمثل القيم المفقودة أشياء مجهولة، لذا فهي “معدية”: أي عملية تتضمن قيمة مجهولة ستكون نتيجتها مجهولة أيضاً:

NA > 5
#> [1] NA
10 == NA
#> [1] NA

والنتيجة الأكثر إرباكاً هي هذه النتيجة:

NA == NA
#> [1] NA

تسهل ملامسة أسباب ذلك إذا أضفنا سياقاً توضيحياً بسيطاً:

# لا نعلم كم عمر ماري
age_mary <- NA

# لا نعلم كم عمر جون
age_john <- NA

# هل ماري وجون في نفس العمر؟
age_mary == age_john
#> [1] NA
# نحن لا نعلم!

لذا، إذا أردت العثور على جميع الرحلات التي تكون فيها قيمة ووقت المغادرة dep_time مفقودة، فإن الكود التالي لن يعمل لأن dep_time == NA سيعطي النتيجة NA لكل صف، والدالة filter() تستبعد القيم المفقودة تلقائياً:

flights |> 
  filter(dep_time == NA)
#> # A tibble: 0 × 19
#> # ℹ 19 variables: year <int>, month <int>, day <int>, dep_time <int>,
#> #   sched_dep_time <int>, dep_delay <dbl>, arr_time <int>, …

بدلاً من ذلك، سنحتاج إلى أداة جديدة: الدالة is.na().

12.2.3 is.na()

تعمل الدالة is.na(x) مع أي نوع من المتجهات، وترجع القيمة TRUE مع القيم المفقودة و FALSE مع كل ما عداها:

is.na(c(TRUE, NA, FALSE))
#> [1] FALSE  TRUE FALSE
is.na(c(1, NA, 3))
#> [1] FALSE  TRUE FALSE
is.na(c("a", NA, "b"))
#> [1] FALSE  TRUE FALSE

يمكننا استخدام الدالة is.na() للبحث عن جميع الصفوف التي تحتوي على قيمة مفقودة في dep_time:

flights |> 
  filter(is.na(dep_time))
#> # A tibble: 8,255 × 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       NA           1630        NA       NA           1815
#> 2  2013     1     1       NA           1935        NA       NA           2240
#> 3  2013     1     1       NA           1500        NA       NA           1825
#> 4  2013     1     1       NA            600        NA       NA            901
#> 5  2013     1     2       NA           1540        NA       NA           1747
#> 6  2013     1     2       NA           1620        NA       NA           1746
#> # ℹ 8,249 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …

يمكن أن تكون is.na() مفيدة أيضاً داخل الدالة arrange(). تضع الدالة arrange() عادة جميع القيم المفقودة في النهاية، ولكن يمكنك تجاوز هذا السلوك الافتراضي بالترتيب أولاً حسب is.na():

flights |> 
  filter(month == 1, day == 1) |> 
  arrange(dep_time)
#> # 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, day == 1) |> 
  arrange(desc(is.na(dep_time)), dep_time)
#> # 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       NA           1630        NA       NA           1815
#> 2  2013     1     1       NA           1935        NA       NA           2240
#> 3  2013     1     1       NA           1500        NA       NA           1825
#> 4  2013     1     1       NA            600        NA       NA            901
#> 5  2013     1     1      517            515         2      830            819
#> 6  2013     1     1      533            529         4      850            830
#> # ℹ 836 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …

سنعود لتغطية القيم المفقودة بمزيد من التوسع والعمق في الفصل 18.

12.2.4 تمارين

  1. كيف تعمل الدالة dplyr::near()؟ اكتب near للظهور على الكود المصدري الخاص بها. هل sqrt(2)^2 قريبة من 2؟
  2. استخدم mutate() و is.na() و count() معاً لوصف كيفية ارتباط القيم المفقودة في المتغيرات dep_time و sched_dep_time و dep_delay ببعضها البعض.

12.3 الجبر البولياني (Boolean algebra)

بمجرد توفر متجهات منطقية متعددة لديك، يمكنك دمجها معاً باستخدام الجبر البولياني. في بيئة R، يمثل المعامل & المعامل المنطقي “و” (and)، ويمثل المعامل | المعامل المنطقي “أو” (or)، بينما يمثل المعامل ! المعامل المنطقي “ليس” (not)، وتمثل الدالة xor() معامل “أو الاستبعادي” (exclusive or)2. على سبيل المثال، الاستعلام df |> filter(!is.na(x)) يجد جميع الصفوف التي تكون فيها القيمة x غير مفقودة، بينما الاستعلام df |> filter(x < -10 | x > 0) يجد جميع الصفوف التي تكون فيها القيمة x أصغر من -10 أو أكبر من 0. يوضح الشكل 12.1 أمثلة على العمليات البوليانية الشائعة الاستخدام وكيفية عملها.

سبعة مخططات فين (Venn diagrams)، يشرح كل منها معاملاً منطقياً معنياً. تمثل الدوائر (المجموعات) في كل مخطط القيمتين x و y.
الشكل 12.1: مجموعة من العمليات البوليانية الشائعة الاستخدام. تمثل x الدائرة اليسرى، وتمثل y الدائرة اليمنى، بينما توضح المناطق المظللة الأجزاء التي يحددها كل معامل.

بالإضافة إلى المعاملات & و |، تتضمن بيئة R أيضاً المعاملين && و ||. لا تستخدمهما إطلاقاً داخل دوال حزمة dplyr! تُعرف هذه المعاملات بمعاملات التقصير (short-circuiting operators) ولا ترجع سوى قيمة فريدة واحدة إما TRUE أو FALSE؛ وهي أدوات موجهة للبرمجة المتقدمة وليست مخصصة لعلم البيانات.

12.3.1 القيم المفقودة

تُعد قواعد التعامل مع القيم المفقودة في الجبر البولياني معقدة في الشرح نوعاً ما لأنها تبدو غير متسقة للوهلة الأولى:

df <- tibble(x = c(TRUE, FALSE, NA))

df |> 
  mutate(
    and = x & NA,
    or = x | NA
  )
#> # A tibble: 3 × 3
#>   x     and   or   
#>   <lgl> <lgl> <lgl>
#> 1 TRUE  NA    TRUE 
#> 2 FALSE FALSE NA   
#> 3 NA    NA    NA

ولفهم ما يحدث هنا، فكّر في التعبير NA | TRUE (أي NA أو TRUE). تعني القيمة المفقودة في المتجه المنطقي أن القيمة الكامنة قد تكون إما TRUE أو FALSE. التعبيرات TRUE | TRUE و FALSE | TRUE تعطي كلاهما القيمة TRUE؛ لأن شرطاً واحداً على الأقل منهما محقق وصحيح. وبالتالي، فإن التعبير NA | TRUE يجب أن يعطي أيضاً القيمة TRUE؛ لأن القيمة المفقودة NA بغض النظر عن كونها TRUE أو FALSE لن تغير النتيجة. ولكن التعبير NA | FALSE يعطي القيمة NA؛ لأننا لا نعلم بعد هل القيمة المفقودة هي TRUE أم FALSE. وينطبق التفكير المنطقي ذاته على المعامل & مع مراعاة ضرورة تحقق كلا الشرطين معاً؛ لذا فإن NA & TRUE تعطي NA لأننا لا نعلم طبيعة القيمة المفقودة، بينما تعطي NA & FALSE القيمة FALSE؛ لأن أحد الشرطين غير محقق بالتأكيد.

12.3.2 ترتيب العمليات

لاحظ أن ترتيب العمليات لا يعمل كما هو الحال في اللغة الإنجليزية. تأمل الكود التالي الذي يحدد جميع الرحلات المغادرة في شهري نوفمبر أو ديسمبر:

flights |> 
  filter(month == 11 | month == 12)

قد ترغب في كتابته كما تقول باللغة الإنجليزية: “أوجد جميع الرحلات التي غادرت في نوفمبر أو ديسمبر”:

flights |> 
  filter(month == 11 | 12)
#> # 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>, …

هذا الكود لا يعطي خطأ برمجياً ولكنه لا يعمل كما ينبغي. ما الذي يحدث هنا؟ تقوم R أولاً بتقييم month == 11 فتنشئ متجهاً منطقياً نسميه nov. ثم تحسب nov | 12. عندما تستخدم عدداً مع معامل منطقي، فإن R تحول أي عدد غير الصفر إلى TRUE؛ لذا فإن التعبير يعادل nov | TRUE والتي تكون دائماً TRUE؛ وبالتالي سيتم تحديد جميع الصفوف في مجموعة البيانات:

flights |> 
  mutate(
    nov = month == 11,
    final = nov | 12,
    .keep = "used"
  )
#> # A tibble: 336,776 × 3
#>   month nov   final
#>   <int> <lgl> <lgl>
#> 1     1 FALSE TRUE 
#> 2     1 FALSE TRUE 
#> 3     1 FALSE TRUE 
#> 4     1 FALSE TRUE 
#> 5     1 FALSE TRUE 
#> 6     1 FALSE TRUE 
#> # ℹ 336,770 more rows

12.3.3 المعامل %in%

تعد طريقة %in% خياراً سهلاً لتفادي أخطاء الترتيب بين المعاملين == و |. يرجع التعبير x %in% y متجهاً منطقياً بنفس طول x تكون قيمته TRUE كلما كانت القيمة الموجودة في x متوفرة في أي مكان داخل y.

1:12 %in% c(1, 5, 11)
#>  [1]  TRUE FALSE FALSE FALSE  TRUE FALSE FALSE FALSE FALSE FALSE  TRUE FALSE
letters %in% c("a", "e", "i", "o", "u")
#>  [1]  TRUE FALSE FALSE FALSE  TRUE FALSE FALSE FALSE  TRUE FALSE FALSE FALSE
#> [13] FALSE FALSE  TRUE FALSE FALSE FALSE FALSE FALSE  TRUE FALSE FALSE FALSE
#> [25] FALSE FALSE

لذا للعثور على جميع الرحلات الجوية في شهري نوفمبر وديسمبر يمكننا كتابة:

flights |> 
  filter(month %in% c(11, 12))

لاحظ أن %in% تتبع قواعد مختلفة للقيم المفقودة NA مقارنة بـ ==؛ حيث إن NA %in% NA تعطي TRUE.

c(1, 2, NA) == NA
#> [1] NA NA NA
c(1, 2, NA) %in% NA
#> [1] FALSE FALSE  TRUE

يمكن أن يوفر هذا طريقة اختصار مفيدة:

flights |> 
  filter(dep_time %in% c(NA, 0800))
#> # A tibble: 8,803 × 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      800            800         0     1022           1014
#> 2  2013     1     1      800            810       -10      949            955
#> 3  2013     1     1       NA           1630        NA       NA           1815
#> 4  2013     1     1       NA           1935        NA       NA           2240
#> 5  2013     1     1       NA           1500        NA       NA           1825
#> 6  2013     1     1       NA            600        NA       NA            901
#> # ℹ 8,797 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …

12.3.4 تمارين

  1. أوجد جميع الرحلات التي تكون فيها قيمة تأخير الوصول arr_delay مفقودة بينما قيمة تأخير المغادرة dep_delay غير مفقودة. ثم أوجد جميع الرحلات التي لا تكون فيها قيمة وقت الوصول arr_time ولا وقت الوصول المجدول sched_arr_time مفقودة، لكن قيمة تأخير الوصول arr_delay مفقودة.
  2. كم عدد الرحلات التي تحتوي على قيمة مفقودة في ووقت المغادرة dep_time؟ ما هي المتغيرات الأخرى المفقودة في هذه الصفوف؟ ماذا قد تمثل هذه الصفوف؟
  3. بفرض أن القيمة المفقودة في ووقت المغادرة dep_time تعني أن الرحلة قد أُلغيت، افحص عدد الرحلات المُلغاة يومياً. هل هناك نمط معين؟ هل توجد علاقة بين نسبة الرحلات المُلغاة ومتوسط التأخير للرحلات غير المُلغاة؟

12.4 الملخصات الإحصائية

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

12.4.1 الملخصات المنطقية

هناك دالتان تلخيصيتان منطقيتان رئيسيتان: any() و all(). الدالة any(x) هي المكافئ للمعامل |؛ حيث ترجع TRUE إذا وجِدت أي قيمة TRUE واحدة على الأقل في المتجه x. الدالة all(x) هي المكافئ للمعامل &؛ حيث ترجع TRUE فقط إذا كانت جميع القيم في المتجه x هي TRUE. ومثل معظم دوال التلخيص، يمكنك استبعاد القيم المفقودة باستخدام الوسيط na.rm = TRUE.

على سبيل المثال، يمكننا استخدام all() و any() لمعرفة ما إذا كانت كل رحلة قد تأخرت عند المغادرة بساعة واحدة على الأكثر، أو ما إذا كانت هناك أي رحلات تأخرت عند الوصول بمقدار خمس ساعات أو أكثر. وتتيح لنا الدالة group_by() تطبيق ذلك لكل يوم على حدة:

flights |> 
  group_by(year, month, day) |> 
  summarize(
    all_delayed = all(dep_delay <= 60, na.rm = TRUE),
    any_long_delay = any(arr_delay >= 300, na.rm = TRUE),
    .groups = "drop"
  )
#> # A tibble: 365 × 5
#>    year month   day all_delayed any_long_delay
#>   <int> <int> <int> <lgl>       <lgl>         
#> 1  2013     1     1 FALSE       TRUE          
#> 2  2013     1     2 FALSE       TRUE          
#> 3  2013     1     3 FALSE       FALSE         
#> 4  2013     1     4 FALSE       FALSE         
#> 5  2013     1     5 FALSE       TRUE          
#> 6  2013     1     6 FALSE       FALSE         
#> # ℹ 359 more rows

ومع ذلك، في معظم الحالات تكون الدالتان any() و all() غير دقيقتين بما يكفي، ويكون من الأفضل معرفة المزيد من التفاصيل حول عدد القيم الصحيحة TRUE أو الخاطئة FALSE؛ وهو ما ينقلنا إلى الملخصات العددية.

12.4.2 الملخصات العددية للمتجهات المنطقية

عندما تستخدم متجهاً منطقياً في سياق عددي، تتحول القيمة TRUE إلى 1 وتتحول القيمة FALSE إلى 0. هذا يجعل الدالتين sum() و mean() مفيدتين جداً مع المتجهات المنطقية؛ حيث تعطي sum(x) عدد الحالات الصحيحة TRUE بينما تعطي mean(x) نسبة الحالات الصحيحة TRUE (لأن mean() هي ببساطة ناتج قسمة sum() على الطول length()).

يتيح لنا ذلك، على سبيل المثال، معرفة نسبة الرحلات التي تأخرت عند المغادرة بساعة واحدة على الأكثر وعدد الرحلات التي تأخرت عند الوصول بخمس ساعات أو أكثر:

flights |> 
  group_by(year, month, day) |> 
  summarize(
    proportion_delayed = mean(dep_delay <= 60, na.rm = TRUE),
    count_long_delay = sum(arr_delay >= 300, na.rm = TRUE),
    .groups = "drop"
  )
#> # A tibble: 365 × 5
#>    year month   day proportion_delayed count_long_delay
#>   <int> <int> <int>              <dbl>            <int>
#> 1  2013     1     1              0.939                3
#> 2  2013     1     2              0.914                3
#> 3  2013     1     3              0.941                0
#> 4  2013     1     4              0.953                0
#> 5  2013     1     5              0.964                1
#> 6  2013     1     6              0.959                0
#> # ℹ 359 more rows

12.4.3 الاقتطاع المنطقي (Logical subsetting)

هناك استخدام أخير للمتجهات المنطقية في التلخيص: يمكنك استخدام متجه منطقي لتصفية متغير واحد واقتطاع جزئية معينة تهتم بها. ويعتمد هذا على معامل الاقتطاع الأساسي [ (الأقواس المربعة)، والذي ستتعلم عنه المزيد في قسم 27.2.

تخيل أننا نريد حساب متوسط التأخير فقط للرحلات التي تأخرت بالفعل. إحدى الطرق للقيام بذلك هي تصفية الرحلات أولاً ثم حساب متوسط التأخير:

flights |> 
  filter(arr_delay > 0) |> 
  group_by(year, month, day) |> 
  summarize(
    behind = mean(arr_delay),
    n = n(),
    .groups = "drop"
  )
#> # A tibble: 365 × 5
#>    year month   day behind     n
#>   <int> <int> <int>  <dbl> <int>
#> 1  2013     1     1   32.5   461
#> 2  2013     1     2   32.0   535
#> 3  2013     1     3   27.7   460
#> 4  2013     1     4   28.3   297
#> 5  2013     1     5   22.6   238
#> 6  2013     1     6   24.4   381
#> # ℹ 359 more rows

هذا يعمل، ولكن ماذا لو أردنا أيضاً حساب متوسط التأخير للرحلات التي وصلت مبكراً؟ سنحتاج إلى إجراء خطوة تصفية (filter) منفصلة، ثم معرفة كيفية دمج إطاري البيانات (data frames) معاً.3 بدلاً من ذلك، يمكنك استخدام الأقواس المربعة [ لإجراء تصفية مضمنة (inline filtering)؛ حيث إنَّ arr_delay[arr_delay > 0] سينتج عنه فقط قيم تأخير الوصول الموجبة.

يقودنا هذا إلى الكود التالي:

flights |> 
  group_by(year, month, day) |> 
  summarize(
    behind = mean(arr_delay[arr_delay > 0], na.rm = TRUE),
    ahead = mean(arr_delay[arr_delay < 0], na.rm = TRUE),
    n = n(),
    .groups = "drop"
  )
#> # A tibble: 365 × 6
#>    year month   day behind ahead     n
#>   <int> <int> <int>  <dbl> <dbl> <int>
#> 1  2013     1     1   32.5 -12.5   842
#> 2  2013     1     2   32.0 -14.3   943
#> 3  2013     1     3   27.7 -18.2   914
#> 4  2013     1     4   28.3 -17.0   915
#> 5  2013     1     5   22.6 -14.0   720
#> 6  2013     1     6   24.4 -13.6   832
#> # ℹ 359 more rows

لاحظ أيضاً الفرق في حجم المجموعة: في التجميع الأول يعطي n() عدد الرحلات المتأخرة يومياً، بينما في التجميع الثاني يعطي n() العدد الإجمالي للرحلات.

12.4.4 تمارين

  1. ماذا يخبرك التعبير sum(is.na(x))؟ وماذا عن التعبير mean(is.na(x))؟
  2. ماذا ترجع الدالة prod() عند تطبيقها على متجه منطقي؟ وما هي دالة التلخيص المنطقية التي تكافئها؟ وماذا ترجع الدالة min() عند تطبيقها على متجه منطقي؟ وما هي دالة التلخيص المنطقية التي تكافئها؟ اقرأ وثائق الدوال وأجرِ بعض التجارب.

12.5 التحويلات الشرطية (Conditional transformations)

من أقوى ميزات المتجهات المنطقية استخدامها في إجراء تحويلات شرطية؛ أي القيام بأمر معين عند تحقق الشرط x، وإجراء أمر مختلف تماماً عند تحقق الشرط y. وهناك أداتان رئيسيتان لتحقيق ذلك: الدالة if_else() والدالة case_when().

12.5.1 الدالة if_else()

إذا أردت استخدام قيمة معينة عندما يكون الشرط صحيحاً TRUE وقيمة أخرى عندما يكون الشرط خاطئاً FALSE، يمكنك استخدام الدالة dplyr::if_else()4. ستستخدم دائماً الوسائط الثلاثة الأولى والدائمة للدالة if_else()؛ الوسيط الأول condition وهو متجه منطقي، والوسيط الثاني true ويحدد المخرجات في حالة صحة الشرط، والوسيط الثالث false ويحدد المخرجات في حالة عدم صحة الشرط.

لنبدأ بمثال بسيط لتصنيف متجه عددي إما كقيمة موجبة “+ve” أو قيمة سالبة “-ve”:

x <- c(-3:3, NA)
if_else(x > 0, "+ve", "-ve")
#> [1] "-ve" "-ve" "-ve" "-ve" "+ve" "+ve" "+ve" NA

يتوفر أيضاً وسيط رابع اختياري وهو missing يُستخدم لتحديد القيمة المستخرجة إذا كان المدخل قيمة مفقودة NA:

if_else(x > 0, "+ve", "-ve", "???")
#> [1] "-ve" "-ve" "-ve" "-ve" "+ve" "+ve" "+ve" "???"

يمكنك أيضاً استخدام متجهات للوسيطين true و false. يتيح لنا هذا، على سبيل المثال، إنشاء تطبيق مبسط وبديل للدالة abs() (القيمة المطلقة):

if_else(x < 0, -x, x)
#> [1]  3  2  1  0  1  2  3 NA

حتى الآن استخدمت جميع الوسائط نفس المتجهات، لكن بالطبع يمكنك المزج والتوفيق بين المتجهات المتنوعة. على سبيل المثال، يمكنك بناء نسخة مبسطة من الدالة coalesce() على النحو التالي:

x1 <- c(NA, 1, 2, NA)
y1 <- c(3, NA, 4, 6)
if_else(is.na(x1), y1, x1)
#> [1] 3 1 2 6

ربما لاحظت دقة غير مكتملة في مثال تصنيف الأعداد السابق: الصفر ليس موجباً ولا سالباً. يمكننا حل هذه المشكلة بإضافة دالة if_else() متداخلة أخرى:

if_else(x == 0, "0", if_else(x < 0, "-ve", "+ve"), "???")
#> [1] "-ve" "-ve" "-ve" "0"   "+ve" "+ve" "+ve" "???"

يصعب قراءة هذا الكود نوعاً ما، ويمكنك أن تتخيل مدى الصعوبة والتعقيد إذا كان لديك العديد من الشروط المتداخلة. بدلاً من ذلك، يمكنك الانتقال إلى استخدام الدالة المباشرة dplyr::case_when().

12.5.2 الدالة case_when()

مفهوم الدالة case_when() في حزمة dplyr مستوحى من جملة CASE في لغة SQL، وهي توفر طريقة مرنة للغاية لإجراء حسابات مختلفة وفقاً لشروط متعددة ومتنوعة. تتميز بتركيب برمجي خاص يتكون من أزواج بصيغة condition ~ output. يجب أن يكون condition متجهاً منطقياً؛ وعندما تكون قيمته صحيح TRUE يتم إرجاع النتيجة المقابلة في output.

هذا يعني أنه يمكننا إعادة بناء المثال المتداخل السابق لـ if_else() على النحو التالي:

x <- c(-3:3, NA)
case_when(
  x == 0   ~ "0",
  x < 0    ~ "-ve", 
  x > 0    ~ "+ve",
  is.na(x) ~ "???"
)
#> [1] "-ve" "-ve" "-ve" "0"   "+ve" "+ve" "+ve" "???"

يتطلب هذا صياغة أطول نوعاً ما، لكنه أكثر وضوحاً ودقة في التعبير.

لشرح كيفية عمل الدالة case_when()، دعنا نستكشف بعض الحالات البسيطة. إذا لم تتطابق أي من الحالات، ترجع النتيجة القيمة المفقودة NA:

case_when(
  x < 0 ~ "-ve",
  x > 0 ~ "+ve"
)
#> [1] "-ve" "-ve" "-ve" NA    "+ve" "+ve" "+ve" NA

استخدم الوسيط .default إذا أردت تعيين قيمة افتراضية شاملة لكل الحالات الأخرى المتبقية:

case_when(
  x < 0 ~ "-ve",
  x > 0 ~ "+ve",
  .default = "???"
)
#> [1] "-ve" "-ve" "-ve" "???" "+ve" "+ve" "+ve" "???"

ولاحظ أنه إذا تطابقت شروط متعددة، فسيتم تطبيق وتنفيذ الشرط الأول الذي يتحقق فقط:

case_when(
  x > 0 ~ "+ve",
  x > 2 ~ "big"
)
#> [1] NA    NA    NA    NA    "+ve" "+ve" "+ve" NA

تماماً كما هو الحال مع الدالة if_else()، يمكنك استخدام المتغيرات على كلي جانبي العلامة ~ والمزج بينها حسب متطلبات المسألة التحليلية لديك. على سبيل المثال، يمكننا استخدام case_when() لإضافة تسميات سهلة القراءة والتفسير لحالات تأخير الوصول:

flights |> 
  mutate(
    status = case_when(
      is.na(arr_delay)      ~ "cancelled",
      arr_delay < -30       ~ "very early",
      arr_delay < -15       ~ "early",
      abs(arr_delay) <= 15  ~ "on time",
      arr_delay < 60        ~ "late",
      arr_delay < Inf       ~ "very late",
    ),
    .keep = "used"
  )
#> # A tibble: 336,776 × 2
#>   arr_delay status 
#>       <dbl> <chr>  
#> 1        11 on time
#> 2        20 late   
#> 3        33 late   
#> 4       -18 early  
#> 5       -25 early  
#> 6        12 on time
#> # ℹ 336,770 more rows

كن حذراً عند كتابة هذا النوع من تعبيرات case_when() المعقدة؛ فالمحاولات الأولى قد تتداخل فيها الشروط عند خلط معاملات أصغر من < وأكبر من > بشكل غير مقصود.

12.5.3 الأنواع المتوافقة (Compatible types)

لاحظ أن كلاً من الدالتين if_else() و case_when() تطلب وجود أنواع بيانات متوافقة في المخرجات. وإذا كانت غير متوافقة، ستواجه أخطاء برمجية مثل هذه:

if_else(TRUE, "a", 1)
#> Error in `if_else()`:
#> ! Can't combine `true` <character> and `false` <double>.

case_when(
  x < -1 ~ TRUE,  
  x > 0  ~ now()
)
#> Error in `case_when()`:
#> ! Can't combine `..1 (right)` <logical> and `..2 (right)` <datetime<local>>.

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

  • المتجهات العددية والمنطقية متوافقة معاً، كما ناقشنا في قسم 12.4.2.
  • النصوص والعوامل (factors) متوافقة (الفصل 16)، لأنه يمكنك التفكير في العامل على أنه نص ذو مجموعة محددة ومقيدة من القيم.
  • التواريخ والتواريخ؛ والوقت متوافقة والتي سنناقشها في الفصل 17، لأنه يمكنك التفكير في التاريخ كحالة خاصة من التاريخ والوقت.
  • القيمة المفقودة NA - والتي تعد تقنياً متجهاً منطقياً - متوافقة مع جميع الأنواع، لأن كل نوع من المتجهات يحتوي على آلية لتمثيل القيم المفقودة.

لا نتوقع منك حفظ هذه القواعد عن ظهر قلب، ولكنها ستصبح مع الوقت عادة مكتسبة كجزء أصيل من طبيعة العمل نظراً لتطبيقها بشكل متسق عبر كافة حزم tidyverse.

12.5.4 تمارين

  1. يكون العدد زوجياً إذا كان يقبل القسمة على اثنين بدون باقٍ، والتي يمكنك معرفتها في بيئة R باستخدام التعبير x %% 2 == 0. استخدم هذه الحقيقة والدالة if_else() لتحديد ما إذا كان كل عدد بين 0 و 20 زوجياً أم فردياً.

  2. بفرض توفر متجه لأيام الأسبوع مثل x <- c("Monday", "Saturday", "Wednesday")، استخدم عبارة if_else() لتصنيف كل يوم كعطلة نهاية أسبوع (weekend) أو يوم عمل عادي (weekday).

  3. استخدم الدالة if_else() لحساب القيمة المطلقة لمتجه عددي يسمى x.

  4. اكتب عبارة case_when() تستخدم عامودي الشهر month واليوم day من مجموعة بيانات flights لتحديد وتسمية مجموعة مختارة من العطلات الرسمية المهمة في الولايات المتحدة (مثل: رأس السنة الجديدة، الرابع من يوليو، عيد الشكر، وعيد الميلاد). أنشئ أولاً عموداً منطقياً تتحدد قيمته بـ TRUE أو FALSE، ثم أنشئ عموداً نصياً يعطي اسم العطلة الرسمية أو القيمة المفقودة NA.

12.6 ملخص

تعريف المتجه المنطقي بسيط جداً؛ لأن كل قيمة يجب أن تكون إما TRUE أو FALSE أو NA. ومع ذلك، توفر المتجهات المنطقية إمكانات فائقة في معالجة البيانات. تعلمت في هذا الفصل كيفية إنشاء المتجهات المنطقية باستخدام المعاملات >, <, <=, >=, ==, !=, و is.na()، وكيفية دمجها باستخدام المعاملات المنطقية !, &, و |، وكيفية تلخيصها إحصائياً باستخدام any(), all(), sum(), و mean(). كما تعلمت أيضاً الإمكانات القوية للدالتين if_else() و case_when() اللتين تتيحان لك إرجاع قيم متغيرة استناداً إلى القيمة المنطقية المحسوبة.

سنلتقي بالمتجهات المنطقية مجدداً وبشكل متكرر في الفصول القادمة. على سبيل المثال، ستتعلم في الفصل 14 عن الدالة str_detect(x, pattern) التي ترجع متجهاً منطقياً تكون قيمته TRUE للعناصر في x التي تطابق النمط pattern؛ وفي الفصل 17 ستنشئ متجهات منطقية ناتجة عن مقارنة التواريخ والأوقات. ولكن الآن، سننتقل إلى النوع الرئيسي التالي والأكثر أهمية من المتجهات: وهو المتجهات العددية (numeric vectors).


  1. تقوم R عادة باستدعاء الدالة print تلقائياً (أي أن كتابة x هي اختصار لـ print(x))، لكن استدعاءها صراحة يكون مفيداً إذا أردت تمرير وسائط إضافية.↩︎

  2. تعطي الدالة xor(x, y) القيمة صحيح (TRUE) إذا كانت x صحيحة أو y صحيحة، ولكن ليس كلاهما معاً. وهذه هي الطريقة التي نستخدم بها كلمة “أو” عادةً في اللغة الإنجليزية؛ فعبارة “كلاهما” ليست إجابة مقبولة عادةً على السؤال: “هل تود تناول الآيس كريم أم الكعك؟”.↩︎

  3. سنغطي هذا في الفصل 19.↩︎

  4. تتشابه الدالة if_else() في حزمة dplyr تشابهاً كبيراً مع الدالة ifelse() في R الأساسية. ولكن تتميز if_else() بميزتين رئيسيتين: يمكنك تحديد كيفية التعامل مع القيم المفقودة، كما أنها أكثر دقة في إعطاء رسائل أخطاء واضحة ومفسرة إذا كانت متغيراتك ذات أنواع غير متوافقة.↩︎