5  ترتيب البيانات (Data tidying)

5.1 مقدمة

“العائلات السعيدة كلها متشابهة، ولكن كل عائلة تعيسة تكون تعيسة بطريقتها الخاصة.” — ليو تولستوي

“مجموعات البيانات المرتبة كلها متشابهة، ولكن كل مجموعة بيانات غير مرتبة تكون غير مرتبة بطريقتها الخاصة.” — هادلي ويكهام

في هذا الفصل، ستتعلم طريقة متسقة لتنظيم بياناتك في لغة R باستخدام نظام يسمى البيانات المرتبة (tidy data). يتطلب تنظيم بياناتك في هذا التنسيق بعض الجهد المسبق، ولكن هذا الجهد يؤتي ثماره على المدى الطويل. بمجرد حصولك على بيانات مرتبة والأدوات المرتبة التي توفرها الحزم في منظومة tidyverse، ستقضي وقتاً أقل بكثير في تحويل البيانات من تمثيل إلى آخر، مما يتيح لك قضاء المزيد من الوقت في أسئلة البيانات التي تهتم بها.

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

5.1.1 المتطلبات الأساسية

في هذا الفصل، سنركز على حزمة tidyr، وهي حزمة توفر مجموعة من الأدوات للمساعدة في ترتيب مجموعات البيانات غير المرتبة. تعد حزمة tidyr عضواً في النواة الأساسية لمنظومة tidyverse.

بدءاً من هذا الفصل فصاعداً، سنقوم بحجب رسالة التحميل الصادرة عن استدعاء library(tidyverse).

5.2 البيانات المرتبة (Tidy data)

يمكنك تمثيل نفس البيانات الأساسية بطرق متعددة. يوضح المثال أدناه نفس البيانات منظمّة بثلاث طرق مختلفة. تعرض كل مجموعة بيانات نفس القيم لأربعة متغيرات: الدولة (country)، السنة (year)، السكان (population)، وعدد الحالات (cases) الموثقة لمرض السل، ولكن كل مجموعة بيانات تنظم القيم بطريقة مختلفة.

table1
#> # A tibble: 6 × 4
#>   country      year  cases population
#>   <chr>       <dbl>  <dbl>      <dbl>
#> 1 Afghanistan  1999    745   19987071
#> 2 Afghanistan  2000   2666   20595360
#> 3 Brazil       1999  37737  172006362
#> 4 Brazil       2000  80488  174504898
#> 5 China        1999 212258 1272915272
#> 6 China        2000 213766 1280428583

table2
#> # A tibble: 12 × 4
#>   country      year type           count
#>   <chr>       <dbl> <chr>          <dbl>
#> 1 Afghanistan  1999 cases            745
#> 2 Afghanistan  1999 population  19987071
#> 3 Afghanistan  2000 cases           2666
#> 4 Afghanistan  2000 population  20595360
#> 5 Brazil       1999 cases          37737
#> 6 Brazil       1999 population 172006362
#> # ℹ 6 more rows

table3
#> # A tibble: 6 × 3
#>   country      year rate             
#>   <chr>       <dbl> <chr>            
#> 1 Afghanistan  1999 745/19987071     
#> 2 Afghanistan  2000 2666/20595360    
#> 3 Brazil       1999 37737/172006362  
#> 4 Brazil       2000 80488/174504898  
#> 5 China        1999 212258/1272915272
#> 6 China        2000 213766/1280428583

هذه كلها تمثيلات لنفس البيانات الأساسية، ولكنها ليست متساوية في سهولة الاستخدام. أحدها، وهو table1، سيكون التعامل معه أسهل بكثير داخل منظومة tidyverse لأنه مرتب (tidy).

هناك ثلاث قواعد مترابطة تجعل مجموعة البيانات مرتبة:

  1. كل متغير يمثل عموداً؛ وكل عمود يمثل متغيراً.
  2. كل مشاهدة تمثل صفاً؛ وكل صف يمثل مشاهدة.
  3. كل قيمة تمثل خلية؛ وكل خلية تحتوي على قيمة واحدة فقط.

يوضح الشكل 5.1 هذه القواعد بصرياً.

ثلاث لوحات، كل منها يمثل إطار بيانات مرتب. تظهر اللوحة الأولى أن كل متغير عبارة عن عمود. وتظهر اللوحة الثانية أن كل مشاهدة عبارة عن صف. وتظهر اللوحة الثالثة أن كل قيمة عبارة عن خلية.
الشكل 5.1: القواعد الثلاث التالية تجعل مجموعة البيانات مرتبة: المتغيرات هي أعمدة، والمشاهدات هي صفوف، والقيم هي خلايا.

لماذا نتأكد من أن البيانات مرتبة؟ هناك ميزتان رئيسيتان:

  1. هناك ميزة عامة لاختيار طريقة واحدة متسقة لتخزين البيانات. إذا كان لديك بنية بيانات متسقة، فمن الأسهل تعلم الأدوات التي تعمل معها لأنها تمتلك اتساقاً وتناغماً أساسياً.

  2. هناك ميزة محددة لوضع المتغيرات في أعمدة لأنها تتيح لطبيعة R المتجهة (vectorized) أن تتألق. كما تعلمت في قسم 3.3.1 و قسم 3.5.2، فإن معظم دالات R المدمجة تعمل مع متجهات من القيم. هذا يجعل تحويل البيانات المرتبة يبدو طبيعياً للغاية.

تم تصميم حزم dplyr و ggplot2 وجميع الحزم الأخرى في tidyverse للعمل مع البيانات المرتبة. إليك بعض الأمثلة الصغيرة التي توضح كيف يمكنك العمل مع جدول table1.

# حساب المعدل لكل 10,000
table1 |>
  mutate(rate = cases / population * 10000)
#> # A tibble: 6 × 5
#>   country      year  cases population  rate
#>   <chr>       <dbl>  <dbl>      <dbl> <dbl>
#> 1 Afghanistan  1999    745   19987071 0.373
#> 2 Afghanistan  2000   2666   20595360 1.29 
#> 3 Brazil       1999  37737  172006362 2.19 
#> 4 Brazil       2000  80488  174504898 4.61 
#> 5 China        1999 212258 1272915272 1.67 
#> 6 China        2000 213766 1280428583 1.67

# حساب إجمالي الحالات لكل سنة
table1 |> 
  group_by(year) |> 
  summarize(total_cases = sum(cases))
#> # A tibble: 2 × 2
#>    year total_cases
#>   <dbl>       <dbl>
#> 1  1999      250740
#> 2  2000      296920

# التصور البصري للتغيرات بمرور الوقت
ggplot(table1, aes(x = year, y = cases)) +
  geom_line(aes(group = country), color = "grey50") +
  geom_point(aes(color = country, shape = country)) +
  scale_x_continuous(breaks = c(1999, 2000)) # محطات المحور السيني عند 1999 و 2000

يوضح هذا الشكل عدد الحالات in عامي 1999 و 2000 لأفغانستان والبرازيل والصين، مع وجود السنة على المحور السيني وعدد الحالات على المحور الصادي. تمثل كل نقطة في الرسم البياني عدد الحالات في دولة معينة في سنة معينة. وتتميز النقاط الخاصة بكل دولة عن غيرها باللون والشكل وتتصل بخط، مما ينتج عنه ثلاثة خطوط غير متوازية وغير متقاطعة. عدد الحالات في الصين هو الأعلى لعامي 1999 و 2000، بقيم تتجاوز 200,000 لكلا العامين. ويبلغ عدد الحالات في البرازيل تقريباً 40,000 في عام 1999 وتقريباً 75,000 في عام 2000. وعدد الحالات في أفغانستان هو الأقل لعامي 1999 و 2000، بقيم تبدو قريبة جداً من 0 في هذا المقياس.

5.2.1 تمارين

  1. لكل جدول من جداول العينة، صف ما يمثله كل سطر (مشاهدة) وكل عمود.

  2. ارسم مخططاً عاماً للعملية التي ستستخدمها لحساب rate لكل من table2 و table3. ستقتضي الحاجة تنفيذ أربع عمليات:

    أ. استخراج عدد حالات مرض السل (TB cases) لكل دولة في السنة.
    ب. استخراج عدد السكان المقابل لكل دولة في السنة.
    ج. قسمة عدد الحالات على عدد السكان، ثم الضرب في 10000.
    د. إعادة تخزين النتيجة في المكان المناسب.

لم تتعلم بعد جميع الدالات التي ستحتاجها لإجراء هذه العمليات فعلياً، ولكن لا يزال بإمكانك التفكير في التحويلات التي ستحتاجها.

5.3 تزييد طول البيانات (Lengthening data)

قد تبدو مبادئ البيانات المرتبة واضحة للغاية لدرجة تجعلك تتساءل عما إذا كنت ستصادف يوماً ما مجموعة بيانات غير مرتبة. لسوء الحظ، فإن معظم البيانات الحقيقية تكون غير مرتبة، ويرجع ذلك لسببين رئيسيين:

  1. غالباً ما يتم تنظيم البيانات لتسهيل هدف آخر غير التحليل. على سبيل المثال، من الشائع هيكلة البيانات لتسهيل إدخال البيانات وليس تسهيل تحليلها.

  2. معظم الناس ليسوا على دراية بمبادئ البيانات المرتبة، ومن الصعب استنتاجها بنفسك ما لم تقضِ وقتاً طويلاً في العمل مع البيانات.

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

توفر حزمة tidyr دالتين لتحويل محاور البيانات: pivot_longer() و pivot_wider(). سنبدأ أولاً بالدالة pivot_longer() لأنها الحالة الأكثر شيوعاً. دعنا ننتقل إلى بعض الأمثلة.

5.3.1 البيانات في أسماء الأعمدة (Data in column names)

تسجل مجموعة بيانات billboard تصنيف الأغاني في قوائم البيلبورد لعام 2000:

billboard
#> # A tibble: 317 × 79
#>   artist       track               date.entered   wk1   wk2   wk3   wk4   wk5
#>   <chr>        <chr>               <date>       <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 2 Pac        Baby Don't Cry (Ke… 2000-02-26      87    82    72    77    87
#> 2 2Ge+her      The Hardest Part O… 2000-09-02      91    87    92    NA    NA
#> 3 3 Doors Down Kryptonite          2000-04-08      81    70    68    67    66
#> 4 3 Doors Down Loser               2000-10-21      76    76    72    69    67
#> 5 504 Boyz     Wobble Wobble       2000-04-15      57    34    25    17    17
#> 6 98^0         Give Me Just One N… 2000-08-19      51    39    34    26    26
#> # ℹ 311 more rows
#> # ℹ 71 more variables: wk6 <dbl>, wk7 <dbl>, wk8 <dbl>, wk9 <dbl>, …

في مجموعة البيانات هذه، تمثل كل مشاهدة أغنية معينة. الأعمدة الثلاثة الأولى (artist و track و date.entered) هي متغيرات تصف الأغنية. بعد ذلك، لدينا 76 عموداً (wk1-wk76) تصف تصنيف الأغنية في كل أسبوع.1 هنا، تمثل أسماء الأعمدة متغيراً واحداً (وهو الأسبوع week) بينما تمثل قيم الخلايا متغيراً آخر (وهو التصنيف rank).

لترتيب هذه البيانات، سنستخدم الدالة pivot_longer():

billboard |> 
  pivot_longer(
    cols = starts_with("wk"), 
    names_to = "week", 
    values_to = "rank"
  )
#> # A tibble: 24,092 × 5
#>    artist track                   date.entered week   rank
#>    <chr>  <chr>                   <date>       <chr> <dbl>
#>  1 2 Pac  Baby Don't Cry (Keep... 2000-02-26   wk1      87
#>  2 2 Pac  Baby Don't Cry (Keep... 2000-02-26   wk2      82
#>  3 2 Pac  Baby Don't Cry (Keep... 2000-02-26   wk3      72
#>  4 2 Pac  Baby Don't Cry (Keep... 2000-02-26   wk4      77
#>  5 2 Pac  Baby Don't Cry (Keep... 2000-02-26   wk5      87
#>  6 2 Pac  Baby Don't Cry (Keep... 2000-02-26   wk6      94
#>  7 2 Pac  Baby Don't Cry (Keep... 2000-02-26   wk7      99
#>  8 2 Pac  Baby Don't Cry (Keep... 2000-02-26   wk8      NA
#>  9 2 Pac  Baby Don't Cry (Keep... 2000-02-26   wk9      NA
#> 10 2 Pac  Baby Don't Cry (Keep... 2000-02-26   wk10     NA
#> # ℹ 24,082 more rows

بعد تحديد البيانات، هناك ثلاث وسيطات رئيسية:

  • cols: تحدد الأعمدة التي تحتاج إلى تحويل محاورها، أي الأعمدة التي لا تعد متغيرات في حد ذاتها. تستخدم هذه الوسيطة نفس صيغة الدالة select()، لذا يمكننا هنا استخدام !c(artist, track, date.entered) أو starts_with("wk").
  • names_to: تمنح اسماً للمتغير المخزن حالياً في أسماء الأعمدة، وقد أسمينا هذا المتغير الجديد week.
  • values_to: تمنح اسماً للمتغير المخزن حالياً في قيم الخلايا، وقد أسمينا هذا المتغير الجديد rank.

لاحظ أنه في الكود تم وضع علامات اقتباس حول "week" و "rank" لأنها متغيرات جديدة نقوم بإنشائها، وليست موجودة في البيانات عند تشغيل أمر pivot_longer().

دعنا الآن نوجه انتباهنا إلى إطار البيانات الناتج الأكثر طولاً. ماذا يحدث إذا بقيت الأغنية في قائمة أفضل 100 لمدة تقل عن 76 أسبوعاً؟ خذ أغنية “Baby Don’t Cry” للمغني 2 Pac على سبيل المثال. تشير المخرجات أعلاه إلى أنها كانت في قائمة أفضل 100 لمدة 7 أسابيع فقط، وأن جميع الأسابيع المتبقية تم ملؤها بقيم مفقودة. هذه القيم المفقودة NA لا تمثل مشاهدات مجهولة في الواقع؛ بل فُرِض وجودها بسبب بنية مجموعة البيانات،2 ولذلك يمكننا توجيه الدالة pivot_longer() للتخلص منها عن طريق تعيين values_drop_na = TRUE:

billboard |> 
  pivot_longer(
    cols = starts_with("wk"), 
    names_to = "week", 
    values_to = "rank",
    values_drop_na = TRUE
  )
#> # A tibble: 5,307 × 5
#>   artist track                   date.entered week   rank
#>   <chr>  <chr>                   <date>       <chr> <dbl>
#> 1 2 Pac  Baby Don't Cry (Keep... 2000-02-26   wk1      87
#> 2 2 Pac  Baby Don't Cry (Keep... 2000-02-26   wk2      82
#> 3 2 Pac  Baby Don't Cry (Keep... 2000-02-26   wk3      72
#> 4 2 Pac  Baby Don't Cry (Keep... 2000-02-26   wk4      77
#> 5 2 Pac  Baby Don't Cry (Keep... 2000-02-26   wk5      87
#> 6 2 Pac  Baby Don't Cry (Keep... 2000-02-26   wk6      94
#> # ℹ 5,301 more rows

أصبح عدد الصفوف الآن أقل بكثير، مما يشير إلى أنه تم إسقاط العديد من الصفوف التي تحتوي على قيم NA.

قد تتساءل أيضاً عما يحدث إذا بقيت أغنية في قائمة أفضل 100 لأكثر من 76 أسبوعاً؟ لا يمكننا معرفة ذلك من هذه البيانات، ولكن يمكنك تخمين أنه سيتم إضافة أعمدة إضافية مثل wk77 و wk78 وهكذا إلى مجموعة البيانات.

أصبحت هذه البيانات الآن مرتبة، ولكن يمكننا تسهيل العمليات الحسابية المستقبلية قليلاً عن طريق تحويل قيم المتغير week من سلاسل نصية إلى أرقام باستخدام الدالتين mutate() و readr::parse_number(). الدالة parse_number() هي دالة مفيدة للغاية تقوم باستخراج أول رقم تجده في السلسلة النصية، متجاهلة أي نصوص أخرى.

billboard_longer <- billboard |> 
  pivot_longer(
    cols = starts_with("wk"), 
    names_to = "week", 
    values_to = "rank",
    values_drop_na = TRUE
  ) |> 
  mutate(
    week = parse_number(week)
  )
billboard_longer
#> # A tibble: 5,307 × 5
#>   artist track                   date.entered  week  rank
#>   <chr>  <chr>                   <date>       <dbl> <dbl>
#> 1 2 Pac  Baby Don't Cry (Keep... 2000-02-26       1    87
#> 2 2 Pac  Baby Don't Cry (Keep... 2000-02-26       2    82
#> 3 2 Pac  Baby Don't Cry (Keep... 2000-02-26       3    72
#> 4 2 Pac  Baby Don't Cry (Keep... 2000-02-26       4    77
#> 5 2 Pac  Baby Don't Cry (Keep... 2000-02-26       5    87
#> 6 2 Pac  Baby Don't Cry (Keep... 2000-02-26       6    94
#> # ℹ 5,301 more rows

الآن بعد أن أصبح لدينا جميع أرقام الأسابيع في متغير واحد وجميع قيم التصنيفات في متغير آخر، أصبحنا في وضع جيد لتصور كيفية تغير تصنيفات الأغاني بمرور الوقت. يظهر الكود أدناه والنتيجة في الشكل 5.2. يمكننا أن نرى أن عدداً قليلاً جداً من الأغاني يظل في قائمة أفضل 100 لأكثر من 20 أسبوعاً.

billboard_longer |> 
  ggplot(aes(x = week, y = rank, group = track)) + 
  geom_line(alpha = 0.25) + 
  scale_y_reverse()
رسم بياني خطي يظهر الأسبوع على المحور السيني والتصنيف على المحور الصادي، حيث يمثل كل خط أغنية معينة. يبدو أن معظم الأغاني تبدأ بتصنيف عالٍ، ثم تتسارع سريعاً إلى تصنيف منخفض (أفضل)، ثم تتراجع مجدداً. هناك عدد قليل بشكل مفاجئ من المسارات في المنطقة التي يكون فيها الأسبوع أكبر من 20 والتصنيف أكبر من 50.
الشكل 5.2: رسم بياني خطي يوضح كيف يتغير تصنيف الأغنية بمرور الوقت.

5.3.2 كيف يعمل تحويل المحاور؟

الآن بعد أن رأيت كيف يمكننا استخدام تحويل المحاور لإعادة تشكيل بياناتنا، دعنا نخصص بعض الوقت لاكتساب فهم منطقي وبديهي حول ما يفعله تحويل المحاور بالبيانات. لنبدأ بمجموعة بيانات بسيطة للغاية ليسهل رؤية ما يحدث. افترض أن لدينا ثلاثة مرضى بمعرفات (id) هي A و B و C، وقمنا بأخذ قياسين لضغط الدم لكل مريض. سنقوم بإنشاء البيانات باستخدام الدالة tribble()، وهي دالة مفيدة لبناء جداول ممتدة (tibbles) صغيرة يدوياً:

df <- tribble(
  ~id,  ~bp1, ~bp2,
   "A",  100,  120,
   "B",  140,  115,
   "C",  120,  125
)

نريد أن تحتوي مجموعة البيانات الجديدة على ثلاثة متغيرات: المعرف id (موجود بالفعل)، والقياس measurement (أسماء الأعمدة الحالية)، والقيمة value (قيم الخلايا الحالية). لتحقيق ذلك, نحتاج إلى تحويل المحاور لـ df ليصبح أطول:

df |> 
  pivot_longer(
    cols = bp1:bp2,
    names_to = "measurement",
    values_to = "value"
  )
#> # A tibble: 6 × 3
#>   id    measurement value
#>   <chr> <chr>       <dbl>
#> 1 A     bp1           100
#> 2 A     bp2           120
#> 3 B     bp1           140
#> 4 B     bp2           115
#> 5 C     bp1           120
#> 6 C     bp2           125

كيف تعمل إعادة التشكيل هذه؟ من الأسهل رؤية ذلك إذا فكرنا فيه عموداً بعمود. كما هو موضح في الشكل 5.3، فإن القيم الموجودة في العمود الذي كان يمثل متغيراً بالفعل في مجموعة البيانات الأصلية (id) تحتاج إلى التكرار، مرة واحدة لكل عمود يتم تحويل محاوره.

مخطط يوضح كيف تقوم الدالة pivot_longer بتحويل مجموعة بيانات بسيطة، باستخدام الألوان لإبراز كيف تتكرر القيم في عمود id (وهي A، B، C) مرتين في المخرجات لأن هناك عمودين يتم تحويل محاورهما (bp1 و bp2).
الشكل 5.3: الأعمدة التي تعد متغيرات بالفعل تحتاج إلى التكرار، مرة واحدة لكل عمود يتم تحويل محاوره.

تصبح أسماء الأعمدة قيماً في متغير جديد، ويتم تحديد اسمه بواسطة الوسيطة names_to، كما هو موضح في الشكل 5.4. وهي تحتاج إلى التكرار مرة واحدة لكل سطر في مجموعة البيانات الأصلية.

مخطط يوضح كيف تقوم الدالة pivot_longer بتحويل مجموعة بيانات بسيطة، باستخدام الألوان لإبراز كيف تصبح أسماء الأعمدة (bp1 و bp2) قيماً في عمود القياس الجديد measurement. وتتكرر ثلاث مرات لوجود ثلاثة أسطر في المدخلات.
الشكل 5.4: تتحول أسماء الأعمدة التي تم تدوير محاورها إلى قيم في عمود جديد. وتحتاج هذه القيم إلى التكرار مرة واحدة لكل سطر من أسطر مجموعة البيانات الأصلية.

تصبح قيم الخلايا أيضاً قيماً في متغير جديد، باسم يتم تحديده بواسطة الوسيطة values_to. ويتم فكها (unwound) سطراً بسطر. يوضح الشكل 5.5 هذه العملية.

مخطط يوضح كيف تقوم الدالة pivot_longer بتحويل البيانات، باستخدام الألوان لإبراز كيف تصبح قيم الخلايا (قياسات ضغط الدم) قيماً في عمود جديد يحمل اسم value. ويتم فكها سطراً بسطر، بحيث تصبح الأسطر الأصلية (100، 120)، ثم (140، 115)، ثم (120، 125)، عموداً واحداً يمتد من 100 إلى 125.
الشكل 5.5: يتم الحفاظ على عدد القيم (دون تكرار)، ولكن يتم فكها سطراً بسطر.

5.3.3 متغيرات متعددة في أسماء الأعمدة

تحدث حالة أكثر صعوبة عندما يكون لديك أجزاء متعددة من المعلومات مكدسة داخل أسماء الأعمدة، وتريد تخزينها في متغيرات جديدة منفصلة. على سبيل المثال، لنأخذ مجموعة بيانات who2، وهي مصدر جدول table1 والجداول المصاحبة له التي رأيتها أعلاه:

who2
#> # A tibble: 7,240 × 58
#>   country      year sp_m_014 sp_m_1524 sp_m_2534 sp_m_3544 sp_m_4554
#>   <chr>       <dbl>    <dbl>     <dbl>     <dbl>     <dbl>     <dbl>
#> 1 Afghanistan  1980       NA        NA        NA        NA        NA
#> 2 Afghanistan  1981       NA        NA        NA        NA        NA
#> 3 Afghanistan  1982       NA        NA        NA        NA        NA
#> 4 Afghanistan  1983       NA        NA        NA        NA        NA
#> 5 Afghanistan  1984       NA        NA        NA        NA        NA
#> 6 Afghanistan  1985       NA        NA        NA        NA        NA
#> # ℹ 7,234 more rows
#> # ℹ 51 more variables: sp_m_5564 <dbl>, sp_m_65 <dbl>, sp_f_014 <dbl>, …

تسجل مجموعة البيانات هذه، التي جمعتها منظمة الصحة العالمية، معلومات حول تشخيصات مرض السل. هناك عمودان يمثلان متغيرات بالفعل ويسهل تفسيرهما: country و year. يليهما 56 عموداً مثل sp_m_014 و ep_m_4554 و rel_m_3544. إذا أمعنت النظر في هذه الأعمدة لفترة كافية، فستلاحظ وجود نمط معين؛ حيث يتكون كل اسم عمود من ثلاثة أجزاء تفصل بينها شرطة سفلية _. الجزء الأول، sp/rel/ep، يصف الطريقة المستخدمة في التشخيص، والجزء الثاني، m/f هو الجنس gender، (والمرمز كمتغير ثنائي في مجموعة البيانات هذه)، والجزء الثالث، 014/1524/2534/3544/4554/5564/65 هو الفئة العمرية (age، حيث يمثل 014 الفئة من 0 إلى 14 عاماً، على سبيل المثال).

لذلك، لدينا في هذه الحالة ستة أجزاء من المعلومات المسجلة في who2: الدولة والسنة (أعمدة بالفعل)؛ وطريقة التشخيص، وفئة الجنس، وفئة الفئة العمرية (الموجودة في أسماء الأعمدة الأخرى)؛ وعدد المرضى في تلك الفئة (قيم الخلايا). لتنظيم هذه الأجزاء الستة من المعلومات في ستة أعمدة منفصلة، نستخدم الدالة pivot_longer() مع تمرير متجه لأسماء الأعمدة لـ names_to وتوجيهات لتقسيم أسماء المتغيرات الأصلية إلى أجزاء عبر الوسيطة names_sep، بالإضافة إلى اسم عمود لـ values_to:

who2 |> 
  pivot_longer(
    cols = !(country:year),
    names_to = c("diagnosis", "gender", "age"), 
    names_sep = "_",
    values_to = "count"
  )
#> # A tibble: 405,440 × 6
#>   country      year diagnosis gender age   count
#>   <chr>       <dbl> <chr>     <chr>  <chr> <dbl>
#> 1 Afghanistan  1980 sp        m      014      NA
#> 2 Afghanistan  1980 sp        m      1524     NA
#> 3 Afghanistan  1980 sp        m      2534     NA
#> 4 Afghanistan  1980 sp        m      3544     NA
#> 5 Afghanistan  1980 sp        m      4554     NA
#> 6 Afghanistan  1980 sp        m      5564     NA
#> # ℹ 405,434 more rows

البديل للوسيطة names_sep هو names_pattern، والتي يمكنك استخدامها لاستخراج المتغيرات من سيناريوهات التسمية الأكثر تعقيداً، بمجرد أن تتعلم عن التعبيرات المنتظمة (regular expressions) في الفصل 15.

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

مخطط يستخدم الألوان لتوضيح كيف يؤدي توفير names_sep و names_to متعددة إلى إنشاء متغيرات متعددة في المخرجات. تحتوي المدخلات على أسماء متغيرات "x_1" و "y_2" والتي يتم تقسيمها بواسطة "_" لإنشاء أعمدة الاسم والتحقق في المخرجات. هذه حالة مشابهة لحالة وجود names_to واحدة، ولكن ما كان سيكون متغيراً واحداً في المخرجات يتم فصله الآن إلى متغيرات متعددة.
الشكل 5.6: تحويل محاور الأعمدة التي تحتوي على أجزاء متعددة من المعلومات في أسمائها يعني أن كل اسم عمود يملأ الآن القيم في أعمدة مخرجات متعددة.

5.3.4 أسماء البيانات والمتغيرات في رؤوس الأعمدة

الخطوة التالية في التعقيد هي عندما تتضمن أسماء الأعمدة مزيجاً من قيم المتغيرات وأسماء المتغيرات. على سبيل المثال، لنأخذ مجموعة بيانات household:

household
#> # A tibble: 5 × 5
#>   family dob_child1 dob_child2 name_child1 name_child2
#>    <int> <date>     <date>     <chr>       <chr>      
#> 1      1 1998-11-26 2000-01-29 Susan       Jose       
#> 2      2 1996-06-22 NA         Mark        <NA>       
#> 3      3 2002-07-11 2004-04-05 Sam         Seth       
#> 4      4 2004-10-10 2009-08-27 Craig       Khai       
#> 5      5 2000-12-05 2005-02-28 Parker      Gracie

تحتوي مجموعة البيانات هذه على بيانات حول خمس عائلات، مع أسماء وتواريخ ميلاد ما يصل إلى طفلين. التحدي الجديد في مجموعة البيانات هذه هو أن أسماء الأعمدة تحتوي على أسماء متغيرين (dob، name) وقيم متغير آخر (child، بالقيم 1 أو 2). لحل هذه المشكلة، نحتاج مرة أخرى إلى تزويد names_to بمتجه، ولكن هذه المرة نستخدم الحارس الخاص ".value"؛ هذا ليس اسماً لمتغير بل قيمة فريدة تخبر الدالة pivot_longer() بالقيام بشيء مختلف. هذا يتجاوز وسيطة values_to المعتادة لاستخدام المكون الأول من اسم العمود الذي تم تدويره كاسم متغير في المخرجات.

household |> 
  pivot_longer(
    cols = !family, 
    names_to = c(".value", "child"), 
    names_sep = "_", 
    values_drop_na = TRUE
  )
#> # A tibble: 9 × 4
#>   family child  dob        name 
#>    <int> <chr>  <date>     <chr>
#> 1      1 child1 1998-11-26 Susan
#> 2      1 child2 2000-01-29 Jose 
#> 3      2 child1 1996-06-22 Mark 
#> 4      3 child1 2002-07-11 Sam  
#> 5      3 child2 2004-04-05 Seth 
#> 6      4 child1 2004-10-10 Craig
#> # ℹ 3 more rows

نستخدم مرة أخرى values_drop_na = TRUE، نظراً لأن شكل المدخلات يفرض إنشاء متغيرات مفقودة صريحة (على سبيل المثال، للعائلات التي لديها طفل واحد فقط).

يوضح الشكل 5.7 الفكرة الأساسية بمثال أبسط. عند استخدام ".value" في names_to، فإن أسماء الأعمدة في المدخلات تساهم في كل من القيم وأسماء المتغيرات في المخرجات.

مخطط يستخدم الألوان لتوضيح كيفية عمل الحارس الخاص ".value". تحتوي المدخلات على أسماء "x_1" و "x_2" و "y_1" و "y_2"، ونريد استخدام المكون الأول ("x"، "y") كاسم متغير والمكون الثاني ("1"، "2") كقيمة لعمود "num" الجديد.
الشكل 5.7: يؤدي تحويل المحاور باستخدام names_to = c(".value", "num") إلى تقسيم أسماء الأعمدة إلى مكونين: الجزء الأول يحدد اسم عمود المخرجات (x أو y)، والجزء الثاني يحدد قيمة عمود num.

5.4 تزييد عرض البيانات (Widening data)

حتى الآن، استخدمنا الدالة pivot_longer() لحل الفئة الشائعة من المشاكل حيث انتهت القيم في أسماء الأعمدة. بعد ذلك، سننتقل إلى الدالة pivot_wider()، والتي تجعل مجموعات البيانات أكثر عرضاً (wider) عن طريق زيادة الأعمدة وتقليل الأسطر، وتساعد عندما تكون المشاهدة الواحدة موزعة عبر أسطر متعددة. يبدو أن هذا يظهر بشكل أقل شيوعاً في الواقع، ولكنه يتكرر كثيراً عند التعامل مع البيانات الحكومية.

سنبدأ بالنظر إلى cms_patient_experience، وهي مجموعة بيانات من مراكز خدمات الرعاية الطبية والخدمات الطبية (CMS) التي تجمع بيانات حول تجارب المرضى:

cms_patient_experience
#> # A tibble: 500 × 5
#>   org_pac_id org_nm                     measure_cd   measure_title   prf_rate
#>   <chr>      <chr>                      <chr>        <chr>              <dbl>
#> 1 0446157747 USC CARE MEDICAL GROUP INC CAHPS_GRP_1  CAHPS for MIPS…       63
#> 2 0446157747 USC CARE MEDICAL GROUP INC CAHPS_GRP_2  CAHPS for MIPS…       87
#> 3 0446157747 USC CARE MEDICAL GROUP INC CAHPS_GRP_3  CAHPS for MIPS…       86
#> 4 0446157747 USC CARE MEDICAL GROUP INC CAHPS_GRP_5  CAHPS for MIPS…       57
#> 5 0446157747 USC CARE MEDICAL GROUP INC CAHPS_GRP_8  CAHPS for MIPS…       85
#> 6 0446157747 USC CARE MEDICAL GROUP INC CAHPS_GRP_12 CAHPS for MIPS…       24
#> # ℹ 494 more rows

الوحدة الأساسية التي يتم دراستها هي المنظمة (organization)، ولكن كل منظمة ممتدة عبر ستة صفوف، مع صف واحد لكل قياس تم إجراؤه في استطلاع المنظمة. يمكننا رؤية المجموعة الكاملة من القيم لـ measure_cd و measure_title باستخدام الدالة distinct():

cms_patient_experience |> 
  distinct(measure_cd, measure_title)
#> # A tibble: 6 × 2
#>   measure_cd   measure_title                                                 
#>   <chr>        <chr>                                                         
#> 1 CAHPS_GRP_1  CAHPS for MIPS SSM: Getting Timely Care, Appointments, and In…
#> 2 CAHPS_GRP_2  CAHPS for MIPS SSM: How Well Providers Communicate            
#> 3 CAHPS_GRP_3  CAHPS for MIPS SSM: Patient's Rating of Provider              
#> 4 CAHPS_GRP_5  CAHPS for MIPS SSM: Health Promotion and Education            
#> 5 CAHPS_GRP_8  CAHPS for MIPS SSM: Courteous and Helpful Office Staff        
#> 6 CAHPS_GRP_12 CAHPS for MIPS SSM: Stewardship of Patient Resources

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

تمتلك الدالة pivot_wider() واجهة معاكسة للدالة pivot_longer(): بدلاً من اختيار أسماء أعمدة جديدة، نحتاج إلى توفير الأعمدة الحالية التي تحدد القيم (values_from) واسم العمود (names_from):

cms_patient_experience |> 
  pivot_wider(
    names_from = measure_cd,
    values_from = prf_rate
  )
#> # A tibble: 500 × 9
#>   org_pac_id org_nm                   measure_title   CAHPS_GRP_1 CAHPS_GRP_2
#>   <chr>      <chr>                    <chr>                 <dbl>       <dbl>
#> 1 0446157747 USC CARE MEDICAL GROUP … CAHPS for MIPS…          63          NA
#> 2 0446157747 USC CARE MEDICAL GROUP … CAHPS for MIPS…          NA          87
#> 3 0446157747 USC CARE MEDICAL GROUP … CAHPS for MIPS…          NA          NA
#> 4 0446157747 USC CARE MEDICAL GROUP … CAHPS for MIPS…          NA          NA
#> 5 0446157747 USC CARE MEDICAL GROUP … CAHPS for MIPS…          NA          NA
#> 6 0446157747 USC CARE MEDICAL GROUP … CAHPS for MIPS…          NA          NA
#> # ℹ 494 more rows
#> # ℹ 4 more variables: CAHPS_GRP_3 <dbl>, CAHPS_GRP_5 <dbl>, …

المخرجات لا تبدو صحيحة تماماً؛ يبدو أنه لا يزال لدينا أسطر متعددة لكل منظمة. هذا لأننا بحاجة أيضاً إلى إخبار pivot_wider() بأي عمود أو أعمدة تحتوي على قيم تحدد كل سطر بشكل فريد؛ في هذه الحالة، هي المتغيرات التي تبدأ بـ "org":

cms_patient_experience |> 
  pivot_wider(
    id_cols = starts_with("org"),
    names_from = measure_cd,
    values_from = prf_rate
  )
#> # A tibble: 95 × 8
#>   org_pac_id org_nm           CAHPS_GRP_1 CAHPS_GRP_2 CAHPS_GRP_3 CAHPS_GRP_5
#>   <chr>      <chr>                  <dbl>       <dbl>       <dbl>       <dbl>
#> 1 0446157747 USC CARE MEDICA…          63          87          86          57
#> 2 0446162697 ASSOCIATION OF …          59          85          83          63
#> 3 0547164295 BEAVER MEDICAL …          49          NA          75          44
#> 4 0749333730 CAPE PHYSICIANS…          67          84          85          65
#> 5 0840104360 ALLIANCE PHYSIC…          66          87          87          64
#> 6 0840109864 REX HOSPITAL INC          73          87          84          67
#> # ℹ 89 more rows
#> # ℹ 2 more variables: CAHPS_GRP_8 <dbl>, CAHPS_GRP_12 <dbl>

هذا يعطينا المخرجات التي نبحث عنها.

5.4.1 كيف تعمل الدالة pivot_wider()؟

لفهم كيفية عمل الدالة pivot_wider()، دعنا نبدأ مرة أخرى بمجموعة بيانات بسيطة للغاية. هذه المرة لدينا مريضان بمعرفات id هما A و B، ولدينا ثلاثة قياسات لضغط الدم للمريض A وقياسان للمريض B:

df <- tribble(
  ~id, ~measurement, ~value,
  "A",        "bp1",    100,
  "B",        "bp1",    140,
  "B",        "bp2",    115, 
  "A",        "bp2",    120,
  "A",        "bp3",    105
)

سنأخذ القيم من عمود value والأسماء من عمود measurement:

df |> 
  pivot_wider(
    names_from = measurement,
    values_from = value
  )
#> # A tibble: 2 × 4
#>   id      bp1   bp2   bp3
#>   <chr> <dbl> <dbl> <dbl>
#> 1 A       100   120   105
#> 2 B       140   115    NA

لبدء العملية، تحتاج الدالة pivot_wider() أولاً إلى معرفة ما الذي سيذهب في الصفوف والأعمدة. ستكون أسماء الأعمدة الجديدة هي القيم الفريدة لـ measurement.

df |> 
  distinct(measurement) |> 
  pull()
#> [1] "bp1" "bp2" "bp3"

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

df |> 
  select(!measurement & !value) |> 
  distinct()
#> # A tibble: 2 × 1
#>   id   
#>   <chr>
#> 1 A    
#> 2 B

تقوم الدالة pivot_wider() بعد ذلك بدمج هذه النتائج لإنشاء إطار بيانات فارغ:

df |> 
  select(!measurement & !value) |> 
  distinct() |> 
  mutate(x = NA, y = NA, z = NA)
#> # A tibble: 2 × 4
#>   id    x     y     z    
#>   <chr> <lgl> <lgl> <lgl>
#> 1 A     NA    NA    NA   
#> 2 B     NA    NA    NA

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

قد تتساءل أيضاً عما يحدث إذا كان هناك صفوف متعددة في المدخلات تتوافق مع خلية واحدة في المخرجات. المثال أدناه يحتوي على صفين يتوافقان مع المعرف id “A” والقياس measurement “bp1”:

df <- tribble(
  ~id, ~measurement, ~value,
  "A",        "bp1",    100,
  "A",        "bp1",    102,
  "A",        "bp2",    120,
  "B",        "bp1",    140, 
  "B",        "bp2",    115
)

إذا حاولنا تحويل محاور هذا الجدول، فسنحصل على مخرجات تحتوي على أعمدة القوائم (list-columns)، والتي ستتعلم المزيد عنها في الفصل 23:

df |> 
  pivot_wider(
    names_from = measurement,
    values_from = value
  )
#> Warning: Values from `value` are not uniquely identified; output will contain
#> list-cols.
#> • Use `values_fn = list` to suppress this warning.
#> • Use `values_fn = {summary_fun}` to summarise duplicates.
#> • Use the following dplyr code to identify duplicates.
#>   {data} |>
#>   dplyr::summarise(n = dplyr::n(), .by = c(id, measurement)) |>
#>   dplyr::filter(n > 1L)
#> # A tibble: 2 × 3
#>   id    bp1       bp2      
#>   <chr> <list>    <list>   
#> 1 A     <dbl [2]> <dbl [1]>
#> 2 B     <dbl [1]> <dbl [1]>

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

df |> 
  group_by(id, measurement) |> 
  summarize(n = n(), .groups = "drop") |> 
  filter(n > 1)
#> # A tibble: 1 × 3
#>   id    measurement     n
#>   <chr> <chr>       <int>
#> 1 A     bp1             2

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

5.5 ملخص

في هذا الفصل، تعلمت عن البيانات المرتبة (tidy data): البيانات التي تحتوي على متغيرات في الأعمدة ومشاهدات في الصفوف. تسهل البيانات المرتبة العمل في منظومة tidyverse، لأنها بنية متسقة تفهمها معظم الدالات، والتحدي الرئيسي يكمن في تحويل البيانات من أي بنية تتلقاها بها إلى تنسيق مرتب. وتحقيقاً لهذه الغاية، تعلمت عن الدالتين pivot_longer() و pivot_wider() اللتين تتيحان لك ترتيب العديد من مجموعات البيانات غير المرتبة. الأمثلة التي قدمناها هنا هي مجموعة مختارة من تلك الموجودة في دليل الحزمة vignette("pivot", package = "tidyr")، لذا إذا واجهت مشكلة لا يساعدك هذا الفصل في حلها، فإن هذا الدليل يعد مكاناً جيداً لتجربته بعد ذلك.

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

إذا استمتعت بهذا الفصل وأردت معرفة المزيد عن النظرية الكامنة وراءه، يمكنك معرفة المزيد عن التاريخ والأسس النظرية في ورقة Tidy Data المنشورة في مجلة البرمجيات الإحصائية (Journal of Statistical Software).

الآن بعد أن أصبحت تكتب كمية كبيرة من أكواد R، فقد حان الوقت لمعرفة المزيد حول تنظيم الكود الخاص بك في ملفات ومجلدات. في الفصل القادم، ستتعلم كل شيء عن مزايا السكربتات والمشاريع (scripts and projects)، وبعض الأدوات العديدة التي توفرها لتجعل حياتك أسهل.


  1. سيتم تضمين الأغنية طالما أنها كانت ضمن أفضل 100 أغنية في أي وقت من عام 2000، ويتم تتبعها لمدة تصل إلى 72 أسبوعاً بعد ظهورها.↩︎

  2. سنعود إلى هذه الفكرة في الفصل 18.↩︎