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(x1)
#> List of 3
#>  $ : int [1:4] 1 2 3 4
#>  $ : chr "a"
#>  $ : logi TRUE
str(x2)
#> List of 3
#>  $ a: int [1:2] 1 2
#>  $ b: int [1:3] 1 2 3
#>  $ c: int [1:4] 1 2 3 4

كما ترى، تعرض str() كل ابن من أبناء القائمة في سطر مستقل. حيث تعرض الاسم -إن وجد- ثم اختصاراً للنوع، ثم القيم القليلة الأولى.

23.2.1 الهيكلية الهرمية (Hierarchy)

يمكن للقوائم أن تحتوي على أي نوع من الكائنات، بما في ذلك قوائم أخرى. هذا يجعلها مناسبة لتمثيل الهياكل الهرمية (الشجرية):

x3 <- list(list(1, 2), list(3, 4))
str(x3)
#> List of 2
#>  $ :List of 2
#>   ..$ : num 1
#>   ..$ : num 2
#>  $ :List of 2
#>   ..$ : num 3
#>   ..$ : num 4

هذا يختلف بشكل ملحوظ عن c()، التي تولد متجهاً مسطحاً:

c(c(1, 2), c(3, 4))
#> [1] 1 2 3 4

x4 <- c(list(1, 2), list(3, 4))
str(x4)
#> List of 4
#>  $ : num 1
#>  $ : num 2
#>  $ : num 3
#>  $ : num 4

كلما أصبحت القوائم أكثر تعقيداً، أصبحت str() أكثر فائدة، لأنها تتيح لك رؤية الهيكل الهرمي بنظرة واحدة:

x5 <- list(1, list(2, list(3, list(4, list(5)))))
str(x5)
#> List of 2
#>  $ : num 1
#>  $ :List of 2
#>   ..$ : num 2
#>   ..$ :List of 2
#>   .. ..$ : num 3
#>   .. ..$ :List of 2
#>   .. .. ..$ : num 4
#>   .. .. ..$ :List of 1
#>   .. .. .. ..$ : num 5

مع كبر حجم القوائم وازدياد تعقيدها، تبدأ str() في الفشل في نهاية المطاف، وستحتاج إلى الانتقال إلى استخدام View()1. يعرض الشكل 23.1 نتيجة استدعاء View(x5). يبدأ المستعرض باظهار المستوى الأعلى فقط من القائمة، ولكن يمكنك توسيع أي من المكونات تفاعلياً لرؤية المزيد، كما في الشكل 23.2. سيظهر لك RStudio أيضاً الكود الذي تحتاجه للوصول إلى ذلك العنصر المحدد، كما في الشكل 23.3. سنعود إلى كيفية عمل هذا الكود في قسم 27.3.

لقطة شاشة لـ RStudio تظهر مستعرض القوائم. يظهر الابنين لـ x5: الابن الأول هو متجه مزدوج والابن الثاني هو قائمة. يشير المثلث المتجه لليمين إلى أن الابن الثاني لديه أبناء ولكن لا يمكنك رؤيتهم حالياً.
الشكل 23.1: يتيح لك مستعرض RStudio استكشاف القوائم المعقدة تفاعلياً. يفتح المستعرض مع إظهار المستوى الأعلى من القائمة فقط.
لقطة شاشة أخرى لمستعرض القوائم بعد توسيع الابن الثاني لـ x5. ولديه أيضاً ابنان، متجه مزدوج وقائمة أخرى.
الشكل 23.2: يؤدي النقر على المثلث المتجه لليمين إلى توسيع ذلك المكون من القائمة حتى تتمكن من رؤية أبنائه أيضاً.
لقطة شاشة أخرى بعد توسيع حفيد x5 لرؤية ابنيه، وهما مجدداً متجه مزدوج وقائمة.
الشكل 23.3: يمكنك تكرار هذه العملية عدة مرات حسب الحاجة للوصول إلى البيانات التي تهتم بها. لاحظ الزاوية السفلية اليسرى: إذا قمت بالنقر فوق عنصر من القائمة، سيعطيك RStudio كود التفرع المطلوب للوصول إليه، في هذه الحالة x5[[2]][[2]][[2]].

23.2.2 أعمدة القوائم (List-columns)

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

إليك مثالاً بسيطاً على أعمدة القوائم:

df <- tibble(
  x = 1:2, 
  y = c("a", "b"),
  z = list(list(1, 2), list(3, 4, 5))
)
df
#> # A tibble: 2 × 3
#>       x y     z         
#>   <int> <chr> <list>    
#> 1     1 a     <list [2]>
#> 2     2 b     <list [3]>

لا يوجد شيء خاص حول القوائم في 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().

NoteR الأساسية (Base R)

من الممكن وضع قائمة في عمود داخل 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. أعمدة القوائم المسماة تتفكك طبيعياً إلى أعمدة: يصبح كل عنصر مسمى عموداً مسمى جديداً.

df1 <- tribble(
  ~x, ~y,
  1, list(a = 11, b = 12),
  2, list(a = 21, b = 22),
  3, list(a = 31, b = 32),
)

عندما تكون العناصر الأبناء غير مسماة (unnamed)، يميل عدد العناصر إلى التفاوت من صف إلى آخر. على سبيل المثال، في df2، فإن عناصر عمود القائمة y غير مسماة ويتفاوت طولها من واحد إلى ثلاثة. أعمدة القوائم غير المسماة تتفكك طبيعياً إلى صفوف: ستحصل على صف واحد لكل ابن.

df2 <- tribble(
  ~x, ~y,
  1, list(11, 12, 13),
  2, list(21),
  3, list(31, 32),
)

توفر 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 على رقمين، ونص، وقيمة منطقية، وهي عناصر لا يمكن خلطها عادةً في عمود واحد.

df4 <- tribble(
  ~x, ~y,
  "a", list(1),
  "b", list("a", TRUE, 5)
)

تحافظ 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 تمارين

  1. ماذا يحدث عندما تستخدم unnest_wider() مع أعمدة القوائم غير المسماة مثل df2؟ ما المعامل الذي يصبح ضرورياً الآن؟ ماذا يحدث للقيم المفقودة؟

  2. ماذا يحدث عندما تستخدم unnest_longer() مع أعمدة القوائم المسماة مثل df1؟ ما هي المعلومات الإضافية التي تحصل عليها في المخرجات؟ كيف يمكنك إخفاء تلك التفاصيل الإضافية؟

  3. من وقت لآخر تصادف أطر بيانات مع أعمدة قوائم متعددة ذات قيم متحاذية. على سبيل المثال، في إطار البيانات التالي، قيم y و z متحاذية (أي أن y و z سيكون لهما دائماً نفس الطول داخل الصف، والقيمة الأولى لـ y تتوافق مع القيمة الأولى لـ z). ماذا يحدث إذا طبقت استدعائين لـ unnest_longer() على إطار البيانات هذا؟ كيف يمكنك الحفاظ على العلاقة بين x و y؟ (تلميح: اقرأ التوثيق بعناية).

    df4 <- tribble(
      ~x, ~y, ~z,
      "a", list("y-a-1", "y-a-2"), list("z-a-1", "z-a-2"),
      "b", list("y-b-1", "y-b-2", "y-b-3"), list("z-b-1", "z-b-2", "z-b-3")
    )

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():

locations |> 
  select(city, formatted_address, geometry) |> 
  hoist(
    geometry,
    ne_lat = c("bounds", "northeast", "lat"),
    sw_lat = c("bounds", "southwest", "lat"),
    ne_lng = c("bounds", "northeast", "lng"),
    sw_lng = c("bounds", "southwest", "lng"),
  )

إذا كانت دراسات الحالة هذه قد أثارت شغفك للمزيد من تطبيقات الصيغة المستطيلة للبيانات في الحياة الواقعية، يمكنك رؤية المزيد من الأمثلة في vignette("rectangling", package = "tidyr").

23.4.4 تمارين

  1. قدر بشكل تقريبي متى تم إنشاء gh_repos. لماذا يمكنك تقدير التاريخ بشكل تقريبي فقط؟

  2. يحتوي العمود owner في gh_repos على الكثير من المعلومات المكررة لأن كل مالك يمكن أن يمتلك العديد من المستودعات. هل يمكنك إنشاء إطار بيانات باسم owners يحتوي على صف واحد لكل مالك؟ (تلميح: هل تعمل distinct() مع أعمدة القوائم list-cols؟)

  3. اتبع الخطوات المستخدمة مع titles لإنشاء جداول مماثلة للأسماء المستعارة (aliases)، والولاءات (allegiances)، والكتب (books)، والمسلسلات التلفزيونية (TV series) لشخصيات Game of Thrones.

  4. اشرح الكود التالي سطرًا بسطر. لماذا يثير الاهتمام؟ لماذا يعمل مع 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)
  5. في 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    27

23.5.4 تمارين

  1. قم بتحويل البيانات إلى صيغة مستطيلة 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.


  1. هذه ميزة خاصة بـ RStudio.↩︎