23 البيانات الهرمية
23.1 مقدمة
في هذا الفصل، ستتعلم فن تسوية البيانات في جداول مستطيلة (data rectangling): أخذ البيانات ذات البنية الهرمية (hierarchical) أو الشجرية في أصلها، وتحويلها إلى إطار بيانات مستطيل (rectangular data frame) مكون من صفوف وأعمدة. هذا الأمر مهم لأن البيانات الهرمية شائعة بشكل مدهش، خاصة عند التعامل مع البيانات القادمة من الويب.
للتعلم عن تحويل البيانات إلى مستطيلة، ستتحاج أولاً إلى التعرف على القوائم (lists)، وهي بنية البيانات التي تجعل تمثيل البيانات الهرمية ممكناً. ثم ستتعلم دالتين حاسمتين من حزمة tidyr وهما: tidyr::unnest_longer() و tidyr::unnest_wider(). سنعرض لك بعد ذلك بضع دراسات حالة، ونطبق هذه الدوال البسيطة مراراً وتكراراً لحل مشكلات حقيقية. وسنختتم بالحديث عن صيغة JSON، وهي المصدر الأكثر شيوعاً لمجموعات البيانات الهرمية وصيغة شائعة لتبادل البيانات على الويب.
23.1.1 المتطلبات المسبقة
في هذا الفصل، سنستخدم العديد من الدوال من حزمة tidyr، وهي عضو أساسي في منظومة tidyverse. سنستخدم أيضاً حزمة repurrrsive لتقديم بعض مجموعات البيانات الممتعة للممارسة على التربيع، وسنختتم باستخدام jsonlite لقراءة ملفات JSON وتحويلها إلى قوائم في R.
23.2 القوائم (Lists)
حتى الآن، عملت مع أطر البيانات التي تحتوي على متجهات بسيطة مثل الأعداد الصحيحة، والأرقام، والنصوص، والتواريخ/الأوقات، والعوامل (factors). تُعد هذه المتجهات بسيطة لأنها متجانسة: كل عنصر فيها من نفس نوع البيانات. إذا كنت تريد تخزين عناصر من أنواع مختلفة في نفس المتجه، فستحتاج إلى قائمة (list)، والتي تنشئها باستخدام list():
x1 <- list(1:4, "a", TRUE)
x1
#> [[1]]
#> [1] 1 2 3 4
#>
#> [[2]]
#> [1] "a"
#>
#> [[3]]
#> [1] TRUEغالباً ما يكون من المناسب تسمية المكونات، أو الأبناء (children) داخل القائمة، والتي يمكنك القيام بها بنفس طريقة تسمية أعمدة tibble:
x2 <- list(a = 1:2, b = 1:3, c = 1:4)
x2
#> $a
#> [1] 1 2
#>
#> $b
#> [1] 1 2 3
#>
#> $c
#> [1] 1 2 3 4حتى بالنسبة لهذه القوائم البسيطة جداً، فإن الطباعة تأخذ مساحة كبيرة إلى حد ما. البديل المفيد هو استخدام الدالة str()، والتي تولد عرضاً مضغوطاً لـ هيكل (structure) القائمة، مع تقليل التركيز على المحتويات التفصيلية:
كما ترى، تعرض str() كل ابن من أبناء القائمة في سطر مستقل. حيث تعرض الاسم -إن وجد- ثم اختصاراً للنوع، ثم القيم القليلة الأولى.
23.2.1 الهيكلية الهرمية (Hierarchy)
يمكن للقوائم أن تحتوي على أي نوع من الكائنات، بما في ذلك قوائم أخرى. هذا يجعلها مناسبة لتمثيل الهياكل الهرمية (الشجرية):
هذا يختلف بشكل ملحوظ عن c()، التي تولد متجهاً مسطحاً:
كلما أصبحت القوائم أكثر تعقيداً، أصبحت str() أكثر فائدة، لأنها تتيح لك رؤية الهيكل الهرمي بنظرة واحدة:
مع كبر حجم القوائم وازدياد تعقيدها، تبدأ str() في الفشل في نهاية المطاف، وستحتاج إلى الانتقال إلى استخدام View()1. يعرض الشكل 23.1 نتيجة استدعاء View(x5). يبدأ المستعرض باظهار المستوى الأعلى فقط من القائمة، ولكن يمكنك توسيع أي من المكونات تفاعلياً لرؤية المزيد، كما في الشكل 23.2. سيظهر لك RStudio أيضاً الكود الذي تحتاجه للوصول إلى ذلك العنصر المحدد، كما في الشكل 23.3. سنعود إلى كيفية عمل هذا الكود في قسم 27.3.
x5[[2]][[2]][[2]].
23.2.2 أعمدة القوائم (List-columns)
يمكن للقوائم أيضاً أن تعيش داخل tibble، حيث نطلق عليها اسم أعمدة القوائم (list-columns). تُعد أعمدة القوائم مفيدة لأنها تتيح لك وضع كائنات في tibble لا تنتمي عادة إليها. على وجه الخصوص، تُستخدم أعمدة القوائم كثيراً في بيئة tidymodels، لأنها تتيح لك تخزين أشياء مثل مخرجات النماذج أو إعادة العينات داخل إطار البيانات.
إليك مثالاً بسيطاً على أعمدة القوائم:
لا يوجد شيء خاص حول القوائم في tibble؛ فهي تتصرف تماماً مثل أي عمود آخر:
df |>
filter(x == 1)
#> # A tibble: 1 × 3
#> x y z
#> <int> <chr> <list>
#> 1 1 a <list [2]>الحوسبة باستخدام أعمدة القوائم أصعب، ولكن هذا لأن الحوسبة مع القوائم أصعب بشكل عام؛ وسنعود إلى ذلك في الفصل 26. في هذا الفصل، سنركز على فك تسلسل (تداخل) (unnesting) أعمدة القوائم لتحويلها إلى متغيرات عادية حتى تتمكن من استخدام أدواتك الحالية عليها.
طريقة الطباعة الافتراضية تعرض فقط ملخصاً تقريبياً للمحتويات. يمكن أن يكون عمود القائمة معقداً لأقصى درجة، لذا لا توجد طريقة مثالية لطباعته. إذا كنت تريد رؤيته، فستحتاج إلى سحب عمود قائمة واحد وتطبيق إحدى التقنيات التي تعلمتها أعلاه، مثل df |> pull(z) |> str() أو df |> pull(z) |> View().
من الممكن وضع قائمة في عمود داخل data.frame، ولكن الأمر أشد تعقيداً لأن data.frame() تتعامل مع القائمة كقائمة أعمدة:
data.frame(x = list(1:3, 3:5))
#> x.1.3 x.3.5
#> 1 1 3
#> 2 2 4
#> 3 3 5يمكنك إجبار data.frame() على التعامل مع القائمة كقائمة صفوف عن طريق تلفيفها بدالة I()، ولكن النتيجة لا تُطبع بشكل جيد للغاية:
data.frame(
x = I(list(1:2, 3:5)),
y = c("1, 2", "3, 4, 5")
)
#> x y
#> 1 1, 2 1, 2
#> 2 3, 4, 5 3, 4, 5من الأسهل استخدام أعمدة القوائم مع tibbles لأن tibble() تتعامل مع القوائم مثل المتجهات وتم تصميم طريقة الطباعة مع مراعاة القوائم.
23.3 فك التسلسل أو التداخل (Unnesting)
الآن بعد أن تعلمت أساسيات القوائم وأعمدة القوائم، دعنا نستكشف كيف يمكنك تحويلها مجدداً إلى صفوف وأعمدة عادية. هنا سنستخدم بيانات عينة بسيطة جداً حتى تتمكن من الحصول على الفكرة الأساسية؛ وفي القسم التالي سننتقل إلى بيانات حقيقية.
تميل أعمدة القوائم إلى الظهور في شكلين أساسيين: مسمى وغير مسمى. عندما تكون العناصر الأبناء مسماة (named)، فإنها تميل إلى امتلاك نفس الأسماء في كل صف. على سبيل المثال، في df1، يحتوي كل عنصر في عمود القائمة y على عنصرين مسميين هما a و b. أعمدة القوائم المسماة تتفكك طبيعياً إلى أعمدة: يصبح كل عنصر مسمى عموداً مسمى جديداً.
عندما تكون العناصر الأبناء غير مسماة (unnamed)، يميل عدد العناصر إلى التفاوت من صف إلى آخر. على سبيل المثال، في df2، فإن عناصر عمود القائمة y غير مسماة ويتفاوت طولها من واحد إلى ثلاثة. أعمدة القوائم غير المسماة تتفكك طبيعياً إلى صفوف: ستحصل على صف واحد لكل ابن.
توفر tidyr دالتين لهاتين الحالتين: unnest_wider() و unnest_longer(). توضح الأقسام التالية كيفية عملهما.
23.3.1 unnest_wider()
عندما يحتوي كل صف على نفس العدد من العناصر وبنفس الأسماء، مثل df1، فمن الطبيعي وضع كل مكون في عموده الخاص باستخدام unnest_wider():
df1 |>
unnest_wider(y)
#> # A tibble: 3 × 3
#> x a b
#> <dbl> <dbl> <dbl>
#> 1 1 11 12
#> 2 2 21 22
#> 3 3 31 32بشكل افتراضي، تأتي أسماء الأعمدة الجديدة حصرياً من أسماء عناصر القائمة، ولكن يمكنك استخدام معامل names_sep لطلب دمج اسم العمود واسم العنصر معاً. هذا مفيد لإزالة الإبهام عن الأسماء المكررة.
df1 |>
unnest_wider(y, names_sep = "_")
#> # A tibble: 3 × 3
#> x y_a y_b
#> <dbl> <dbl> <dbl>
#> 1 1 11 12
#> 2 2 21 22
#> 3 3 31 32
23.3.2 unnest_longer()
عندما يحتوي كل صف على قائمة غير مسماة، فمن الأكثر طبيعية وضع كل عنصر في صفه الخاص باستخدام unnest_longer():
df2 |>
unnest_longer(y)
#> # A tibble: 6 × 2
#> x y
#> <dbl> <dbl>
#> 1 1 11
#> 2 1 12
#> 3 1 13
#> 4 2 21
#> 5 3 31
#> 6 3 32لاحظ كيف يتم تكرار x لكل عنصر داخل y: نحصل على صف واحد من المخرجات لكل عنصر داخل عمود القائمة. ولكن ماذا يحدث إذا كان أحد العناصر فارغاً، كما في المثال التالي؟
df6 <- tribble(
~x, ~y,
"a", list(1, 2),
"b", list(3),
"c", list()
)
df6 |> unnest_longer(y)
#> # A tibble: 3 × 2
#> x y
#> <chr> <dbl>
#> 1 a 1
#> 2 a 2
#> 3 b 3نحصل على صفر صفوف في المخرجات، وبالتالي يختفي الصف تماماً. إذا كنت تريد الحفاظ على ذلك الصف وإضافة NA في y، فقم بضبط keep_empty = TRUE.
23.3.3 الأنواع غير المتناسقة
ماذا يحدث إذا قمت بفك تسطير عمود قائمة يحتوي على أنواع مختلفة من المتجهات؟ على سبيل المثال، خذ مجموعة البيانات التالية حيث يحتوي عمود القائمة y على رقمين، ونص، وقيمة منطقية، وهي عناصر لا يمكن خلطها عادةً في عمود واحد.
تحافظ unnest_longer() دائمًا على مجموعة الأعمدة دون تغيير، بينما تغير عدد الصفوف. فماذا يحدث إذاً؟ كيف تنتج unnest_longer() خمسة صفوف مع الحفاظ على كل شيء داخل y؟
df4 |>
unnest_longer(y)
#> # A tibble: 4 × 2
#> x y
#> <chr> <list>
#> 1 a <dbl [1]>
#> 2 b <chr [1]>
#> 3 b <lgl [1]>
#> 4 b <dbl [1]>كما ترى، تحتوي المخرجات على عمود قائمة، ولكن كل عنصر في عمود القائمة يحتوي على عنصر واحد فقط. نظراً لأن unnest_longer() لا يمكنها العثور على نوع مشترك من المتجهات، فإنها تحتفظ بالأنواع الأصلية داخل عمود قائمة. قد تتساءل عما إذا كان هذا يكسر القاعدة التي تنص على أن كل عنصر في العمود يجب أن يكون من نفس النوع. في الحقيقة لا يكسرها: كل عنصر هو قائمة، حتى لو كانت المحتويات بداخلها من أنواع مختلفة.
التعامل مع الأنواع غير المتناسقة أمر مليء بالتحديات وتعتمد تفاصيله على الطبيعة الدقيقة للمشكلة وأهدافك، ولكن من المرجح أنك ستحتاج إلى أدوات من الفصل 26.
23.3.4 دوال أخرى
تحتوي tidyr على بضع الدوال النافعة الأخرى الخاصة بتحويل البيانات إلى صيغة مستطيلة لن نغطيها في هذا الكتاب:
-
unnest_auto()تختار تلقائياً بينunnest_longer()وunnest_wider()بناءً على هيكل عمود القائمة. إنها ممتازة للاستكشاف السريع، ولكنها فكرة سيئة في نهاية المطاف لأنها لا تجبرك على فهم كيفية هيكلة بياناتك، وتجعل كودك أسرع صعوبة في الفهم. -
unnest()توسع كلاً من الصفوف والأعمدة. إنها مفيدة عندما يكون لديك عمود قائمة يحتوي على هيكل ثنائي الأبعاد مثل إطار بيانات، وهو ما لا تراه في هذا الكتاب، ولكن قد تصادفه إذا كنت تستخدم بيئة tidymodels.
من الجيد معرفة هذه الدوال حيث قد تصادفها عند قراءة كود أشخاص آخرين أو عند معالجة تحديات أكثر ندرة في تحويل البيانات إلى صيغة مستطيلة بنفسك.
23.3.5 تمارين
ماذا يحدث عندما تستخدم
unnest_wider()مع أعمدة القوائم غير المسماة مثلdf2؟ ما المعامل الذي يصبح ضرورياً الآن؟ ماذا يحدث للقيم المفقودة؟ماذا يحدث عندما تستخدم
unnest_longer()مع أعمدة القوائم المسماة مثلdf1؟ ما هي المعلومات الإضافية التي تحصل عليها في المخرجات؟ كيف يمكنك إخفاء تلك التفاصيل الإضافية؟-
من وقت لآخر تصادف أطر بيانات مع أعمدة قوائم متعددة ذات قيم متحاذية. على سبيل المثال، في إطار البيانات التالي، قيم
yوzمتحاذية (أي أنyوzسيكون لهما دائماً نفس الطول داخل الصف، والقيمة الأولى لـyتتوافق مع القيمة الأولى لـz). ماذا يحدث إذا طبقت استدعائين لـunnest_longer()على إطار البيانات هذا؟ كيف يمكنك الحفاظ على العلاقة بينxوy؟ (تلميح: اقرأ التوثيق بعناية).
23.4 دراسات حالة
الفرق الرئيسي بين الأمثلة البسيطة التي استخدمناها أعلاه والبيانات الحقيقية هو أن البيانات الحقيقية تحتوي عادةً على مستويات متعددة من التداخل تتطلب استدعاءات متعددة لـ unnest_longer() و/أو unnest_wider(). لإظهار ذلك عملياً، يعمل هذا القسم من خلال ثلاثة تحديات حقيقية تحويل البيانات إلى صيغة مستطيلة باستخدام مجموعات بيانات من حزمة repurrrsive.
23.4.1 بيانات عريضة جداً
سنبدأ بـ gh_repos. هذه قائمة تحتوي على بيانات حول مجموعة من مستودعات GitHub المجلوبة باستخدام واجهة برمجة تطبيقات GitHub (API). إنها قائمة متداخلة بعمق كبير لذا من الصعب إظهار الهيكل في هذا الكتاب؛ نوصي بالاستكشاف قليلاً بمفردك مع View(gh_repos) قبل أن نواصل.
تُعد gh_repos قائمة، ولكن أدواتنا تعمل مع أعمدة القوائم، لذا سنبدأ بوضعها في tibble. نسمي هذا العمود json لأسباب سنصل إليها لاحقاً.
repos <- tibble(json = gh_repos)
repos
#> # A tibble: 6 × 1
#> json
#> <list>
#> 1 <list [30]>
#> 2 <list [30]>
#> 3 <list [30]>
#> 4 <list [26]>
#> 5 <list [30]>
#> 6 <list [30]>تحتوي هذه الـ tibble على 6 صفوف، صف واحد لكل ابن من أبناء gh_repos. يحتوي كل صف على قائمة غير مسماة تضم إما 26 أو 30 صفاً. نظراً لأن هذه القوائم غير مسماة، سنبدأ بـ unnest_longer() لوضع كل ابن في صفه الخاص:
repos |>
unnest_longer(json)
#> # A tibble: 176 × 1
#> json
#> <list>
#> 1 <named list [68]>
#> 2 <named list [68]>
#> 3 <named list [68]>
#> 4 <named list [68]>
#> 5 <named list [68]>
#> 6 <named list [68]>
#> # ℹ 170 more rowsللوهلة الأولى، قد يبدو أننا لم نحسن الموقف: بينما لدينا صفوف أكثر (176 بدلاً من 6)، إلا أن كل عنصر من عناصر json ما زال قائمة. ومع ذلك، هناك فرق مهم: الآن كل عنصر هو قائمة مسماة لذا يمكننا استخدام unnest_wider() لوضع كل عنصر في عموده الخاص:
repos |>
unnest_longer(json) |>
unnest_wider(json)
#> # A tibble: 176 × 68
#> id name full_name owner private html_url
#> <int> <chr> <chr> <list> <lgl> <chr>
#> 1 61160198 after gaborcsardi/after <named list> FALSE https://github…
#> 2 40500181 argufy gaborcsardi/argu… <named list> FALSE https://github…
#> 3 36442442 ask gaborcsardi/ask <named list> FALSE https://github…
#> 4 34924886 baseimports gaborcsardi/base… <named list> FALSE https://github…
#> 5 61620661 citest gaborcsardi/cite… <named list> FALSE https://github…
#> 6 33907457 clisymbols gaborcsardi/clis… <named list> FALSE https://github…
#> # ℹ 170 more rows
#> # ℹ 62 more variables: description <chr>, fork <lgl>, url <chr>, …لقد نجح هذا ولكن النتيجة ساحقة قليلاً: هناك الكثير من الأعمدة لدرجة أن tibble لا تطبعها جميعاً! يمكننا رؤيتها جميعاً باستخدام names()؛ وهنا ننظر إلى أول 10 منها:
repos |>
unnest_longer(json) |>
unnest_wider(json) |>
names() |>
head(10)
#> [1] "id" "name" "full_name" "owner" "private"
#> [6] "html_url" "description" "fork" "url" "forks_url"دعنا نسحب القليل منها مما يبدو ممتعاً:
repos |>
unnest_longer(json) |>
unnest_wider(json) |>
select(id, full_name, owner, description)
#> # A tibble: 176 × 4
#> id full_name owner description
#> <int> <chr> <list> <chr>
#> 1 61160198 gaborcsardi/after <named list [17]> Run Code in the Backgro…
#> 2 40500181 gaborcsardi/argufy <named list [17]> Declarative function ar…
#> 3 36442442 gaborcsardi/ask <named list [17]> Friendly CLI interactio…
#> 4 34924886 gaborcsardi/baseimports <named list [17]> Do we get warnings for …
#> 5 61620661 gaborcsardi/citest <named list [17]> Test R package and repo…
#> 6 33907457 gaborcsardi/clisymbols <named list [17]> Unicode symbols for CLI…
#> # ℹ 170 more rowsيمكنك استخدام هذا للعمل العكسي وفهم كيف تم هيكلة gh_repos: كان كل ابن عبارة عن مستخدم GitHub يحتوي على قائمة تضم ما يصل إلى 30 مستودعاً من مستودعات GitHub التي تم إنشاؤها.
يمثل owner عمود قائمة آخر، وبما أنه يحتوي على قائمة مسماة، فيمكننا استخدام unnest_wider() للوصول إلى القيم:
repos |>
unnest_longer(json) |>
unnest_wider(json) |>
select(id, full_name, owner, description) |>
unnest_wider(owner)
#> Error in `unnest_wider()`:
#> ! Can't duplicate names between the affected columns and the original
#> data.
#> ✖ These names are duplicated:
#> ℹ `id`, from `owner`.
#> ℹ Use `names_sep` to disambiguate using the column name.
#> ℹ Or use `names_repair` to specify a repair strategy.أوه أوه، يحتوي عمود القائمة هذا أيضاً على عمود باسم id ولا يمكننا امتلاك عمودين باسم id في نفس إطار البيانات. كما هو مقترح، دعنا نستخدم names_sep لحل المشكلة:
repos |>
unnest_longer(json) |>
unnest_wider(json) |>
select(id, full_name, owner, description) |>
unnest_wider(owner, names_sep = "_")
#> # A tibble: 176 × 20
#> id full_name owner_login owner_id owner_avatar_url
#> <int> <chr> <chr> <int> <chr>
#> 1 61160198 gaborcsardi/after gaborcsardi 660288 https://avatars.gith…
#> 2 40500181 gaborcsardi/argufy gaborcsardi 660288 https://avatars.gith…
#> 3 36442442 gaborcsardi/ask gaborcsardi 660288 https://avatars.gith…
#> 4 34924886 gaborcsardi/baseimports gaborcsardi 660288 https://avatars.gith…
#> 5 61620661 gaborcsardi/citest gaborcsardi 660288 https://avatars.gith…
#> 6 33907457 gaborcsardi/clisymbols gaborcsardi 660288 https://avatars.gith…
#> # ℹ 170 more rows
#> # ℹ 15 more variables: owner_gravatar_id <chr>, owner_url <chr>, …يعطينا هذا مجموعة بيانات عريضة أخرى، ولكن يمكنك إدراك أن owner يبدو أنه يحتوي على الكثير من البيانات الإضافية حول الشخص الذي “يمتلك” المستودع.
23.4.2 البيانات العلاقاتية (Relational data)
تُستخدم البيانات المتداخلة أحياناً لتمثيل البيانات التي نوزعها عادة عبر أطر بيانات متعددة. على سبيل المثال، خذ got_chars التي تحتوي على بيانات حول الشخصيات التي تظهر في كتب ومسلسل “Game of Thrones”. مثل gh_repos فإنها عبارة عن قائمة، لذا نبدأ بتحويلها إلى عمود قائمة داخل tibble:
chars <- tibble(json = got_chars)
chars
#> # A tibble: 30 × 1
#> json
#> <list>
#> 1 <named list [18]>
#> 2 <named list [18]>
#> 3 <named list [18]>
#> 4 <named list [18]>
#> 5 <named list [18]>
#> 6 <named list [18]>
#> # ℹ 24 more rowsيحتوي العمود json على عناصر مسماة، لذا سنبدأ بتوسيعها عرضياً (widening):
chars |>
unnest_wider(json)
#> # A tibble: 30 × 18
#> url id name gender culture born
#> <chr> <int> <chr> <chr> <chr> <chr>
#> 1 https://www.anapio… 1022 Theon Greyjoy Male "Ironborn" "In 278 AC or …
#> 2 https://www.anapio… 1052 Tyrion Lannist… Male "" "In 273 AC, at…
#> 3 https://www.anapio… 1074 Victarion Grey… Male "Ironborn" "In 268 AC or …
#> 4 https://www.anapio… 1109 Will Male "" ""
#> 5 https://www.anapio… 1166 Areo Hotah Male "Norvoshi" "In 257 AC or …
#> 6 https://www.anapio… 1267 Chett Male "" "At Hag's Mire"
#> # ℹ 24 more rows
#> # ℹ 12 more variables: died <chr>, alive <lgl>, titles <list>, …ثم نحدد بضعة أعمدة لتسهيل القراءة:
characters <- chars |>
unnest_wider(json) |>
select(id, name, gender, culture, born, died, alive)
characters
#> # A tibble: 30 × 7
#> id name gender culture born died
#> <int> <chr> <chr> <chr> <chr> <chr>
#> 1 1022 Theon Greyjoy Male "Ironborn" "In 278 AC or 27… ""
#> 2 1052 Tyrion Lannister Male "" "In 273 AC, at C… ""
#> 3 1074 Victarion Greyjoy Male "Ironborn" "In 268 AC or be… ""
#> 4 1109 Will Male "" "" "In 297 AC, at…
#> 5 1166 Areo Hotah Male "Norvoshi" "In 257 AC or be… ""
#> 6 1267 Chett Male "" "At Hag's Mire" "In 299 AC, at…
#> # ℹ 24 more rows
#> # ℹ 1 more variable: alive <lgl>تحتوي مجموعة البيانات هذه أيضاً على العديد من أعمدة القوائم:
chars |>
unnest_wider(json) |>
select(id, where(is.list))
#> # A tibble: 30 × 8
#> id titles aliases allegiances books povBooks tvSeries playedBy
#> <int> <list> <list> <list> <list> <list> <list> <list>
#> 1 1022 <chr [2]> <chr [4]> <chr [1]> <chr [3]> <chr> <chr> <chr>
#> 2 1052 <chr [2]> <chr [11]> <chr [1]> <chr [2]> <chr> <chr> <chr>
#> 3 1074 <chr [2]> <chr [1]> <chr [1]> <chr [3]> <chr> <chr> <chr>
#> 4 1109 <chr [1]> <chr [1]> <NULL> <chr [1]> <chr> <chr> <chr>
#> 5 1166 <chr [1]> <chr [1]> <chr [1]> <chr [3]> <chr> <chr> <chr>
#> 6 1267 <chr [1]> <chr [1]> <NULL> <chr [2]> <chr> <chr> <chr>
#> # ℹ 24 more rowsدعنا نستكشف العمود titles (الألقاب). إنه عمود قائمة غير مسمى، لذا سنقوم بفك تسطيره طولياً إلى صفوف:
chars |>
unnest_wider(json) |>
select(id, titles) |>
unnest_longer(titles)
#> # A tibble: 59 × 2
#> id titles
#> <int> <chr>
#> 1 1022 Prince of Winterfell
#> 2 1022 Lord of the Iron Islands (by law of the green lands)
#> 3 1052 Acting Hand of the King (former)
#> 4 1052 Master of Coin (former)
#> 5 1074 Lord Captain of the Iron Fleet
#> 6 1074 Master of the Iron Victory
#> # ℹ 53 more rowsقد تتوقع رؤية هذه البيانات في جدول مستقل خاص بها لأنه سيكون من السهل ربطها ببيانات الشخصيات حسب الحاجة. دعنا نفعل ذلك، وهو ما يتطلب تنظيفاً بسيطاً: إزالة الصفوف التي تحتوي على نصوص فارغة وإعادة تسمية titles إلى title نظراً لأن كل صف يحتوي الآن على لقب واحد فقط.
titles <- chars |>
unnest_wider(json) |>
select(id, titles) |>
unnest_longer(titles) |>
filter(titles != "") |>
rename(title = titles)
titles
#> # A tibble: 52 × 2
#> id title
#> <int> <chr>
#> 1 1022 Prince of Winterfell
#> 2 1022 Lord of the Iron Islands (by law of the green lands)
#> 3 1052 Acting Hand of the King (former)
#> 4 1052 Master of Coin (former)
#> 5 1074 Lord Captain of the Iron Fleet
#> 6 1074 Master of the Iron Victory
#> # ℹ 46 more rowsيمكنك تخيل إنشاء جدول مثل هذا لكل عمود من أعمدة القوائم، ثم استخدام عمليات الربط (joins) لدمجها مع بيانات الشخصية حسب حاجتك.
23.4.3 التداخل العميق (Deeply nested)
سنختتم دراسات الحالة هذه بعمود قائمة متداخل بعمق شديد ويتطلب جولات متكررة من unnest_wider() و unnest_longer() لفكه: gmaps_cities. هذه عبارة عن tibble من عمودين تحتوي على أسماء خمس مدن ونتائج استخدام واجهة برمجة تطبيقات الترميز الجغرافي من Google geocoding API لتحديد مواقعها:
gmaps_cities
#> # A tibble: 5 × 2
#> city json
#> <chr> <list>
#> 1 Houston <named list [2]>
#> 2 Washington <named list [2]>
#> 3 New York <named list [2]>
#> 4 Chicago <named list [2]>
#> 5 Arlington <named list [2]>json هو عمود قائمة بأسماء داخلية، لذا نبدأ باستدعاء unnest_wider():
gmaps_cities |>
unnest_wider(json)
#> # A tibble: 5 × 3
#> city results status
#> <chr> <list> <chr>
#> 1 Houston <list [1]> OK
#> 2 Washington <list [2]> OK
#> 3 New York <list [1]> OK
#> 4 Chicago <list [1]> OK
#> 5 Arlington <list [2]> OKيعطينا هذا الحالة status والنتائج results. سنحذف عمود الحالة نظراً لأنها جميعاً OK؛ وفي التحليل الحقيقي، سترغب أيضاً في التقاط جميع الصفوف التي تكون فيها status != "OK" لمعرفة الخطأ الذي حدث. results هي قائمة غير مسماة، تحتوي على عنصر واحد أو عنصرين (سنرى السبب بعد قليل) لذا سنفك تسطيرها طولياً إلى صفوف:
gmaps_cities |>
unnest_wider(json) |>
select(-status) |>
unnest_longer(results)
#> # A tibble: 7 × 2
#> city results
#> <chr> <list>
#> 1 Houston <named list [5]>
#> 2 Washington <named list [5]>
#> 3 Washington <named list [5]>
#> 4 New York <named list [5]>
#> 5 Chicago <named list [5]>
#> 6 Arlington <named list [5]>
#> # ℹ 1 more rowالآن أصبحت results قائمة مسماة، لذا سنستخدم unnest_wider():
locations <- gmaps_cities |>
unnest_wider(json) |>
select(-status) |>
unnest_longer(results) |>
unnest_wider(results)
locations
#> # A tibble: 7 × 6
#> city address_components formatted_address geometry
#> <chr> <list> <chr> <list>
#> 1 Houston <list [4]> Houston, TX, USA <named list [4]>
#> 2 Washington <list [2]> Washington, USA <named list [4]>
#> 3 Washington <list [4]> Washington, DC, USA <named list [4]>
#> 4 New York <list [3]> New York, NY, USA <named list [4]>
#> 5 Chicago <list [4]> Chicago, IL, USA <named list [4]>
#> 6 Arlington <list [4]> Arlington, TX, USA <named list [4]>
#> # ℹ 1 more row
#> # ℹ 2 more variables: place_id <chr>, types <list>الآن يمكننا أن نرى لماذا حصلت مدينتان على نتيجتين: طابقت واشنطن كلاً من ولاية واشنطن وواشنطن العاصمة، وطابقت أرلينغتون كلاً من أرلينغتون بفرجينيا وأرلينغتون بتكساس.
هناك بضعة مسارات مختلفة يمكننا سلوكها من هنا. قد نرغب في تحديد الموقع الدقيق للمطابقة، والمخزن في عمود القائمة geometry:
locations |>
select(city, formatted_address, geometry) |>
unnest_wider(geometry)
#> # A tibble: 7 × 6
#> city formatted_address bounds location location_type
#> <chr> <chr> <list> <list> <chr>
#> 1 Houston Houston, TX, USA <named list [2]> <named list> APPROXIMATE
#> 2 Washington Washington, USA <named list [2]> <named list> APPROXIMATE
#> 3 Washington Washington, DC, USA <named list [2]> <named list> APPROXIMATE
#> 4 New York New York, NY, USA <named list [2]> <named list> APPROXIMATE
#> 5 Chicago Chicago, IL, USA <named list [2]> <named list> APPROXIMATE
#> 6 Arlington Arlington, TX, USA <named list [2]> <named list> APPROXIMATE
#> # ℹ 1 more row
#> # ℹ 1 more variable: viewport <list>يعطينا ذلك الحدود bounds الجديدة (منطقة مستطيلة) والموقع location (نقطة). يمكننا فك تسطير location لرؤية خط العرض (lat) وخط الطول (lng):
locations |>
select(city, formatted_address, geometry) |>
unnest_wider(geometry) |>
unnest_wider(location)
#> # A tibble: 7 × 7
#> city formatted_address bounds lat lng location_type
#> <chr> <chr> <list> <dbl> <dbl> <chr>
#> 1 Houston Houston, TX, USA <named list [2]> 29.8 -95.4 APPROXIMATE
#> 2 Washington Washington, USA <named list [2]> 47.8 -121. APPROXIMATE
#> 3 Washington Washington, DC, USA <named list [2]> 38.9 -77.0 APPROXIMATE
#> 4 New York New York, NY, USA <named list [2]> 40.7 -74.0 APPROXIMATE
#> 5 Chicago Chicago, IL, USA <named list [2]> 41.9 -87.6 APPROXIMATE
#> 6 Arlington Arlington, TX, USA <named list [2]> 32.7 -97.1 APPROXIMATE
#> # ℹ 1 more row
#> # ℹ 1 more variable: viewport <list>يتطلب استخراج الحدود بضع خطوات إضافية:
locations |>
select(city, formatted_address, geometry) |>
unnest_wider(geometry) |>
# التركيز على المتغيرات المترغوبة
select(!location:viewport) |>
unnest_wider(bounds)
#> # A tibble: 7 × 4
#> city formatted_address northeast southwest
#> <chr> <chr> <list> <list>
#> 1 Houston Houston, TX, USA <named list [2]> <named list [2]>
#> 2 Washington Washington, USA <named list [2]> <named list [2]>
#> 3 Washington Washington, DC, USA <named list [2]> <named list [2]>
#> 4 New York New York, NY, USA <named list [2]> <named list [2]>
#> 5 Chicago Chicago, IL, USA <named list [2]> <named list [2]>
#> 6 Arlington Arlington, TX, USA <named list [2]> <named list [2]>
#> # ℹ 1 more rowثم نعيد تسمية southwest (الجنوب الغربي) و northeast (الشمال الشرقي) -وهي أركان المستطيل- حتى نتمكن من استخدام names_sep لإنشاء أسماء قصيرة ومعبرة:
locations |>
select(city, formatted_address, geometry) |>
unnest_wider(geometry) |>
select(!location:viewport) |>
unnest_wider(bounds) |>
rename(ne = northeast, sw = southwest) |>
unnest_wider(c(ne, sw), names_sep = "_")
#> # A tibble: 7 × 6
#> city formatted_address ne_lat ne_lng sw_lat sw_lng
#> <chr> <chr> <dbl> <dbl> <dbl> <dbl>
#> 1 Houston Houston, TX, USA 30.1 -95.0 29.5 -95.8
#> 2 Washington Washington, USA 49.0 -117. 45.5 -125.
#> 3 Washington Washington, DC, USA 39.0 -76.9 38.8 -77.1
#> 4 New York New York, NY, USA 40.9 -73.7 40.5 -74.3
#> 5 Chicago Chicago, IL, USA 42.0 -87.5 41.6 -87.9
#> 6 Arlington Arlington, TX, USA 32.8 -97.0 32.6 -97.2
#> # ℹ 1 more rowلاحظ كيف نقوم بفك تسطير عمودين متزامنين عن طريق تزويد متجه لأسماء المتغيرات إلى unnest_wider().
بمجرد اكتشاف المسار للوصول إلى المكونات التي تهتم بها، يمكنك استخراجها مباشرة باستخدام دالة أخرى من tidyr وهي hoist():
إذا كانت دراسات الحالة هذه قد أثارت شغفك للمزيد من تطبيقات الصيغة المستطيلة للبيانات في الحياة الواقعية، يمكنك رؤية المزيد من الأمثلة في vignette("rectangling", package = "tidyr").
23.4.4 تمارين
قدر بشكل تقريبي متى تم إنشاء
gh_repos. لماذا يمكنك تقدير التاريخ بشكل تقريبي فقط؟يحتوي العمود
ownerفيgh_reposعلى الكثير من المعلومات المكررة لأن كل مالك يمكن أن يمتلك العديد من المستودعات. هل يمكنك إنشاء إطار بيانات باسمownersيحتوي على صف واحد لكل مالك؟ (تلميح: هل تعملdistinct()مع أعمدة القوائمlist-cols؟)اتبع الخطوات المستخدمة مع
titlesلإنشاء جداول مماثلة للأسماء المستعارة (aliases)، والولاءات (allegiances)، والكتب (books)، والمسلسلات التلفزيونية (TV series) لشخصيات Game of Thrones.-
اشرح الكود التالي سطرًا بسطر. لماذا يثير الاهتمام؟ لماذا يعمل مع
got_charsولكنه قد لا يعمل بشكل عام؟tibble(json = got_chars) |> unnest_wider(json) |> select(id, where(is.list)) |> pivot_longer( where(is.list), names_to = "name", values_to = "value" ) |> unnest_longer(value) في
gmaps_cities، ماذا يحتويaddress_components؟ لماذا يتفاوت الطول بين الصفوف؟ قم بفك تسطيره بشكل مناسب لاكتشاف ذلك. (تلميح: يظهر أنtypesيحتوي دائماً على عنصرين. هل تجعلunnest_wider()العمل معه أسهل مقارنة بـunnest_longer()؟)
23.5 JSON
جميع دراسات الحالة في القسم السابق تم جلبها من صيغ JSON واقعية. JSON هي اختصار لـ JavaScript Object Notation وهي الطريقة التي تعيد بها معظم واجهات برمجة تطبيقات الويب (APIs) البيانات. من المهم فهمها لأنه على الرغم من أن أنواع البيانات في JSON و R متشابهة جداً، إلا أنه لا يوجد تطابق كلي بنسبة 1 إلى 1، لذا من الجيد فهم القليل عن JSON إذا ساءت الأمور.
23.5.1 أنواع البيانات
JSON هي صيغة بسيطة مصممة ليسهل قراءتها وكتابتها بواسطة الآلات، وليس البشر. تحتوي على ستة أنواع رئيسية من البيانات. أربعة منها قياسية (scalars):
- أبسط نوع هو الفارغ (
null) والذي يؤدي نفس دورNAفي R. ويمثل غياب البيانات. - السلسلة النصية (string) تشبه إلى حد كبير النص في R، ولكن يجب دائماً استخدام علامات التنصيص المزدوجة.
-
الرقم (number) يشبه الأرقام في R: يمكن استخدام الأعداد الصحيحة (مثل 123)، أو العشرية (مثل 123.45)، أو التدوين العلمي (مثل 1.23e3). لا تدعم JSON القيم
Infأو-InfأوNaN. -
القيمة المنطقية (boolean) تشبه
TRUEوFALSEفي R، ولكنها تستخدم أحرفاً صغيرةtrueوfalse.
تعد النصوص والأرقام والقيم المنطقية في JSON متشابهة جداً مع المتجهات النصية والعدذية والمنطقية في R. الفرق الرئيسي هو أن القيم القياسية في JSON يمكنها تمثيل قيمة واحدة فقط. ولتمثيل قيم متعددة تحتاج إلى استخدام أحد النوعين المتبقيين: المصفوفات المتعددة الأبعاد (arrays) والكائنات (objects).
تتشابه كلٌّ من المصفوفات المتعددة الأبعاد (Arrays) والكائنات (Objects) مع القوائم (Lists) في لغة R؛ ويكمن الاختلاف في كونها مُسماة (Named) أم لا. وتُشبه المصفوفة المتعددة الأبعاد (array) القائمة غير المُسماة، وتُكتب باستخدام الأقواس المعقوفة []. على سبيل المثال، تُعد [1, 2, 3] مصفوفة تحتوي على ثلاثة أرقام، بينما تُعد [null, 1, "string", false] مصفوفة تحتوي على قيمة فارغة (null)، ورقم، وسلسلة نصية (string)، وقيمة بولينية (boolean). أما الكائن (object) فيُشبه القائمة المُسماة، ويُكتب باستخدام الأقواس المعقوفة المزدوجة {}. وتكون الأسماء (أو المفاتيح Keys بترميز JSON) عبارة عن سلاسل نصية، لذا يجب إحاطتها بعلامات تنصيص. على سبيل المثال، يُعد {"x": 1, "y": 2} كائناً يربط x بالقيمة 1 و y بالقيمة 2.
لاحظ أنه لا توجد طريقة أصلية في JSON لتمثيل التواريخ أو الأوقات، لذا غالباً ما يتم تخزينها كنصوص، وستحتاج إلى استخدام readr::parse_date() أو readr::parse_datetime() لتحويلها إلى بنية البيانات الصحيحة. وبالمثل، فإن قواعد JSON لتمثيل أرقام النقطة العائمة (floating point numbers) غير دقيقة قليلاً، لذا ستجد أحياناً أرقاماً مخزنة في نصوص. طبق readr::parse_double() حسب الحاجة للحصول على نوع المتغير الصحيح.
23.5.2 jsonlite
لتحويل JSON إلى بنيات بيانات في R، نوصي بحزمة jsonlite للمطور Jeroen Ooms. سنستخدم دالتين فقط من jsonlite وهما: read_json() و parse_json(). في الحياة الواقعية، ستستخدم read_json() لقراءة ملف JSON من القرص. على سبيل المثال، توفر حزمة repurrsive أيضاً المصدر لـ gh_user كملف JSON ويمكنك قراءته باستخدام read_json():
# مسار لملف json داخل الحزمة:
gh_users_json()
#> [1] "/home/runner/work/_temp/Library/repurrrsive/extdata/gh_users.json"
# قراءته باستخدام read_json()
gh_users2 <- read_json(gh_users_json())
# التحقق من أنه مطابق للبيانات التي كنا نستخدمها سابقاً
identical(gh_users, gh_users2)
#> [1] TRUEفي هذا الكتاب، سنستخدم أيضاً parse_json()، لأنها تأخذ نصاً يحتوي على JSON، مما يجعلها جيدة لتوليد أمثلة بسيطة. للبدء، إليك ثلاث مجموعات بيانات JSON بسيطة، تبدأ برقم، ثم وضع بضعة أرقام في مصفوفة، ثم وضع تلك المصفوفة في كائن:
str(parse_json('1'))
#> int 1
str(parse_json('[1, 2, 3]'))
#> List of 3
#> $ : int 1
#> $ : int 2
#> $ : int 3
str(parse_json('{"x": [1, 2, 3]}'))
#> List of 1
#> $ x:List of 3
#> ..$ : int 1
#> ..$ : int 2
#> ..$ : int 3تحتوي jsonlite على دالة مهمة أخرى تسمى fromJSON(). نحن لا نستخدمها هنا لأنها تقوم بالتبسيط التلقائي (simplifyVector = TRUE). غالباً ما يعمل هذا بشكل جيد، خاصة في الحالات البسيطة، ولكننا نعتقد أنه من الأفضل أن تقوم بتحويل البيانات إلى صيغة مستطيلة بنفسك حتى تعرف بالضبط ما يحدث ويمكنك التعامل بمرونة أكبر مع الهياكل المتداخلة الأكثر تعقيداً.
23.5.3 بدء عملية تسوية البيانات في جداول مستطيلة
في معظم الحالات، تحتوي ملفات JSON على مصفوفة واحدة في المستوى الأعلى، لأنها مصممة لتقديم بيانات حول “أشياء” متعددة، مثل صفحات متعددة، أو سجلات متعددة، أو نتائج متعددة. في هذه الحالة، ستشرع في تحويل البيانات إلى صيغة مستطيلة باستخدام tibble(json) بحيث يصبح كل عنصر صفاً:
json <- '[
{"name": "John", "age": 34},
{"name": "Susan", "age": 27}
]'
df <- tibble(json = parse_json(json))
df
#> # A tibble: 2 × 1
#> json
#> <list>
#> 1 <named list [2]>
#> 2 <named list [2]>
df |>
unnest_wider(json)
#> # A tibble: 2 × 2
#> name age
#> <chr> <int>
#> 1 John 34
#> 2 Susan 27في حالات أندر، يتكون ملف JSON من كائن JSON واحد في المستوى الأعلى، يمثل “شيئاً” واحداً. في هذه الحالة، ستحتاج إلى بدء عملية تحويل البيانات إلى صيغة مستطيلة بتغليفه داخل قائمة، قبل وضعه في tibble.
json <- '{
"status": "OK",
"results": [
{"name": "John", "age": 34},
{"name": "Susan", "age": 27}
]
}
'
df <- tibble(json = list(parse_json(json)))
df
#> # A tibble: 1 × 1
#> json
#> <list>
#> 1 <named list [2]>
df |>
unnest_wider(json) |>
unnest_longer(results) |>
unnest_wider(results)
#> # A tibble: 2 × 3
#> status name age
#> <chr> <chr> <int>
#> 1 OK John 34
#> 2 OK Susan 27بدلاً من ذلك، يمكنك الوصول إلى داخل JSON المحلل والبدء بالجزء الذي تهتم به بالفعل:
df <- tibble(results = parse_json(json)$results)
df |>
unnest_wider(results)
#> # A tibble: 2 × 2
#> name age
#> <chr> <int>
#> 1 John 34
#> 2 Susan 2723.5.4 تمارين
-
قم بتحويل البيانات إلى صيغة مستطيلة
df_colوdf_rowأدناه. إنهما يمثلان الطريقتين لترميز إطار البيانات في JSON.json_col <- parse_json(' { "x": ["a", "x", "z"], "y": [10, null, 3] } ') json_row <- parse_json(' [ {"x": "a", "y": 10}, {"x": "x", "y": null}, {"x": "z", "y": 3} ] ') df_col <- tibble(json = list(json_col)) df_row <- tibble(json = json_row)
23.6 ملخص
في هذا الفصل، تعلمت ما هي القوائم، وكيف يمكنك توليدها من ملفات JSON، وكيفية تحويلها إلى أطر بيانات مستطيلة. ومن المدهش أننا احتجنا فقط إلى دالتين جديدتين: unnest_longer() لوضع عناصر القائمة في صفوف و unnest_wider() لوضع عناصر القائمة في أعمدة. لا يهم مدى عمق تداخل عمود القائمة؛ كل ما عليك فعله هو استدعاء هاتين الدالتين تكراراً.
صيغة JSON هي أكثر صيغ البيانات شيوعاً التي تعيدها واجهات برمجة تطبيقات الويب. ماذا يحدث إذا لم يكن للموقع الإلكتروني واجهة برمجة تطبيقات، ولكن يمكنك رؤية البيانات التي تريدها على الموقع؟ هذا هو موضوع الفصل التالي: كشط الويب (web scraping)، واستخراج البيانات من صفحات HTML.
هذه ميزة خاصة بـ RStudio.↩︎