27  دليل ميداني لـ base R

27.1 مقدمة

لإنهاء قسم البرمجة، سنقدم لك جولة سريعة على أهم دالّات base R التي لم نناقشها في الكتاب. تُعد هذه الأدوات مفيدة بشكل خاص كلما زادت ممارستك للبرمجة، وستساعدك على قراءة الكود الذي ستصادفه في المشاريع المختلفة.

هذا مكان ممتاز لتذكيرك بأن tidyverse ليست الطريقة الوحيدة لحل مشكلات علم البيانات. نحن ندرس tidyverse في هذا الكتاب لأن حزم tidyverse تتشارك في فلسفة تصميم واحدة، مما يزيد من الاتساق بين الدالّات، ويجعل تعلم كل دالّة أو حزمة جديدة واستخدامها أسهل قليلاً. ليس من الممكن استخدام tidyverse دون استخدام base R، لذا فقد علمناك بالفعل الكثير من دالّات base R: بداية من library() لتحميل الحزم، إلى sum() و mean() للملخصات الرقمية، إلى أنواع البيانات مثل العوامل (factor) والتواريخ (date) و POSIXct، وبالطبع جميع المعاملات الأساسية مثل + و - و / و * و | و & و !. ما لم نركز عليه حتى الآن هو مسارات العمل (workflows) الخاصة بـ base R، لذا سنقوم بتسليط الضوء على بعض منها في هذا الفصل.

بعد قراءة هذا الكتاب، ستتعلم أساليب أخرى لنفس المشكلات باستخدام base R و data.table وحزم أخرى. ستصادف بلا شك هذه الأساليب الأخرى عندما تبدأ في قراءة كود R المكتوب بواسطة الآخرين، خاصةً إذا كنت تستخدم موقع Stack Overflow. من المقبول تماماً (100%) كتابة كود يستخدم مزيجاً من الأساليب، ولا تدع أحداً يخبرك بغير ذلك!

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

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

يركز هذا الفصل على base R لذا فإنه لا يتطلب أي متطلبات أساسية حقيقية، ولكننا سنقوم بتحميل حزمة tidyverse لشرح بعض الاختلافات.

27.2 تحديد عناصر متعددة باستخدام [

تُستخدم [ لاستخراج المكونات الفرعية من المتجهات (vectors) وإطارات البيانات (data frames)، ويتم استدعاؤها مثل x[i] أو x[i, j]. في هذا القسم، سنعرفك على قوة المعامل [، حيث سنبين لك أولاً كيفية استخدامه مع المتجهات، ثم كيف تمتد نفس المبادئ بطريقة مباشرة إلى الهياكل ثنائية الأبعاد (2d) مثل إطارات البيانات. سنساعدك بعد ذلك على ترسيخ تلك المعرفة من خلال إظهار كيف أن أفعال dplyr المختلفة هي حالات خاصة من [.

27.2.1 اقتطاع المتجهات (Subsetting vectors)

هناك خمسة أنواع رئيسية من المكونات التي يمكنك اقتطاع المتجه باستخدامها، أي التي يمكن أن تحل محل i في x[i]:

  1. متجه أعداد صحيحة موجبة (A vector of positive integers). يُبقي الاقتطاع باستخدام الأعداد الصحيحة الموجبة على العناصر الموجودة في تلك المواضع:
x <- c("one", "two", "three", "four", "five")
x[c(3, 2, 5)]
#> [1] "three" "two"   "five"

من خلال تكرار الموضع، يمكنك في الواقع الحصول على مخرجات أطول من المدخلات، مما يجعل مصطلح “الاقتطاع” (subsetting) غير دقيق تماماً.

x[c(1, 1, 5, 5, 5, 2)]
#> [1] "one"  "one"  "five" "five" "five" "two"
  1. متجه أعداد صحيحة سالبة (A vector of negative integers). تستبعد القيم السالبة العناصر الموجودة في المواضع المحددة:
x[c(-1, -3, -5)]
#> [1] "two"  "four"
  1. متجه منطقي (A logical vector). يُبقي الاقتطاع باستخدام متجه منطقي على جميع القيم التي تقابل القيمة TRUE. ويكون هذا مفيداً للغاية عند اقترانه بدوال المقارنة.
x <- c(10, 3, NA, 5, 8, 1, NA)

# كافة القيم غير المفقودة للمتغير <bdi>`x`</bdi>
x[!is.na(x)]
#> [1] 10  3  5  8  1

# كافة القيم الزوجية (أو المفقودة!) للمتغير <bdi>`x`</bdi>
x[x %% 2 == 0]
#> [1] 10 NA  8 NA

على عكس الدالة filter()، سيتم تضمين أدلة الفهرسة التي تحمل القيمة NA في المخرجات كـ NA.

  1. متجه رمزي/نصي (A character vector). إذا كان لديك متجه مسمى، يمكنك اقتطاعه باستخدام متجه رمزي:
x <- c(abc = 1, def = 2, xyz = 5)
x[c("xyz", "def")]
#> xyz def 
#>   5   2

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

  1. لا شيء (Nothing). النوع الأخير من الاقتطاع هو عدم إدخال أي شيء x[]، والذي يُرجع x كاملاً. وهذا ليس مفيداً لاقتطاع المتجهات، ولكنه مفيد - كما سنرى قريباً - عند اقتطاع الهياكل ثنائية الأبعاد مثل الـ tibbles.

27.2.2 اقتطاع إطارات البيانات (Subsetting data frames)

هناك عدة طرق مختلفة يمكنك من خلالها استخدام [ مع إطار البيانات1، ولكن الطريقة الأكثر أهمية هي تحديد الصفوف والأعمدة بشكل مستقل باستخدام df[rows, cols]. هنا تكون rows و cols عبارة عن متجهات كما هو موضح أعلاه. على سبيل المثال، تحدد df[rows, ] و df[, cols] الصفوف فقط أو الأعمدة فقط، باستخدام الاقتطاع الفارغ للحفاظ على البعد الآخر.

إليك بضعة أمثلة:

df <- tibble(
  x = 1:3, 
  y = c("a", "e", "f"), 
  z = runif(3)
)

# تحديد الصف الأول والعمود الثاني
df[1, 2]
#> # A tibble: 1 × 1
#>   y    
#>   <chr>
#> 1 a

# اختيار كافة الصفوف والعمودين <bdi>`x`</bdi> و <bdi>`y`</bdi>
df[, c("x" , "y")]
#> # A tibble: 3 × 2
#>       x y    
#>   <int> <chr>
#> 1     1 a    
#> 2     2 e    
#> 3     3 f

# اختيار الصفوف التي تكون فيها قيمة <bdi>`x`</bdi> أكبر من 1 وكافة الأوردة/الأعمدة
df[df$x > 1, ]
#> # A tibble: 2 × 3
#>       x y         z
#>   <int> <chr> <dbl>
#> 1     2 e     0.834
#> 2     3 f     0.601

سنعود إلى المعامل $ قريباً، ولكن يجب أن تكون قادراً على التخمين مما تفعل df$x من السياق: فهي تستخرج المتغير x من df. نحتاج إلى استخدامها هنا لأن [ لا تستخدم التقييم المنظم (tidy evaluation)، لذا يلزم أن تكون صريحاً بشأن مصدر المتغير x.

هناك فرق مهم بين الـ tibbles وإطارات البيانات العادية (data frames) عندما يتعلق الأمر بـ [. في هذا الكتاب، استخدمنا بشكل أساسي tibbles، والتي تعتبر إطارات بيانات، ولكنها تضبط بعض السلوكيات لتجعل حياتك أسهل قليلاً. في معظم الأماكن، يمكنك استخدام “tibble” و “data frame” بالتبادل، لذلك عندما نريد توجيه انتباه خاص إلى إطار البيانات المدمج في R، سنكتب data.frame. إذا كان df عبارة عن data.frame، فإن df[, cols] ستعيد متجهاً إذا حدد cols عموداً واحداً، وإطار بيانات إذا حدد أكثر من عمود واحد. إذا كان df عبارة عن tibble، فإن [ ستعيد دائماً tibble.

df1 <- data.frame(x = 1:3)
df1[, "x"]
#> [1] 1 2 3

df2 <- tibble(x = 1:3)
df2[, "x"]
#> # A tibble: 3 × 1
#>       x
#>   <int>
#> 1     1
#> 2     2
#> 3     3

إحدى الطرق لتجنب هذا الغموض مع data.frame هي تحديد drop = FALSE صراحةً:

df1[, "x" , drop = FALSE]
#>   x
#> 1 1
#> 2 2
#> 3 3

27.2.3 مكافئات dplyr

تعتبر العديد من أفعال dplyr حالات خاصة من [:

  • filter() تكافئ اقتطاع الصفوف باستخدام متجه منطقي، مع الحرص على استبعاد القيم المفقودة:
df <- tibble(
  x = c(2, 3, 1, 1, NA), 
  y = letters[1:5], 
  z = runif(5)
)
df |> filter(x > 1)

# نفس النتيجة
df[!is.na(df$x) & df$x > 1, ]

تقنية أخرى شائعة في الممارسات البرمجية هي استخدام which() لتأثيرها الجانبي المتمثل في إسقاط القيم المفقودة: df[which(df$x > 1), ].

  • arrange() تكافئ اقتطاع الصفوف باستخدام متجه أعداد صحيحة، وعادة ما يتم إنشاؤه باستخدام order():
df |> arrange(x, y)

# نفس النتيجة
df[order(df$x, df$y), ]

يمكنك استخدام order(decreasing = TRUE) لفرز جميع الأعمدة بترتيب تنازلي أو -rank(col) لفرز الأعمدة بشكل تنازلي فردياً.

  • تتشابه كل من select() و relocate() مع اقتطاع الأعمدة باستخدام متجه نصي:
df |> select(x, z)

# نفس النتيجة 
df[, c("x", "z")]

توفّر (base R) أيضاً دالّة تجمع بين ميزات filter() و select()2 تُسمى subset():

df |> 
  filter(x > 1) |> 
  select(y, z)
#> # A tibble: 2 × 2
#>   y           z
#>   <chr>   <dbl>
#> 1 a     0.157  
#> 2 b     0.00740
# نفس النتيجة
df |> subset(x > 1, c(y, z))

كانت هذه الدالّة هي مصدر إلهام لكثير من بناء الجملة (syntax) في حزمة dplyr.

27.2.4 تمارين

  1. أنشئ دالّات تأخذ متجهاً كمدخلات وترجع:

    أ. العناصر الموجودة في المواضع ذات الأرقام الزوجية.
    ب. كل عنصر باستثناء القيمة الأخيرة.
    ج. القيم الزوجية فقط (ودون أي قيم مفقودة).

  2. لماذا لا تُعد x[-which(x > 0)] هي نفسها x[x <= 0]؟ اقرأ التوثيق الخاص بـ which() وقم ببعض التجارب لمعرفة السبب.

27.3 تحديد عنصر واحد باستخدام $ و [[

يقترن المعامل [ (الذي يحدد العديد من العناصر) بـ [[ و $ (اللذين يستخرجان عنصراً واحداً). في هذا القسم، سنوضح لك كيفية استخدام [[ و $ لاستخراج الأعمدة من إطارات البيانات، ونناقش بعض الاختلافات الإضافية بين data.frame و tibbles، ونؤكد على بعض الاختلافات المهمة بين [ و [[ عند استخدامهما مع القوائم (lists).

27.3.1 إطارات البيانات (Data frames)

يمكن استخدام [[ و $ لاستخراج الأعمدة من إطار البيانات. تستطيع [[ الوصول إما حسب الموضع أو حسب الاسم، بينما تُخصص $ للوصول حسب الاسم فقط:

tb <- tibble(
  x = 1:4,
  y = c(10, 4, 1, 21)
)

# حسب الموضع
tb[[1]]
#> [1] 1 2 3 4

# حسب الاسم
tb[["x"]]
#> [1] 1 2 3 4
tb$x
#> [1] 1 2 3 4

يمكن استخدامها أيضاً لإنشاء أعمدة جديدة، وهو المكافئ في (base R) للدالّة mutate():

tb$z <- tb$x + tb$y
tb
#> # A tibble: 4 × 3
#>       x     y     z
#>   <int> <dbl> <dbl>
#> 1     1    10    11
#> 2     2     4     6
#> 3     3     1     4
#> 4     4    21    25

هناك عدة أساليب أخرى في (base R) لإنشاء أعمدة جديدة بما في ذلك transform() و with() و within(). جمع هادلي (Hadley) بعض الأمثلة على https://gist.github.com/hadley/1986a273e384fb2d4d752c18ed71bedf.

يكون استخدام $ مباشرة مريحاً عند إجراء ملخصات سريعة. على سبيل المثال، إذا أردت فقط العثور على حجم أكبر ألماسة أو القيم المحتملة للقطع cut، فلا داعي لاستخدام summarize():

max(diamonds$carat)
#> [1] 5.01

levels(diamonds$cut)
#> [1] "Fair"      "Good"      "Very Good" "Premium"   "Ideal"

توفّر dplyr أيضاً مكافئاً لـ [[/$ لم نذكره في الفصل 3: وهي الدالّة pull(). تأخذ pull() إما اسم متغير أو موضع متغير وترجع ذلك العمود فقط. هذا يعني أنه يمكننا إعادة كتابة الكود أعلاه باستخدام الأنبوب (pipe):

diamonds |> pull(carat) |> max()
#> [1] 5.01

diamonds |> pull(cut) |> levels()
#> [1] "Fair"      "Good"      "Very Good" "Premium"   "Ideal"

27.3.2 الـ Tibbles

هناك بضعة فروق مهمة بين الـ tibbles وإطارات البيانات العادية data.frame عندما يتعلق الأمر بالمعامل $. تطابق إطارات البيانات البادئة لأي أسماء متغيرات (ما يُعرف بـ النقش الجزئي - partial matching) ولا تشتكي إذا كان العمود غير موجود:

df <- data.frame(x1 = 1)
df$x
#> [1] 1
df$z
#> NULL

أما الـ tibbles فهي أكثر صرامة: فهي تطابق أسماء المتغيرات بالضبط دائماً، وستقوم بإصدار تحذير إذا كان العمود الذي تحاول الوصول إليه غير موجود:

tb <- tibble(x1 = 1)

tb$x
#> Warning: Unknown or uninitialised column: `x`.
#> NULL
tb$z
#> Warning: Unknown or uninitialised column: `z`.
#> NULL

لهذا السبب نمزح أحياناً بأن الـ tibbles كسولة ونكدية: فهي تفعل القليل وتشتكي كثيراً.

27.3.3 القوائم (Lists)

يعد المعاملان [[ و $ مهمين جداً أيضاً عند التعامل مع القوائم، ومن المهم فهم كيف يختلفان عن المعامل [. لنوضح الفروق باستخدام قائمة تسمى l:

l <- list(
  a = 1:3, 
  b = "a string", 
  c = pi, 
  d = list(-1, -5)
)
  • يستخرج المعامل [ قائمة فرعية. لا يهم عدد العناصر التي تستخرجها، ستكون النتيجة دائماً قائمة.
str(l[1:2])
#> List of 2
#>  $ a: int [1:3] 1 2 3
#>  $ b: chr "a string"

str(l[1])
#> List of 1
#>  $ a: int [1:3] 1 2 3

str(l[4])
#> List of 1
#>  $ d:List of 2
#>   ..$ : num -1
#>   ..$ : num -5

كما هو الحال مع المتجهات، يمكنك اقتطاع القائمة باستخدام متجه منطقي أو عددي صحيح أو نصي.

  • يستخرج المعاملان [[ و $ مكوناً واحداً فقط من القائمة. يقومان بإزالة مستوى واحد من الهيراركية (التسلسل الهرمي) للقائمة.
str(l[[1]])
#>  int [1:3] 1 2 3

str(l[[4]])
#> List of 2
#>  $ : num -1
#>  $ : num -5

str(l$a)
#>  int [1:3] 1 2 3

يعتبر الفرق بين [ و [[ مهماً بشكل خاص للقوائم لأن [[ تغوص إلى داخل القائمة نفسها، بينما تعيد [ قائمة جديدة أصغر حجمًا. ولمساعدتك على تذكر الفرق، ألقِ نظرة على ملاحة الفلفل غير العادية الموضحة في الشكل 27.1. إذا كانت ملاحة الفلفل هذه هي قائمتك pepper، فإن pepper[1] هي ملاحة فلفل تحتوي على كيس فلفل واحد. وستبدو pepper[2] مشابهة، ولكنها ستحتوي على الكيس الثاني. أما pepper[1:2] فستكون ملاحة فلفل تحتوي على كيسين من الفلفل. بينما تقتطع pepper[[1]] كيس الفلفل نفسه من الداخل.

ثلاث صور. على اليسار صورة لملاحة فلفل زجاجية. وبدلاً من أن تحتوي الملاحة على فلفل سائب، تحتوي على كيس فلفل واحد. وفي الوسط صورة لكيس فلفل واحد. وعلى اليمين صورة لمحتويات كيس الفلفل.
الشكل 27.1: (يسار) ملاحة فلفل وجدها هادلي في غرفته بالفندق. (وسط) pepper[1]. (يمين) pepper[[1]]

ينطبق هذا المبدأ نفسه عندما تستخدم المعامل أحادي البعد [ مع إطار بيانات: تعيد df["x"] إطار بيانات من عمود واحد، بينما تعيد df[["x"]] متجهاً.

27.3.4 تمارين

  1. ماذا يحدث عندما تستخدم [[ مع عدد صحيح موجب أكبر من طول المتجه؟ ماذا يحدث عندما تقوم بالاقتطاع باستخدام اسم غير موجود؟

  2. ماذا ستكون نتيجة pepper[[1]][1]؟ وماذا عن pepper[[1]][[1]]؟

27.4 عائلة apply

في الفصل 26، تعلمت تقنيات tidyverse للتكرار مثل dplyr::across() وعائلة دالّات map. في هذا القسم، ستتعلم عن مكافئاتها في base R، والمعروفة بـ عائلة apply. في هذا السياق، تعد apply و map مترادفين لأن هناك طريقة أخرى للقول “تطبيق (map) دالّة على كل عنصر من عناصر المتجه” وهي “تطبيق (apply) دالّة على كل عنصر من عناصر المتجه”. سنعطيك هنا نظرة عامة سريعة على هذه العائلة حتى تتمكن من التعرف عليها في الأكواد البرمجية.

أهم عضو في هذه العائلة هو lapply()، وهي شبيهة جداً بـ purrr::map()3. في الواقع، ولأننا لم نستخدم أيًا من الميزات الأكثر تقدمًا لـ map()، يمكنك استبدال كل استدعاء الفصل 26 لـ map() بـ lapply().

لا يوجد مكافئ دقيق في base R لـ across()، ولكن يمكنك الاقتراب منها باستخدام [ مع lapply(). يعمل هذا لأن إطارات البيانات هي في الأصل قوائم من الأعمدة، لذا فإن استدعاء lapply() على إطار بيانات يطبق الدالّة على كل عمود.

df <- tibble(a = 1, b = 2, c = "a", d = "b", e = 4)

# أولاً نحدد الأعمدة الرقمية
num_cols <- sapply(df, is.numeric)
num_cols
#>     a     b     c     d     e 
#>  TRUE  TRUE FALSE FALSE  TRUE

# ثم نقوم بتحويل كل عمود باستخدام lapply() واستبدال القيم الأصلية
df[, num_cols] <- lapply(df[, num_cols, drop = FALSE], \(x) x * 2)
df
#> # A tibble: 1 × 5
#>       a     b c     d         e
#>   <dbl> <dbl> <chr> <chr> <dbl>
#> 1     2     4 a     b         8

يستخدم الكود أعلاه دالّة جديدة وهي sapply(). إنها تشبه lapply() لكنها تحاول دائماً تبسيط النتيجة، ومن هنا جاء حرف s في اسمها (simplify)، حيث تنتج هنا متجهاً منطقياً بدلاً من قائمة. لا نوصي باستخدامها في البرمجة لأن التبسيط قد يفشل ويعطيك نوعاً غير متوقع، ولكنها ممتازة للاستخدام التفاعلي عادةً. تحتوي حزمة purrr على دالّة مماثلة تُسمى map_vec() والتي لم نذكرها في الفصل 26.

توفر (base R) نسخة أكثر صرامة من sapply() تُسمى vapply()، وهي اختصار لـ vector apply. تأخذ وسيطاً إضافياً يحدد النوع المتوقع للنتيجة، مما يضمن حدوث التبسيط بنفس الطريقة بغض النظر عن المدخلات. على سبيل المثال، يمكننا استبدال استدعاء sapply() أعلاه بـ vapply() هذا، حيث نحدد أننا نتوقع أن تعيد is.numeric() متجهاً منطقياً بطول 1:

vapply(df, is.numeric, logical(1))
#>     a     b     c     d     e 
#>  TRUE  TRUE FALSE FALSE  TRUE

يعتبر التمييز بين sapply() و vapply() مهماً جداً عندما يكونان داخل دالّة (لأنه يصنع فارقاً كبيراً في متانة الدالّة تجاه المدخلات غير العادية)، ولكنه لا يهم عادة في تحليل البيانات اليومي.

عضو مهم آخر في عائلة apply هو tapply() والتي تحسب ملخصاً مجوّفاً (grouped summary) واحداً:

diamonds |> 
  group_by(cut) |> 
  summarize(price = mean(price))
#> # A tibble: 5 × 2
#>   cut       price
#>   <ord>     <dbl>
#> 1 Fair      4359.
#> 2 Good      3929.
#> 3 Very Good 3982.
#> 4 Premium   4584.
#> 5 Ideal     3458.

tapply(diamonds$price, diamonds$cut, mean)
#>      Fair      Good Very Good   Premium     Ideal 
#>  4358.758  3928.864  3981.760  4584.258  3457.542

لسوء الحظ، تُرجع الدالة tapply() نتائجها في متجه مسمى (Named vector)، وهو ما يتطلب بعض الجهد والالتفاف البرمجي إذا كنت تريد تجميع ملخصات متعددة ومتغيرات تجميع في إطار بيانات (Data frame) — (من الممكن بالتأكيد عدم القيام بذلك والعمل فقط مع متجهات طليقة غير مرتبطة، ولكن بناءً على خبرتنا، فإن ذلك يؤجل العمل الفعلي فحسب). إذا كنت تريد أن ترى كيف يمكنك استخدام tapply() أو أساليب آر الأساسية (Base R) الأخرى لإجراء ملخصات مجمعة إضافية، فقد جمع هادلي (Hadley) بضع تقنيات في موجز كود (Gist).

العضو الأخير في عائلة apply هو الدالّة الاسمية apply()، والتي تعمل مع المصفوفات (matrices) والمصفوفات متعددة الأبعاد (arrays). على وجه الخصوص، احذر من apply(df, 2, something)، لأنها طريقة بطيئة وقد تكون خطيرة لأداء lapply(df, something). نادراً ما يظهر هذا في علم البيانات لأننا نعمل عادة مع إطارات البيانات وليس المصفوفات.

27.5 حلقات التكرار for

تعتبر حلقات التكرار for اللبنة الأساسية للتكرار التي تستخدمها عائلتا apply و map في الأصل. تُعد حلقات for أدوات قوية وعامة من المهم تعلمها عندما تصبح برمجيًا أكثر خبرة في R. يبدو الهيكل الأساسي لحلقة for كما يلي:

for (element in vector) {
  # قم بعمل شيء ما مع العنصر
}

الاستخدام الأكثر إيجازاً لحلقات for هو تحقيق نفس تأثير walk(): استدعاء دالّة ذات تأثير جانبي (side-effect) على كل عنصر من عناصر القائمة. على سبيل المثال، في قسم 26.4.1 بدلاً من استخدام walk():

paths |> walk(append_file)

كان بإمكاننا استخدام حلقة for:

for (path in paths) {
  append_file(path)
}

تصبح الأمور أكثر تعقيداً قليلاً إذا أردت حفظ مخرجات حلقة for، على سبيل المثال قراءة جميع ملفات Excel في دليل معين كما فعلنا في الفصل 26:

paths <- dir("data/gapminder", pattern = "\\.xlsx$", full.names = TRUE)
files <- map(paths, readxl::read_excel)

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

files <- vector("list", length(paths))

ثم بدلاً من التكرار عبر عناصر paths نفسها، سنكرر عبر فهارسها (indices)، باستخدام seq_along() لإنشاء فهرس واحد لكل عنصر من عناصر paths:

seq_along(paths)
#>  [1]  1  2  3  4  5  6  7  8  9 10 11 12

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

for (i in seq_along(paths)) {
  files[[i]] <- readxl::read_excel(paths[[i]])
}

ولدمج قائمة الـ tibbles في tibble واحد، يمكنك استخدام do.call() + rbind():

do.call(rbind, files)
#> # A tibble: 1,704 × 5
#>   country     continent lifeExp      pop gdpPercap
#>   <chr>       <chr>       <dbl>    <dbl>     <dbl>
#> 1 Afghanistan Asia         28.8  8425333      779.
#> 2 Albania     Europe       55.2  1282697     1601.
#> 3 Algeria     Africa       43.1  9279525     2449.
#> 4 Angola      Africa       30.0  4232095     3521.
#> 5 Argentina   Americas     62.5 17876956     5911.
#> 6 Australia   Oceania      69.1  8691212    10040.
#> # ℹ 1,698 more rows

بدلاً من إنشاء قائمة وحفظ النتائج خطوة بخطوة، هناك أسلوب أبسط وهو بناء إطار البيانات جزءاً فجزء:

out <- NULL
for (path in paths) {
  out <- rbind(out, readxl::read_excel(path))
}

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

27.6 الرسوم البيانية

يفضل العديد من مستخدمي R الذين لا يستخدمون tidyverse حزمة ggplot2 للرسم البياني بسبب ميزاتها المفيدة مثل الإعدادات الافتراضية الذكية، والمفاتيح التوضيحية الأوتوماتيكية، والمظهر الحديث. ومع ذلك، لا تزال دالّات الرسم البياني في base R مفيدة لأنها موجزة للغاية — فهي تتطلب القليل جداً من الكتابة لإنشاء رسم بياني استكشافي أساسي.

هناك نوعان رئيسيان من الرسوم البيانية الأساسية التي ستراها في المشاريع: مخططات التشتت (scatterplots) والمدرجات التكرارية (histograms)، وتنتجان باستخدام plot() و hist() على التوالي. إليك مثالاً سريعاً من مجموعة بيانات الألماس (diamonds):

# يسار
hist(diamonds$carat)

# يمين
plot(diamonds$carat, diamonds$price)

على اليسار، مدرج تكراري لعيارات الألماس، يتراوح من 0 إلى 5 قيراط. التوزيع أحادي المنوال ومائل نحو اليمين. على اليمين، مخطط نقطي للسعر مقابل القيراط، يظهر علاقة إيجابية تتسع مع زيادة كل من السعر والقيراط. يظهر المخطط النقطي عدداً قليلاً جداً من الألماس الأكبر من 3 قيراط مقارنة بالألماس بين 0 إلى 3 قيراط.

على اليسار، مدرج تكراري لعيارات الألماس، يتراوح من 0 إلى 5 قيراط. التوزيع أحادي المنوال ومائل نحو اليمين. على اليمين، مخطط نقطي للسعر مقابل القيراط، يظهر علاقة إيجابية تتسع مع زيادة كل من السعر والقيراط. يظهر المخطط النقطي عدداً قليلاً جداً من الألماس الأكبر من 3 قيراط مقارنة بالألماس بين 0 إلى 3 قيراط.

لاحظ أن دالّات الرسم البياني في base R تعمل مع المتجهات، لذا يلزمك استخراج الأعمدة من إطار البيانات باستخدام $ أو تقنية أخرى.

27.7 ملخص

في هذا الفصل، قدمنا لك مجموعة مختارة من دالّات (base R) المفيدة للاقتطاع والتكرار. مقارنة بالأساليب المناقشة في أماكن أخرى من الكتاب، تميل هذه الدالّات إلى امتلاك طابع “المتجهات” أكثر من طابع “إطارات البيانات” لأن دالّات (base R) تميل إلى قبول المتجهات الفردية، بدلاً من إطار بيانات وتحديد معايير الأعمدة. غالباً ما يجعل هذا الحياة أسهل عند البرمجة، وبالتالي يصبح أكثر أهمية كلما كتبت المزيد من الدالّات وبدأت في إنشاء حزمك الخاصة.

يختتم هذا الفصل قسم البرمجة في هذا الكتاب. لقد حققت بداية قوية في رحلتك لكي لا تكون مجرد عالم بيانات يستخدم R، بل عالم بيانات يمكنه البرمجة بلغة R. نأمل أن تكون هذه الفصول قد أثارت شغفك بالبرمجة وأنك تتطلع لتعلم المزيد خارج هذا الكتاب.


  1. اقرأ https://adv-r.hadley.nz/subsetting.html#subset-multiple لرؤية كيف يمكنك أيضاً اقتطاع إطار البيانات كما لو كان كائناً أحادي البعد وكيف يمكنك اقتطاعه باستخدام مصفوفة.↩︎

  2. لكنها لا تتعامل مع إطارات البيانات المجمعة بشكل مختلف ولا تدعم دالّات التحديد المساعدة مثل starts_with().↩︎

  3. إنها تفتقر فقط إلى الميزات المريحة مثل أشرطة التقدم (progress bars) والإبلاغ عن العنصر الذي تسبب في المشكلة في حالة حدوث خطأ.↩︎