26 التكرار (Iteration)
26.1 مقدمة
في هذا الفصل، ستتعلم أدوات التكرار (Iteration)، وهي تنفيذ نفس الإجراء بشكل متكرر على كائنات مختلفة. غالباً ما يبدو التكرار في لغة R مختلفاً تماماً عن لغات البرمجة الأخرى لأن الكثير منه يكون ضمنياً ونحصل عليه تلقائياً. على سبيل المثال، إذا أردت مضاعفة المتجه العددي x في R، يمكنك فقط كتابة 2 * x. بينما في معظم اللغات الأخرى، ستحتاج إلى مضاعفة كل عنصر من عناصر x صراحةً باستخدام نوع ما من حلقات التكرار (for loops).
لقد قدم لك هذا الكتاب بالفعل مجموعة صغيرة ولكنها قوية من الأدوات التي تؤدي نفس الإجراء لـ “أشياء” متعددة:
- تُنشئ
facet_wrap()وfacet_grid()رسماً بيانياً لكل مجموعة فرعية. - تحسب
group_by()بالإضافة إلىsummarize()الإحصاءات الملخصة لكل مجموعة فرعية. - تُنشئ
unnest_wider()وunnest_longer()صفوفاً وأعمدة جديدة لكل عنصر في عمود القائمة (list-column).
حان الوقت الآن لتعلم بعض الأدوات الأكثر عمومية، والتي تُسمى غالباً أدوات البرمجة الوظيفية (Functional Programming) لأنها مبنية حول دالّات تأخذ دالّات أخرى كمدخلات. قد يتجه تعلم البرمجة الوظيفية بسهولة نحو التجريد، ولكن في هذا الفصل سنحافظ على الطرح التطبيقي الملموس من خلال التركيز على ثلاث مهام شائعة: تعديل أعمدة متعددة، قراءة ملفات متعددة، وحفظ كائنات متعددة.
26.1.1 المتطلبات المسبقة
سنركز في هذا الفصل على الأدوات التي توفرها حزمتا dplyr و purrr، وكلاهما من الأعضاء الأساسيين في بيئة tidyverse. لقد رأيت dplyr من قبل، ولكن purrr تُعد حزمة جديدة. سنستخدم فقط دالّتين من حزمة purrr في هذا الفصل، ولكنها حزمة رائعة للاستكشاف كلما حسّنت مهاراتك البرمجية.
26.2 تعديل أعمدة متعددة
تخيل أن لديك هذا الجدول البسيط (tibble) وتريد حساب عدد الملاحظات وإيجاد الوسيط (median) لكل عمود.
يمكنك القيام بذلك عبر النسخ واللصق:
هذا يخالف قاعدتنا الذهبية بعدم النسخ واللصق أكثر من مرتين، ويمكنك أن تتخيل أن هذا سيكون مملاً للغاية إذا كان لديك عشرات أو حتى مئات الأعمدة. بدلاً من ذلك، يمكنك استخدام across():
تحتوي across() على ثلاث وسائط (arguments) مهمة بشكل خاص، والتي سنناقشها بالتفصيل في الأقسام التالية. ستستخدم أول وسيطين في كل مرة تستخدم فيها across(): الوسيط الأول، .cols، يحدد الأعمدة التي تريد التكرار عليها، والوسيط الثاني، .fns، يحدد ما يجب فعله بكل عمود. يمكنك استخدام الوسيط .names عندما تحتاج إلى تحكم إضافي في أسماء أعمدة المخرجات، وهو أمر مهم بشكل خاص عند استخدام across() مع mutate(). سنناقش أيضاً متغيرين مهمين، if_any() و if_all()، اللذين يعملان مع filter().
26.2.1 تحديد الأعمدة باستخدام .cols
الوسيط الأول لـ across()، وهو .cols، يحدد الأعمدة المراد تحويلها. يستخدم هذا نفس المواصفات مثل select()، قسم 3.3.2 ، لذلك يمكنك استخدام دالّات مثل starts_with() و ends_with() لتحديد الأعمدة بناءً على أسمائها.
هناك تقنيتان إضافيتان للتحديد مفيدتان بشكل خاص لـ across(): وهما everything() و where(). تعتبر everything() مباشرة: حيث تحدد كل عمود (ليس عمود تجميع):
set.seed(1014)
df <- tibble(
grp = sample(2, 10, replace = TRUE),
a = rnorm(10),
b = rnorm(10),
c = rnorm(10),
d = rnorm(10)
)
df |>
group_by(grp) |>
summarize(across(everything(), median))
#> # A tibble: 2 × 5
#> grp a b c d
#> <int> <dbl> <dbl> <dbl> <dbl>
#> 1 1 -0.244 -0.522 -0.0974 -0.251
#> 2 2 -0.247 0.468 0.112 0.0700لاحظ أن أعمدة التجميع (grp هنا) لا يتم تضمينها في across()، لأنه يتم الحفاظ عليها تلقائياً بواسطة summarize().
تسمح لك where() بتحديد الأعمدة بناءً على نوعها:
-
where(is.numeric)تحدد جميع الأعمدة العددية. -
where(is.character)تحدد جميع الأعمدة النصية. -
where(is.Date)تحدد جميع أعمدة التاريخ. -
where(is.POSIXct)تحدد جميع أعمدة التاريخ والوقت. -
where(is.logical)تحدد جميع الأعمدة المنطقية.
تماماً مثل المحددات الأخرى، يمكنك دمج هذه الوسائط مع الجبر البولياني. على سبيل المثال، !where(is.numeric) تحدد جميع الأعمدة غير العددية، و starts_with("a") & where(is.logical) تحدد جميع الأعمدة المنطقية التي تبدأ أسمائها بـ “a”.
26.2.2 استدعاء دالّة واحدة
يُحدد المعامل (Argument) الثاني للـدالة across() كيفية تحويل كل عمود. وفي الحالات البسيطة، كما هو موضح أعلاه، سيكون هذا المعامل عبارة عن دالة واحدة موجودة بالفعل. وهذه ميزة خاصة ومميزة جداً في لغة R: حيث نقوم بتمرير دالة واحدة (مثل median، و mean، و str_flatten…) إلى دالة أخرى (across). وتُعد هذه إحدى الميزات التي تجعل R لغة برمجة وظيفية (Functional programming language).
من المهم ملاحظة أننا نمرر هذه الدالّة إلى across() حتى تتمكن across() من استدعائها؛ نحن لا نستدعيها بأنفسنا. هذا يعني أن اسم الدالّة لا ينبغي أبداً أن يتبعه قوسان (). إذا نسيت، فستحصل على خطأ:
df |>
group_by(grp) |>
summarize(across(everything(), median()))
#> Error in `summarize()`:
#> ℹ In argument: `across(everything(), median())`.
#> Caused by error in `median.default()`:
#> ! argument "x" is missing, with no defaultينشأ هذا الخطأ لأنك تستدعي الدالّة بدون مدخلات، مثل:
median()
#> Error in `median.default()`:
#> ! argument "x" is missing, with no default26.2.3 استدعاء دالّات متعددة
في الحالات الأكثر تعقيداً، قد ترغب في تقديم وسائط إضافية أو إجراء تحويلات متعددة. دعنا نوضح هذه المشكلة بمثال بسيط: ماذا يحدث إذا كان لدينا بعض القيم المفقودة في بياناتنا؟ تقوم median() بتمرير هذه القيم المفقودة، مما يعطينا مخرجات غير مثالية:
set.seed(1014)
rnorm_na <- function(n, n_na, mean = 0, sd = 1) {
sample(c(rnorm(n - n_na, mean = mean, sd = sd), rep(NA, n_na)))
}
df_miss <- tibble(
a = rnorm_na(5, 1),
b = rnorm_na(5, 1),
c = rnorm_na(5, 2),
d = rnorm(5)
)
df_miss |>
summarize(
across(a:d, median),
n = n()
)
#> # A tibble: 1 × 5
#> a b c d n
#> <dbl> <dbl> <dbl> <dbl> <int>
#> 1 NA NA NA 0.413 5سيكون من الجيد لو تمكنا من تمرير na.rm = TRUE إلى median() لإزالة هذه القيم المفقودة. للقيام بذلك، بدلاً من استدعاء median() مباشرةً، نحتاج إلى إنشاء دالّة جديدة تستدعي median() مع الوسائط المطلوبة:
هذا الأسلوب مُسهَب نوعاً ما، لذا تُوفر لغة R اختصاراً مفيداً: فبالنسبة لهذا النوع من الدوال المؤقتة أو المجهولة (Anonymous)1، يمكنك استبدال الكلمة المفتاحية function بالرمز \2:
في كلتا الحالتين، تتوسع across() فعلياً إلى الكود التالي:
عندما نزيل القيم المفقودة من median()، سيكون من الجيد معرفة عدد القيم التي تم إزالتها بالضبط. يمكننا معرفة ذلك من خلال تقديم دالّتين إلى across(): إحداهما لحساب الوسيط والأخرى لحساب عدد القيم المفقودة. يمكنك تقديم دالّات متعددة باستخدام قائمة مسماة (named list) في الوسيط .fns:
df_miss |>
summarize(
across(a:d, list(
median = \(x) median(x, na.rm = TRUE),
n_miss = \(x) sum(is.na(x))
)),
n = n()
)
#> # A tibble: 1 × 9
#> a_median a_n_miss b_median b_n_miss c_median c_n_miss d_median d_n_miss
#> <dbl> <int> <dbl> <int> <dbl> <int> <dbl> <int>
#> 1 -0.703 1 -0.265 1 -0.522 2 0.413 0
#> # ℹ 1 more variable: n <int>إذا نظرت عن كثب، فقد تستنتج أن الأعمدة تُسمى باستخدام مواصفة glue (قسم 14.3.2) مثل {.col}_{.fn}، حيث تمثل .col اسم العمود الأصلي، وتُعبر .fn عن اسم الدالة. وهذا ليس مجرد مصادفة! فكما ستتعلم في القسم التالي، يمكنك استخدام المعامل .names لتقديم مواصفة glue الخاصة بك.
26.2.4 أسماء الأعمدة
تُسمى مخرجات الدالة ()across وفقاً للمواصفة المحددة في المعامل .names. ويمكننا تحديد مواصفتنا الخاصة إذا أردنا أن يظهر اسم الدالة أولاً3:
df_miss |>
summarize(
across(
a:d,
list(
median = \(x) median(x, na.rm = TRUE),
n_miss = \(x) sum(is.na(x))
),
.names = "{.fn}_{.col}"
),
n = n(),
)
#> # A tibble: 1 × 9
#> median_a n_miss_a median_b n_miss_b median_c n_miss_c median_d n_miss_d
#> <dbl> <int> <dbl> <int> <dbl> <int> <dbl> <int>
#> 1 -0.703 1 -0.265 1 -0.522 2 0.413 0
#> # ℹ 1 more variable: n <int>يكون الوسيط .names مهماً بشكل خاص عند استخدام across() مع mutate(). افتراضياً، يتم إعطاء مخرجات across() نفس أسماء المدخلات. هذا يعني أن across() داخل mutate() ستستبدل الأعمدة الموجودة. على سبيل المثال، نستخدم هنا coalesce() لاستبدال قيم NA بـ 0:
إذا كنت ترغب بدلاً من ذلك في إنشاء أعمدة جديدة، يمكنك استخدام الوسيط .names لإعطاء المخرجات أسماء جديدة:
df_miss |>
mutate(
across(a:d, \(x) coalesce(x, 0), .names = "{.col}_na_zero")
)
#> # A tibble: 5 × 8
#> a b c d a_na_zero b_na_zero c_na_zero d_na_zero
#> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 -0.00557 -0.283 -1.86 -0.783 -0.00557 -0.283 -1.86 -0.783
#> 2 0.255 -0.247 -0.522 -0.00289 0.255 -0.247 -0.522 -0.00289
#> 3 -1.40 -0.554 0.512 0.413 -1.40 -0.554 0.512 0.413
#> 4 -2.44 -0.244 NA 0.724 -2.44 -0.244 0 0.724
#> 5 NA NA NA 2.35 0 0 0 2.3526.2.5 التصفية (Filtering)
تعد across() خياراً رائعاً مع summarize() و mutate()، ولكن استخدامها أكثر صعوبة مع filter()، لأنك عادةً ما تجمع بين شروط متعددة إما بـ | أو &. من الواضح أن across() يمكن أن تساعد في إنشاء أعمدة منطقية متعددة، ولكن ماذا بعد ذلك؟ لذلك توفر dplyr متغيرين من across() يسمى الأول if_any() والثاني if_all():
# نفس df_miss |> filter(is.na(a) | is.na(b) | is.na(c) | is.na(d))
df_miss |> filter(if_any(a:d, is.na))
#> # A tibble: 2 × 4
#> a b c d
#> <dbl> <dbl> <dbl> <dbl>
#> 1 -2.44 -0.244 NA 0.724
#> 2 NA NA NA 2.35
# نفس df_miss |> filter(is.na(a) & is.na(b) & is.na(c) & is.na(d))
df_miss |> filter(if_all(a:d, is.na))
#> # A tibble: 0 × 4
#> # ℹ 4 variables: a <dbl>, b <dbl>, c <dbl>, d <dbl>
26.2.6 across() داخل الدالّات
تعتبر across() مفيدة بشكل خاص عند البرمجة بها لأنها تسمح لك بالعمل على أعمدة متعددة. على سبيل المثال، يستخدم جاكوب سكوت Jacob Scott هذه الدالّة المساعدة الصغيرة التي تغلف مجموعة من دالّات lubridate لتوسيع جميع أعمدة التاريخ إلى أعمدة للسنة، والشهر، واليوم:
expand_dates <- function(df) {
df |>
mutate(
across(where(is.Date), list(year = year, month = month, day = mday))
)
}
df_date <- tibble(
name = c("Amy", "Bob"),
date = ymd(c("2009-08-03", "2010-01-16"))
)
df_date |>
expand_dates()
#> # A tibble: 2 × 5
#> name date date_year date_month date_day
#> <chr> <date> <dbl> <dbl> <int>
#> 1 Amy 2009-08-03 2009 8 3
#> 2 Bob 2010-01-16 2010 1 16تسهل ()across أيضاً مدي المخرجات بأعمدة متعددة في معامل واحد لأن المعامل الأول يستخدم التحديد الأنيق (tidy-selection)؛ كل ما عليك تذكره هو احتواء (Embrace) هذا المعامل بالأقواس المزدوجة، كما ناقشنا في قسم 25.3.2. على سبيل المثال، ستلخص هذه الدالة المتوسطات للأنواع الرقمية افتراضياً. ولكن من خلال توفير المعامل الثاني، يمكنك اختيار تلخيص أعمدة محددة فقط:
summarize_means <- function(df, summary_vars = where(is.numeric)) {
df |>
summarize(
across({{ summary_vars }}, \(x) mean(x, na.rm = TRUE)),
n = n(),
.groups = "drop"
)
}
diamonds |>
group_by(cut) |>
summarize_means()
#> # A tibble: 5 × 9
#> cut carat depth table price x y z n
#> <ord> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <int>
#> 1 Fair 1.05 64.0 59.1 4359. 6.25 6.18 3.98 1610
#> 2 Good 0.849 62.4 58.7 3929. 5.84 5.85 3.64 4906
#> 3 Very Good 0.806 61.8 58.0 3982. 5.74 5.77 3.56 12082
#> 4 Premium 0.892 61.3 58.7 4584. 5.97 5.94 3.65 13791
#> 5 Ideal 0.703 61.7 56.0 3458. 5.51 5.52 3.40 21551
diamonds |>
group_by(cut) |>
summarize_means(c(carat, x:z))
#> # A tibble: 5 × 6
#> cut carat x y z n
#> <ord> <dbl> <dbl> <dbl> <dbl> <int>
#> 1 Fair 1.05 6.25 6.18 3.98 1610
#> 2 Good 0.849 5.84 5.85 3.64 4906
#> 3 Very Good 0.806 5.74 5.77 3.56 12082
#> 4 Premium 0.892 5.97 5.94 3.65 13791
#> 5 Ideal 0.703 5.51 5.52 3.40 21551
26.2.7 المقارنة مع pivot_longer()
قبل أن نواصل، يجدر بنا الإشارة إلى وجود رابط مثير للاهتمام بين الدالتين across() و pivot_longer() (قسم 5.3). في كثير من الحالات، يمكنك إجراء نفس الحسابات عن طريق إعادة تدوير البيانات (pivoting) أولاً، ثم تنفيذ العمليات حسب المجموعة بدلاً من إجرائها حسب العمود. على سبيل المثال، خذ هذا الملخص متعدد الدوال:
يمكننا حساب نفس القيم عن طريق التدوير إلى الشكل الطولي (pivot longer) ثم التلخيص:
long <- df |>
pivot_longer(a:d) |>
group_by(name) |>
summarize(
median = median(value),
mean = mean(value)
)
long
#> # A tibble: 4 × 3
#> name median mean
#> <chr> <dbl> <dbl>
#> 1 a -0.246 -0.0426
#> 2 b 0.155 -0.0656
#> 3 c 0.0480 -0.0297
#> 4 d -0.193 -0.200وإذا أردت نفس الهيكل مثل across() يمكنك التدوير مرة أخرى:
long |>
pivot_wider(
names_from = name,
values_from = c(median, mean),
names_vary = "slowest",
names_glue = "{name}_{.value}"
)
#> # A tibble: 1 × 8
#> a_median a_mean b_median b_mean c_median c_mean d_median d_mean
#> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 -0.246 -0.0426 0.155 -0.0656 0.0480 -0.0297 -0.193 -0.200هذه تقنية مفيدة يجب معرفتها لأنك قد تواجه أحياناً مشكلة لا يمكن حلها حالياً بـ across(): عندما يكون لديك مجموعات من الأعمدة تريد الحساب عليها في وقت واحد. على سبيل المثال، تخيل أن إطار البيانات لدينا يحتوي على قيم وأوزان ونريد حساب متوسط موزون:
لا توجد طريقة حالياً للقيام بذلك باستخدام الدالة across()4، ولكن الأمر أسهل نسبياً باستخدام الدالة pivot_longer():
df_long <- df_paired |>
pivot_longer(
everything(),
names_to = c("group", ".value"),
names_sep = "_"
)
df_long
#> # A tibble: 40 × 3
#> group val wts
#> <chr> <dbl> <dbl>
#> 1 a -1.40 0.290
#> 2 b -1.86 0.461
#> 3 c 0.935 0.528
#> 4 d 2.76 0.709
#> 5 a 0.255 0.678
#> 6 b -0.522 0.315
#> # ℹ 34 more rows
df_long |>
group_by(group) |>
summarize(mean = weighted.mean(val, wts))
#> # A tibble: 4 × 2
#> group mean
#> <chr> <dbl>
#> 1 a -0.207
#> 2 b -0.237
#> 3 c 0.0208
#> 4 d 0.0655إذا لزم الأمر، يمكنك إعادة تدوير هذا الشكل مجدداً pivot_wider() إلى النموذج الأصلي.
26.2.8 تمارين
-
مارس مهاراتك في استخدام
across()من خلال:حساب عدد القيم الفريدة في كل عمود من
palmerpenguins::penguins.حساب المتوسط الحسابي لكل عمود في
mtcars.تجميع
diamondsحسبcutوclarityوcolorثم حساب عدد الملاحظات ومتوسط كل عمود عددي.
ماذا يحدث إذا استخدمت قائمة من الدالّات في
across()ولكنك لم تقم بتسميتها؟ كيف ستتم تسمية المخرجات؟اضبط الدالة
expand_dates()لتقوم بحذف أعمدة التواريخ تلقائياً بعد توسيعها. هل تحتاج إلى استخدام التضمين/التطويق (embrace) مع أيٍّ من الوسائط؟-
اشرح ما تفعله كل خطوة في هذه الدالّة. ما هي الميزة الخاصة لـ
where()التي نستفيد منها؟
26.3 قراءة ملفات متعددة
في القسم السابق، تعلمت كيفية استخدام الدالة dplyr::across() لتكرار عملية التحويل على أعمدة متعددة. وفي هذا القسم، ستتعلم كيفية استخدام الدالة purrr::map() لتنفيذ إجراءٍ ما على كل ملف في مجلدٍ معين. دعنا نبدأ بتمهيد مشوّق: تخيل أن لديك مجلداً مليئاً بجدوال بيانات Excel5 تريد قراءتها. يمكنك القيام بذلك عبر النسخ واللصق:
data2019 <- readxl::read_excel("data/y2019.xlsx")
data2020 <- readxl::read_excel("data/y2020.xlsx")
data2021 <- readxl::read_excel("data/y2021.xlsx")
data2022 <- readxl::read_excel("data/y2022.xlsx")ثم استخدام dplyr::bind_rows() لدمجها جميعاً معاً:
data <- bind_rows(data2019, data2020, data2021, data2022)يمكنك أن تتخيل أن هذا سيصبح مملاً بسرعة، خاصة إذا كان لديك مئات الملفات، وليس أربعة فقط. توضح لك الأقسام التالية كيفية أتمتة هذا النوع من المهام. هناك ثلاث خطوات أساسية: استخدم list.files() لإدراج جميع الملفات في الدليل، ثم استخدم purrr::map() لقراءة كل منها في قائمة، ثم استخدم purrr::list_rbind() لدمجها في إطار بيانات واحد. سنناقش بعد ذلك كيفية التعامل مع الحالات ذات التباين المتزايد، حيث لا يمكنك القيام بنفس الشيء بالضبط لكل ملف.
26.3.1 إدراج الملفات في مجلد
كما يوحي الاسم، تقوم list.files() بإدراج الملفات الموجودة في المجلد. ستستخدم تقريباً ثلاث وسائط دائماً:
الوسيط الأول،
path، هو المجلد الذي سيتم البحث فيه.patternعبارة عن تعبير نمطي (regular expression) يُستخدم لتصفية أسماء الملفات. النمط الأكثر شيوعاً هو شيء مثل[.]xlsx$أو[.]csv$للعثور على جميع الملفات ذات الامتداد المحدد.تحدد
full.namesما إذا كان ينبغي تضمين اسم المجلد في المخرجات أم لا. ترغب دائماً تقريباً في جعل قيمتهاTRUE.
لتوضيح مثالنا الدافع بشكل ملموس، يحتوي هذا الكتاب على مجلد يضم 12 جدول بيانات Excel تحتوي على بيانات من حزمة gapminder. يوجد هذا المجلد على الرابط: https://github.com/hadley/r4ds/tree/main/data/gapminder. يحتوي كل ملف على بيانات سنة واحدة لـ 142 دولة. يمكننا إدراجها جميعاً باستخدام استدعاء مناسب لـ list.files():
paths <- list.files("data/gapminder", pattern = "[.]xlsx$", full.names = TRUE)
paths
#> [1] "data/gapminder/1952.xlsx" "data/gapminder/1957.xlsx"
#> [3] "data/gapminder/1962.xlsx" "data/gapminder/1967.xlsx"
#> [5] "data/gapminder/1972.xlsx" "data/gapminder/1977.xlsx"
#> [7] "data/gapminder/1982.xlsx" "data/gapminder/1987.xlsx"
#> [9] "data/gapminder/1992.xlsx" "data/gapminder/1997.xlsx"
#> [11] "data/gapminder/2002.xlsx" "data/gapminder/2007.xlsx"26.3.2 القوائم (Lists)
الآن بعد أن أصبح لدينا هذه المسارات الـ 12، يمكننا استدعاء read_excel() 12 مرة للحصول على 12 إطار بيانات:
gapminder_1952 <- readxl::read_excel("data/gapminder/1952.xlsx")
gapminder_1957 <- readxl::read_excel("data/gapminder/1957.xlsx")
gapminder_1962 <- readxl::read_excel("data/gapminder/1962.xlsx")
...,
gapminder_2007 <- readxl::read_excel("data/gapminder/2007.xlsx")لكن وضع كل ورقة عمل في متغير خاص بها سيجعل من الصعب التعامل معها لاحقاً. بدلاً من ذلك، سيكون التعامل معها أسهل إذا وضعناها في كائن واحد. تُعد القائمة (List) الأداة المثالية لهذه المهمة:
files <- list(
readxl::read_excel("data/gapminder/1952.xlsx"),
readxl::read_excel("data/gapminder/1957.xlsx"),
readxl::read_excel("data/gapminder/1962.xlsx"),
...,
readxl::read_excel("data/gapminder/2007.xlsx")
)الآن بعد أن أصبح لديك إطارات البيانات هذه في قائمة، كيف تستخرج واحداً منها؟ يمكنك استخدام files[[i]] لاستخراج العنصر رقم \(i\) (ith):
files[[3]]
#> # A tibble: 142 × 5
#> country continent lifeExp pop gdpPercap
#> <chr> <chr> <dbl> <dbl> <dbl>
#> 1 Afghanistan Asia 32.0 10267083 853.
#> 2 Albania Europe 64.8 1728137 2313.
#> 3 Algeria Africa 48.3 11000948 2551.
#> 4 Angola Africa 34 4826015 4269.
#> 5 Argentina Americas 65.1 21283783 7133.
#> 6 Australia Oceania 70.9 10794968 12217.
#> # ℹ 136 more rowsسنعود لمناقشة [[ بمزيد من التفصيل في قسم 27.3.
26.3.3 purrr::map() و list_rbind()
الكود المخصص لجمع إطارات البيانات هذه في قائمة “يدوياً” هو تقريباً مجهد في كتابته بنفس قدر كود قراءة الملفات واحداً تلو الآخر. لحسن الحظ، يمكننا استخدام purrr::map() للاستفادة بشكل أفضل من متجه المسارات paths الخاص بنا. تشبه map() الدالّة across()، ولكن بدلاً من القيام بشيء ما لكل عمود في إطار البيانات، فإنها تفعل شيئاً لكل عنصر في المتجه. تُعد map(x, f) اختصاراً لـ:
list(
f(x[[1]]),
f(x[[2]]),
...,
f(x[[n]])
)لذا يمكننا استخدام map() للحصول على قائمة تحتوي على 12 إطار بيانات:
files <- map(paths, readxl::read_excel)
length(files)
#> [1] 12
files[[1]]
#> # A tibble: 142 × 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.
#> # ℹ 136 more rows(هيكل البيانات هذا هو هيكل آخر لا يعرض نفسه بشكل مدمج للغاية مع str()، لذا قد ترغب في تحميله في RStudio وفحصه باستخدام View()).
الآن يمكننا استخدام purrr::list_rbind() لدمج قائمة إطارات البيانات تلك في إطار بيانات واحد:
list_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أو يمكننا القيام بكِلتا الخطوتين في وقت واحد باستخدام الأنبوب (pipeline):
paths |>
map(readxl::read_excel) |>
list_rbind()ماذا لو أردنا تمرير وسائط إضافية إلى read_excel()؟ نستخدم نفس التقنية التي استخدمناها مع across(). على سبيل المثال، غالباً ما يكون من المفيد إلقاء نظرة خاطفة على الصفوف القليلة الأولى من البيانات باستخدام n_max = 1:
paths |>
map(\(path) readxl::read_excel(path, n_max = 1)) |>
list_rbind()
#> # A tibble: 12 × 5
#> country continent lifeExp pop gdpPercap
#> <chr> <chr> <dbl> <dbl> <dbl>
#> 1 Afghanistan Asia 28.8 8425333 779.
#> 2 Afghanistan Asia 30.3 9240934 821.
#> 3 Afghanistan Asia 32.0 10267083 853.
#> 4 Afghanistan Asia 34.0 11537966 836.
#> 5 Afghanistan Asia 36.1 13079460 740.
#> 6 Afghanistan Asia 38.4 14880372 786.
#> # ℹ 6 more rowsهذا يوضح بوضوح أن هناك شيئاً مفقوداً: لا يوجد عمود year لأن هذه القيمة مسجلة في المسار، وليس في الملفات الفردية. سنعالج هذه المشكلة في الخطوة التالية.
26.3.4 البيانات الموجودة في المسار
في بعض الأحيان يكون اسم الملف نفسه عبارة عن بيانات. في هذا المثال، يحتوي اسم الملف على السنة، والتي لم يتم تسجيلها بخلاف ذلك في الملفات الفردية. للحصول على هذا العمود في إطار البيانات النهائي، نحتاج إلى القيام بأمرين:
أولاً، نقوم بتسمية متجه المسارات. أسهل طريقة للقيام بذلك هي استخدام الدالّة set_names()، والتي يمكنها أخذ دالّة كمدخل. نستخدم هنا basename() لاستخراج اسم الملف فقط من المسار الكامل:
paths |> set_names(basename)
#> 1952.xlsx 1957.xlsx
#> "data/gapminder/1952.xlsx" "data/gapminder/1957.xlsx"
#> 1962.xlsx 1967.xlsx
#> "data/gapminder/1962.xlsx" "data/gapminder/1967.xlsx"
#> 1972.xlsx 1977.xlsx
#> "data/gapminder/1972.xlsx" "data/gapminder/1977.xlsx"
#> 1982.xlsx 1987.xlsx
#> "data/gapminder/1982.xlsx" "data/gapminder/1987.xlsx"
#> 1992.xlsx 1997.xlsx
#> "data/gapminder/1992.xlsx" "data/gapminder/1997.xlsx"
#> 2002.xlsx 2007.xlsx
#> "data/gapminder/2002.xlsx" "data/gapminder/2007.xlsx"يتم نقل هذه الأسماء تلقائياً بواسطة جميع دالّات map، لذا فإن قائمة إطارات البيانات ستكون لها نفس الأسماء:
files <- paths |>
set_names(basename) |>
map(readxl::read_excel)هذا يجعل هذا الاستدعاء لـ map() اختصاراً لـ:
files <- list(
"1952.xlsx" = readxl::read_excel("data/gapminder/1952.xlsx"),
"1957.xlsx" = readxl::read_excel("data/gapminder/1957.xlsx"),
"1962.xlsx" = readxl::read_excel("data/gapminder/1962.xlsx"),
...,
"2007.xlsx" = readxl::read_excel("data/gapminder/2007.xlsx")
)يمكنك أيضاً استخدام [[ لاستخراج العناصر حسب الاسم:
files[["1962.xlsx"]]
#> # A tibble: 142 × 5
#> country continent lifeExp pop gdpPercap
#> <chr> <chr> <dbl> <dbl> <dbl>
#> 1 Afghanistan Asia 32.0 10267083 853.
#> 2 Albania Europe 64.8 1728137 2313.
#> 3 Algeria Africa 48.3 11000948 2551.
#> 4 Angola Africa 34 4826015 4269.
#> 5 Argentina Americas 65.1 21283783 7133.
#> 6 Australia Oceania 70.9 10794968 12217.
#> # ℹ 136 more rowsثم نستخدم الوسيط names_to مع list_rbind() لإخباره بحفظ الأسماء في عمود جديد يسمى year ثم نستخدم readr::parse_number() لاستخراج الرقم من السلسلة النصية.
paths |>
set_names(basename) |>
map(readxl::read_excel) |>
list_rbind(names_to = "year") |>
mutate(year = parse_number(year))
#> # A tibble: 1,704 × 6
#> year country continent lifeExp pop gdpPercap
#> <dbl> <chr> <chr> <dbl> <dbl> <dbl>
#> 1 1952 Afghanistan Asia 28.8 8425333 779.
#> 2 1952 Albania Europe 55.2 1282697 1601.
#> 3 1952 Algeria Africa 43.1 9279525 2449.
#> 4 1952 Angola Africa 30.0 4232095 3521.
#> 5 1952 Argentina Americas 62.5 17876956 5911.
#> 6 1952 Australia Oceania 69.1 8691212 10040.
#> # ℹ 1,698 more rowsفي الحالات الأكثر تعقيداً، قد تكون هناك متغيرات أخرى مخزنة في اسم المجلد، أو ربما يحتوي اسم الملف على أجزاء متعددة من البيانات. في هذه الحالة، استخدم set_names() (بدون أي وسائط) لتسجيل المسار الكامل، ثم استخدم tidyr::separate_wider_delim() وأخواتها لتحويلها إلى أعمدة مفيدة.
paths |>
set_names() |>
map(readxl::read_excel) |>
list_rbind(names_to = "year") |>
separate_wider_delim(year, delim = "/", names = c(NA, "dir", "file")) |>
separate_wider_delim(file, delim = ".", names = c("file", "ext"))
#> # A tibble: 1,704 × 8
#> dir file ext country continent lifeExp pop gdpPercap
#> <chr> <chr> <chr> <chr> <chr> <dbl> <dbl> <dbl>
#> 1 gapminder 1952 xlsx Afghanistan Asia 28.8 8425333 779.
#> 2 gapminder 1952 xlsx Albania Europe 55.2 1282697 1601.
#> 3 gapminder 1952 xlsx Algeria Africa 43.1 9279525 2449.
#> 4 gapminder 1952 xlsx Angola Africa 30.0 4232095 3521.
#> 5 gapminder 1952 xlsx Argentina Americas 62.5 17876956 5911.
#> 6 gapminder 1952 xlsx Australia Oceania 69.1 8691212 10040.
#> # ℹ 1,698 more rows26.3.5 احفظ عملك
الآن بعد أن قمت بكل هذا العمل الشاق للوصول إلى إطار بيانات مرتب (tidy)، فقد حان الوقت لحفظ عملك:
gapminder <- paths |>
set_names(basename) |>
map(readxl::read_excel) |>
list_rbind(names_to = "year") |>
mutate(year = parse_number(year))
write_csv(gapminder, "gapminder.csv")الان، عندما تعود لهذه المشكلة في المستقبل، يمكنك قراءتها في ملف csv واحد. بالنسبة لمجموعات البيانات الكبيرة والأكثر غنى، قد يكون استخدام صيغة parquet خياراً أفضل من ملفات .csv، كما هو موضح في قسم 22.4.
إذا كنت تعمل في مشروع، فنحن نقترح تسمية الملف الذي يقوم بهذا النوع من تجهيز البيانات باسم مثل 0-cleanup.R. يشير الرمز 0 في اسم الملف إلى أنه ينبغي تشغيل هذا الملف قبل أي شيء آخر.
إذا كانت ملفات البيانات المدخلة تتغير مع مرور الوقت، فقد تفكر في تعلم أداة مثل targets لإعداد كود تنظيف البيانات ليعيد التشغيل تلقائياً كلما تم تعديل أحد الملفات المدخلة.
26.3.6 تكرارات بسيطة متعددة
هنا قمنا للتو بتحميل البيانات مباشرة من القرص الصلب، وكنا محظوظين بالحصول على مجموعة بيانات مرتبة. في معظم الحالات، ستحتاج إلى إجراء بعض التنظيف الإضافي، ولديك خياران أساسيان: يمكنك إجراء جولة واحدة من التكرار باستخدام دالّة معقدة، أو إجراء جولات متعددة من التكرار باستخدام دالّات بسيطة. من واقع خبرتنا، يتجه معظم الأشخاص أولاً إلى تكرار معقد واحد، ولكن غالباً ما تكون في وضع أفضل من خلال إجراء تكرارات بسيطة متعددة.
على سبيل المثال، تخيل أنك تريد قراءة مجموعة من الملفات، وتصفية القيم المفقودة، وتدويرها (pivot)، ثم دمجها. إحدى الطرق لمعالجة المشكلة هي كتابة دالّة تأخذ ملفاً وتنفذ كل تلك الخطوات، ثم تستدعي map() مرة واحدة:
process_file <- function(path) {
df <- read_csv(path)
df |>
filter(!is.na(id)) |>
mutate(id = tolower(id)) |>
pivot_longer(jan:dec, names_to = "month")
}
paths |>
map(process_file) |>
list_rbind()بدلاً من ذلك، يمكنك تنفيذ كل خطوة من خطوات process_file() على كل ملف:
paths |>
map(read_csv) |>
map(\(df) df |> filter(!is.na(id))) |>
map(\(df) df |> mutate(id = tolower(id))) |>
map(\(df) df |> pivot_longer(jan:dec, names_to = "month")) |>
list_rbind()نوصي بهذا النهج لأنه يمنعك من التركيز على ضبط الملف الأول بشكل صحيح قبل الانتقال إلى باقي الملفات. من خلال مراعاة جميع البيانات عند القيام بالتنظيف والترتيب، فمن المرجح أن تفكر بنظرة شمولية وتحصل على نتيجة ذات جودة أعلى.
في هذا المثال المحدد، هناك تحسين آخر يمكنك إجراؤه، عن طريق ربط جميع إطارات البيانات معاً في مرحلة مبكرة. ثم يمكنك الاعتماد على سلوك dplyr العادي:
paths |>
map(read_csv) |>
list_rbind() |>
filter(!is.na(id)) |>
mutate(id = tolower(id)) |>
pivot_longer(jan:dec, names_to = "month")26.3.7 البيانات غير المتجانسة (Heterogeneous data)
لسوء الحظ، لا يكون من الممكن أحياناً الانتقال من map() مباشرة إلى list_rbind() لأن إطارات البيانات تكون غير متجانسة للغاية بحيث تفشل list_rbind() أو تنتج إطار بيانات ليس مفيداً جداً. في هذه الحالة، لا يزال من المفيد البدء بتحميل جميع الملفات:
files <- paths |>
map(readxl::read_excel) عندئذٍ تكون الاستراتيجية المفيدة جداً هي التقاط بنية إطارات البيانات (Data frames) حتى تتمكن من استكشافها باستخدام مهاراتك في علم البيانات. وإحدى الطرق للقيام بذلك هي استخدام الدالة المفيدة df_types()6 التي تُرجع جدول بيانات من نوع tibble يحتوي على صف واحد لكل عمود:
df_types <- function(df) {
tibble(
col_name = names(df),
col_type = map_chr(df, vctrs::vec_ptype_full),
n_miss = map_int(df, \(x) sum(is.na(x)))
)
}
df_types(gapminder)
#> # A tibble: 6 × 3
#> col_name col_type n_miss
#> <chr> <chr> <int>
#> 1 year double 0
#> 2 country character 0
#> 3 continent character 0
#> 4 lifeExp double 0
#> 5 pop double 0
#> 6 gdpPercap double 0يمكنك بعد ذلك تطبيق هذه الدالّة على جميع الملفات، وربما إجراء بعض التدوير (pivoting) لتسهيل رؤية مواضع الاختلافات. على سبيل المثال، هذا يسهل التحقق من أن جداول بيانات gapminder التي نعمل عليها كلها متجانسة تماماً:
files |>
map(df_types) |>
list_rbind(names_to = "file_name") |>
select(-n_miss) |>
pivot_wider(names_from = col_name, values_from = col_type)
#> # A tibble: 12 × 6
#> file_name country continent lifeExp pop gdpPercap
#> <chr> <chr> <chr> <chr> <chr> <chr>
#> 1 1952.xlsx character character double double double
#> 2 1957.xlsx character character double double double
#> 3 1962.xlsx character character double double double
#> 4 1967.xlsx character character double double double
#> 5 1972.xlsx character character double double double
#> 6 1977.xlsx character character double double double
#> # ℹ 6 more rowsإذا كانت الملفات تحتوي على تنسيقات غير متجانسة، فقد تحتاج إلى إجراء المزيد من المعالجة قبل أن تتمكن من دمجها بنجاح. قد ترغب في القراءة عن map_if() و map_at()؛ حيث تسمح لك map_if() بتعديل عناصر القائمة اختيارياً بناءً على قيمها، بينما تسمح لك map_at() بتعديل العناصر اختيارياً بناءً على أسمائها.
26.3.8 التعامل مع الأخطاء والإخفاقات
في بعض الأحيان قد يكون هيكل بياناتك معقداً بدرجة كافية بحيث لا يمكنك حتى قراءة جميع الملفات بأمر واحد. وعندها ستواجه أحد العيوب في map(): فهي إما تنجح ككل أو تفشل ككل. ستقوم map() إما بقراءة جميع الملفات في الدليل بنجاح أو تفشل مع ظهور خطأ، وتقرأ صفر ملفات. هذا أمر مزعج: لماذا يمنعك فشل واحد من الوصول إلى جميع النجاحات الأخرى؟
لحسن الحظ، تأتي purrr مع أداة مساعدة لمعالجة هذه المشكلة: possibly(). possibly() هي ما يُعرف بـ “مشغل الدالّة (function operator)”: فهي تأخذ دالّة وترجع دالّة وسلوكها معدل. على وجه الخصوص، تقوم possibly() بتغيير الدالّة من إصدار خطأ إلى إرجاع قيمة تحددها أنت:
files <- paths |>
map(possibly(\(path) readxl::read_excel(path), NULL))
data <- files |> list_rbind()يعمل هذا بشكل جيد هنا لأن list_rbind()، مثل العديد من دالّات tidyverse، تتجاهل قيم NULL تلقائياً.
الآن لديك كل البيانات التي يمكن قراءتها بسهولة، وحان الوقت لمعالجة الجزء الصعب المتمثل في معرفة سبب فشل تحميل بعض الملفات وما يجب فعله حيال ذلك. ابدأ بالحصول على المسارات التي فشلت:
failed <- map_vec(files, is.null)
paths[failed]
#> character(0)ثم استدعِ دالّة الاستيراد مرة أخرى لكل فشل واعرف ما الذي حدث بشكل خاطئ.
26.4 حفظ المخرجات المتعددة
في القسم السابق، تعلمت عن map()، وهي مفيدة لقراءة ملفات متعددة في كائن واحد. في هذا القسم، سنستكشف الآن عكس هذه المشكلة تقريباً: كيف يمكنك أخذ كائن واحد أو أكثر في R وحفظه في ملف واحد أو أكثر؟ سنستكشف هذا التحدي باستخدام ثلاثة أمثلة:
- حفظ إطارات بيانات متعددة في قاعدة بيانات واحدة.
- حفظ إطارات بيانات متعددة في ملفات
.csvمتعددة. - حفظ رسومات بيانية متعددة في ملفات
.pngمتعددة.
26.4.1 الكتابة إلى قاعدة بيانات
في بعض الأحيان عند العمل مع ملفات متعددة في وقت واحد، لا يكون من الممكن استيعاب جميع بياناتك في الذاكرة مرة واحدة، ولا يمكنك إجراء map(files, read_csv). أحد المناهج للتعامل مع هذه المشكلة هو تحميل بياناتك في قاعدة بيانات حتى تتمكن من الوصول إلى الأجزاء التي تحتاجها فقط باستخدام dbplyr.
إذا كنت محظوظاً، فإن حزمة قاعدة البيانات التي تستخدمها ستوفر دالّة مفيدة تأخذ متجهاً من المسارات وتحملها جميعاً في قاعدة البيانات. هذه هي الحالة مع الدالّة duckdb_read_csv() الخاصة بـ duckdb:
con <- DBI::dbConnect(duckdb::duckdb())
duckdb::duckdb_read_csv(con, "gapminder", paths)هذا سيعمل بشكل جيد هنا، لكن ليس لدينا ملفات csv، بل لدينا جداول بيانات excel. لذا سنضطر إلى القيام بذلك “يدوياً”. سيساعدك تعلم القيام بذلك يدوياً أيضاً عندما يكون لديك مجموعة من ملفات csv وقاعدة البيانات التي تعمل معها لا تحتوي على دالّة واحدة تحملها جميعاً.
نحتاج إلى البدء بإنشاء جدول سنملؤه بالبيانات. أسهل طريقة القيام بذلك هي إنشاء قالب (template)، وهو إطار بيانات وهمي يحتوي على جميع الأعمدة التي نريدها، ولكن فقط مع عينة من البيانات. بالنسبة لبيانات gapminder، يمكننا إنشاء هذا القالب عن طريق قراءة ملف واحد وإضافة السنة إليه:
template <- readxl::read_excel(paths[[1]])
template$year <- 1952
template
#> # A tibble: 142 × 6
#> country continent lifeExp pop gdpPercap year
#> <chr> <chr> <dbl> <dbl> <dbl> <dbl>
#> 1 Afghanistan Asia 28.8 8425333 779. 1952
#> 2 Albania Europe 55.2 1282697 1601. 1952
#> 3 Algeria Africa 43.1 9279525 2449. 1952
#> 4 Angola Africa 30.0 4232095 3521. 1952
#> 5 Argentina Americas 62.5 17876956 5911. 1952
#> 6 Australia Oceania 69.1 8691212 10040. 1952
#> # ℹ 136 more rowsالآن يمكننا الاتصال ببيئة قاعدة البيانات، واستخدام DBI::dbCreateTable() لتحويل القالب الخاص بنا إلى جدول قاعدة بيانات:
con <- DBI::dbConnect(duckdb::duckdb())
#> duckdb keeps downloaded extensions and secrets in a temporary directory:
#> ℹ /tmp/Rtmp0oafXl/duckdb
#> This is removed when the R session ends.
#> • Extensions are re-downloaded each session.
#> • Secrets are lost.
#> ℹ Run duckdb(shared_home = TRUE) (or create ~/.duckdb) to keep them (suitable for most users).
#> ℹ Run duckdb(shared_home = FALSE) to accept the temporary directory (and silence this message).
#> ℹ See ?duckdb_storage for details and alternatives.
DBI::dbCreateTable(con, "gapminder", template)لا تستخدم dbCreateTable() البيانات الموجودة في template، بل تستخدم فقط أسماء المتغيرات وأنواعها. لذا إذا تفحصنا جدول gapminder الآن، ستلاحظ أنه فارغ ولكنه يحتوي على المتغيرات التي نحتاجها بالأنواع التي نتوقعها:
con |> tbl("gapminder")
#> # A query: ?? x 6
#> # Database: DuckDB 1.5.5 [unknown@Linux 6.17.0-1022-azure:R 4.6.1/:memory:]
#> # ℹ 6 variables: country <chr>, continent <chr>, lifeExp <dbl>, pop <dbl>,
#> # gdpPercap <dbl>, year <dbl>بعد ذلك، نحتاج إلى دالّة تأخذ مسار ملف واحد، وتقرأه في R، وتضيف النتيجة إلى جدول gapminder. يمكننا القيام بذلك عن طريق الجمع بين read_excel() و DBI::dbAppendTable():
append_file <- function(path) {
df <- readxl::read_excel(path)
df$year <- parse_number(basename(path))
DBI::dbAppendTable(con, "gapminder", df)
}الآن نحتاج إلى استدعاء append_file() مرة واحدة لكل عنصر من مسارات paths. هذا ممكن بالتأكيد باستخدام map():
paths |> map(append_file)ولكننا لا نهتم بمخرجات append_file()، لذا بدلاً من map() من الأفضل قليلاً استخدام walk(). تفعل walk() نفس شيء map() بالضبط ولكنها تتخلص من المخرجات:
paths |> walk(append_file)الآن يمكننا معرفة ما إذا كان لدينا جميع البيانات في جدولنا:
26.4.2 كتابة ملفات csv
ينطبق نفس المبدأ الأساسي إذا أردنا كتابة ملفات csv متعددة، واحد لكل مجموعة. دعنا نتخيل أننا نريد أخذ بيانات ggplot2::diamonds وحفظ ملف csv واحد لكل درجة نقاء clarity. أولاً نحتاج إلى إنشاء مجموعات البيانات الفردية تلك. هناك العديد من الطرق التي يمكنك من خلالها القيام بذلك، ولكن هناك طريقة واحدة نفضلها بشكل خاص: group_nest().
by_clarity <- diamonds |>
group_nest(clarity)
by_clarity
#> # A tibble: 8 × 2
#> clarity data
#> <ord> <list<tibble[,9]>>
#> 1 I1 [741 × 9]
#> 2 SI2 [9,194 × 9]
#> 3 SI1 [13,065 × 9]
#> 4 VS2 [12,258 × 9]
#> 5 VS1 [8,171 × 9]
#> 6 VVS2 [5,066 × 9]
#> # ℹ 2 more rowsهذا يعطينا جدولاً جديداً يتكون من ثمانية صفوف وعمودين. clarity هو متغير التجميع الخاص بنا و data هو عمود قائمة (list-column) يحتوي على جدول واحد لكل قيمة فريدة من clarity:
by_clarity$data[[1]]
#> # A tibble: 741 × 9
#> carat cut color depth table price x y z
#> <dbl> <ord> <ord> <dbl> <dbl> <int> <dbl> <dbl> <dbl>
#> 1 0.32 Premium E 60.9 58 345 4.38 4.42 2.68
#> 2 1.17 Very Good J 60.2 61 2774 6.83 6.9 4.13
#> 3 1.01 Premium F 61.8 60 2781 6.39 6.36 3.94
#> 4 1.01 Fair E 64.5 58 2788 6.29 6.21 4.03
#> 5 0.96 Ideal F 60.7 55 2801 6.37 6.41 3.88
#> 6 1.04 Premium G 62.2 58 2801 6.46 6.41 4
#> # ℹ 735 more rowsأثناء وجودنا هنا، لننشئ عموداً يعطي اسم ملف المخرجات، باستخدام mutate() و str_glue():
by_clarity <- by_clarity |>
mutate(path = str_glue("diamonds-{clarity}.csv"))
by_clarity
#> # A tibble: 8 × 3
#> clarity data path
#> <ord> <list<tibble[,9]>> <glue>
#> 1 I1 [741 × 9] diamonds-I1.csv
#> 2 SI2 [9,194 × 9] diamonds-SI2.csv
#> 3 SI1 [13,065 × 9] diamonds-SI1.csv
#> 4 VS2 [12,258 × 9] diamonds-VS2.csv
#> 5 VS1 [8,171 × 9] diamonds-VS1.csv
#> 6 VVS2 [5,066 × 9] diamonds-VVS2.csv
#> # ℹ 2 more rowsلذا إذا كنا سنحفظ إطارات البيانات هذه يدوياً، فقد نكتب شيئاً مثل:
هذا مختلف قليلاً عن استخداماتنا السابقة لـ map() لأن هناك وسيطين يتغيران، وليس وسيطاً واحداً فقط. هذا يعني أننا بحاجة إلى دالّة جديدة: map2()، والتي تغير كلاً من الوسيطين الأول والثاني. ولأننا مرة أخرى لا نهتم بالمخرجات، فنحن نريد walk2() بدلاً من map2(). هذا يعطينا:
walk2(by_clarity$data, by_clarity$path, write_csv)26.4.3 حفظ الرسوم البيانية
يمكننا اتخاذ نفس النهج الأساسي لإنشاء العديد من الرسوم البيانية. لنقم أولاً بإنشاء دالّة ترسم الرسم البياني الذي نريده:
carat_histogram <- function(df) {
ggplot(df, aes(x = carat)) + geom_histogram(binwidth = 0.1)
}
carat_histogram(by_clarity$data[[1]])
الان يمكننا استخدام الدالة map() لإنشاء قائمة تحتوي على العديد من الرسوم البيانية7 ومسارات ملفاتها النهائية:
ثم نستخدم walk2() مع ggsave() لحفظ كل رسم بياني:
هذا اختصار لـ:
26.5 ملخص
في هذا الفصل، رأيت كيفية استخدام التكرار الصريح لحل ثلاث مشكلات تظهر كثيراً عند ممارسة علم البيانات: التعامل مع أعمدة متعددة، قراءة ملفات متعددة، وحفظ مخرجات متعددة. ولكن بشكل عام، يُعد التكرار قوة فائقة: إذا كنت تعرف تقنية التكرار المناسبة، يمكنك بسهولة الانتقال من حل مشكلة واحدة إلى حل جميع المشكلات. بمجرد إتقان التقنيات الواردة في هذا الفصل، نوصي بشدة بزيادة معرفتك من خلال قراءة فصل الدالّات الوظيفية (Functionals chapter) في كتاب Advanced R والاطلاع على موقع purrr.
إذا كنت تعرف الكثير عن التكرار في اللغات الأخرى، فقد تتفاجأ لأننا لم نناقش حلقة for. يرجع ذلك إلى أن توجه R نحو تحليل البيانات يغير كيفية تكرارنا للعمليات: في معظم الحالات يمكنك الاعتماد على نمط صياغة موحد قائم بالفعل للقيام بشيء ما لكل عمود أو كل مجموعة. وعندما لا تستطيع ذلك، يمكنك غالباً استخدام أداة برمجة وظيفية مثل map() تقوم بشيء ما لكل عنصر في القائمة. ومع ذلك، ستشاهد حلقات for في الكواد المتداولة عموماً، لذا ستتعلم عنها في الفصل القادم حيث سنناقش بعض الأدوات المهمة في لغة R الأساسية (base R).
سُمّيت مجهولة لأننا لم نمنحها اسماً صريحاً مطلقاً باستخدام معامل التعيين
<-. وهناك مصطلح آخر يستخدمه المبرمجون لوصف هذا النوع وهو “دالة لامبدا” (Lambda function).↩︎في الأكواد القديمة قد تلاحظ صياغة تبدو مثل
~ .x + 1. هذه طريقة أخرى لكتابة الدوال المجهولة ولكنها تعمل فقط داخل دوال حزمة tidyverse، وتستخدم دائماً اسم المتغير.x. نحن نوصي الآن باستخدام الصياغة الأساسية للغة (Base syntax):\(x) x + 1.↩︎لا يمكنك حالياً تغيير ترتيب الأعمدة مباشرة، ولكن يمكنك إعادة ترتيبها بعد ذلك باستخدام الدالة
relocate()أو ما يشابهها.↩︎قد تتوفر طريقة لذلك يوماً ما، لكننا لا نرى كيفية القيام بذلك في الوقت الحالي.↩︎
إذا كان لديك بدلاً من ذلك مجلد يحتوي على ملفات csv بنفس التنسيق، فيمكنك استخدام التقنية الموضحة في قسم 7.4.↩︎
لن نقم بشرح كيفية عملها، ولكن إذا نظرت إلى وثائق الدوال المستخدمة (Documentation)، فستتمكن من استنتاج طريقة عملها بنفسك.↩︎
يمكنك طباعة
by_clarity$plotللحصول على رسوم متحركة بسيطة (Animation) — حيث ستحصل على رسم بياني واحد لكل عنصر فيplots. ملاحظة: هذا لم يحدث معي.↩︎