19  عمليات الدمج (Joins)

19.1 مقدمة

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

  • عمليات الدمج التحويرية (Mutating joins). والتي تضيف متغيرات جديدة إلى إطار بيانات واحد من الملاحظات المطابقة في إطار بيانات آخر.
  • عمليات الدمج الترشيحية (Filtering joins). والتي ترشح الملاحظات من إطار بيانات واحد بناءً على ما إذا كانت تطابق ملاحظة في إطار بيانات آخر أم لا.

سنبدأ بمناقشة المفاتيح (keys)، وهي المتغيرات المستخدمة لربط زوج من إطارات البيانات في عملية الدمج. سنثبت النظرية بفحص المفاتيح في مجموعات البيانات من حزمة nycflights13، ثم نستخدم تلك المعرفة للبدء في دمج إطارات البيانات معاً. بعد ذلك، سنناقش كيفية عمل عمليات الدمج، مع التركيز على تأثيرها على الصفوف. سننتهي بمناقشة عمليات الدمج غير المتكافئة (non-equi joins)، وهي عائلة من عمليات الدمج توفر طريقة أكثر مرونة لمطابقة المفاتيح من علاقة المساواة الافتراضية.

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

في هذا الفصل، سنستكشف مجموعات البيانات الخمس المترابطة من حزمة nycflights13 باستخدام دوال الدمج من حزمة dplyr.

19.2 المفاتيح (Keys)

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

19.2.1 المفاتيح الأساسية والخارجية

تتضمن كل عملية دمج زوجاً من المفاتيح: مفتاح أساسي ومفتاح خارجي. المفتاح الأساسي (Primary key) هو متغير أو مجموعة متغيرات تحدد بشكل فريد كل ملاحظة. عندما يلزم أكثر من متغير واحد، يسمى المفتاح مفتاحاً مركباً (compound key). على سبيل المثال، في nycflights13:

  • يسجل جدول airlines قطعتين من البيانات عن كل شركة طيران: رمز الناقل واسمها الكامل. يمكنك تحديد شركة الطيران برمز الناقل المكون من حرفين، مما يجعل carrier هو المفتاح الأساسي.
airlines
#> # A tibble: 16 × 2
#>   carrier name                    
#>   <chr>   <chr>                   
#> 1 9E      Endeavor Air Inc.       
#> 2 AA      American Airlines Inc.  
#> 3 AS      Alaska Airlines Inc.    
#> 4 B6      JetBlue Airways         
#> 5 DL      Delta Air Lines Inc.    
#> 6 EV      ExpressJet Airlines Inc.
#> # ℹ 10 more rows
  • يسجل جدول airports بيانات عن كل مطار. يمكنك تحديد كل مطار بواسطة رمز المطار المكون من ثلاثة أحرف، مما يجعل faa هو المفتاح الأساسي.
airports
#> # A tibble: 1,458 × 8
#>   faa   name                            lat   lon   alt    tz dst  
#>   <chr> <chr>                         <dbl> <dbl> <dbl> <dbl> <chr>
#> 1 04G   Lansdowne Airport              41.1 -80.6  1044    -5 A    
#> 2 06A   Moton Field Municipal Airport  32.5 -85.7   264    -6 A    
#> 3 06C   Schaumburg Regional            42.0 -88.1   801    -6 A    
#> 4 06N   Randall Airport                41.4 -74.4   523    -5 A    
#> 5 09J   Jekyll Island Airport          31.1 -81.4    11    -5 A    
#> 6 0A9   Elizabethton Municipal Airpo…  36.4 -82.2  1593    -5 A    
#> # ℹ 1,452 more rows
#> # ℹ 1 more variable: tzone <chr>
  • يسجل جدول planes بيانات عن كل طائرة. يمكنك تحديد الطائرة برقم ذيلها، مما يجعل tailnum هو المفتاح الأساسي.
planes
#> # A tibble: 3,322 × 9
#>   tailnum  year type              manufacturer    model     engines
#>   <chr>   <int> <chr>             <chr>           <chr>       <int>
#> 1 N10156   2004 Fixed wing multi… EMBRAER         EMB-145XR       2
#> 2 N102UW   1998 Fixed wing multi… AIRBUS INDUSTR… A320-214        2
#> 3 N103US   1999 Fixed wing multi… AIRBUS INDUSTR… A320-214        2
#> 4 N104UW   1999 Fixed wing multi… AIRBUS INDUSTR… A320-214        2
#> 5 N10575   2002 Fixed wing multi… EMBRAER         EMB-145LR       2
#> 6 N105UW   1999 Fixed wing multi… AIRBUS INDUSTR… A320-214        2
#> # ℹ 3,316 more rows
#> # ℹ 3 more variables: seats <int>, speed <int>, engine <chr>
  • يسجل جدول weather بيانات عن الطقس في مطارات المغادرة. يمكنك تحديد كل ملاحظة من خلال تجميعة الموقع والوقت، مما يجعل origin و time_hour هما المفتاح الأساسي المركب.
weather
#> # A tibble: 26,115 × 15
#>   origin  year month   day  hour  temp  dewp humid wind_dir
#>   <chr>  <int> <int> <int> <int> <dbl> <dbl> <dbl>    <dbl>
#> 1 EWR     2013     1     1     1  39.0  26.1  59.4      270
#> 2 EWR     2013     1     1     2  39.0  27.0  61.6      250
#> 3 EWR     2013     1     1     3  39.0  28.0  64.4      240
#> 4 EWR     2013     1     1     4  39.9  28.0  62.2      250
#> 5 EWR     2013     1     1     5  39.0  28.0  64.4      260
#> 6 EWR     2013     1     1     6  37.9  28.0  67.2      240
#> # ℹ 26,109 more rows
#> # ℹ 6 more variables: wind_speed <dbl>, wind_gust <dbl>, …

المفتاح الخارجي (Foreign key) هو متغير (أو مجموعة متغيرات) يتوافق مع مفتاح أساسي في جدول آخر. على سبيل المثال:

  • flights$tailnum هو مفتاح خارجي يتوافق مع المفتاح الأساسي planes$tailnum.
  • flights$carrier هو مفتاح خارجي يتوافق مع المفتاح الأساسي airlines$carrier.
  • flights$origin هو مفتاح خارجي يتوافق مع المفتاح الأساسي airports$faa.
  • flights$dest هو مفتاح خارجي يتوافق مع المفتاح الأساسي airports$faa.
  • flights$origin-flights$time_hour هو مفتاح خارجي مركب يتوافق مع المفتاح الأساسي المركب weather$origin-weather$time_hour.

يتم تلخيص هذه العلاقات بصرياً في الشكل 19.1.

العلاقات بين مجموعات بيانات المطارات والطائرات والرحلات الجوية والطقس وشركات الطيران من حزمة nycflights13. airports$faa متصل بـ flights$origin و flights$dest. planes$tailnum متصل بـ flights$tailnum. weather$time_hour و weather$origin متصلان بشكل مشترك بـ flights$time_hour و flights$origin. airlines$carrier متصل بـ flights$carrier. لا توجد روابط مباشرة بين إطارات بيانات المطارات والطائرات وشركات الطيران والطقس.
الشكل 19.1: الروابط بين جميع إطارات البيانات الخمسة في حزمة nycflights13. المتغيرات التي تشكل مفتاحاً أساسياً ملونة باللون الرمادي، وتتصل بالمفاتيح الخارجية المقابلة لها بأسهم.

ستلاحظ ميزة جيدة في تصميم هذه المفاتيح: المفاتيح الأساسية والخارجية لها تقريباً نفس الأسماء دائماً، مما سيجعل حياة الدمج أسهل بكثير كما سترى قريباً. من الجدير بالذكر أيضاً العلاقة العكسية: تقريباً كل اسم متغير مستخدم في جداول متعددة له نفس المعنى في كل مكان. هناك استثناء واحد فقط: year يعني سنة المغادرة في flights وسنة الصنع في planes. سيصبح هذا مهماً عندما نبدأ فعلياً في دمج الجداول معاً.

19.2.2 التحقق من المفاتيح الأساسية

الآن بعد أن حددنا المفاتيح الأساسية في كل جدول، من الممارسات الجيدة التحقق من أنها تحدد كل ملاحظة بشكل فريد بالفعل. إحدى الطرق للقيام بذلك هي استخدام count() مع المفاتيح الأساسية والبحث عن المدخلات التي يكون فيها n أكبر من واحد. يكشف هذا أن planes و weather يبدوان جيدين:

planes |> 
  count(tailnum) |> 
  filter(n > 1)
#> # A tibble: 0 × 2
#> # ℹ 2 variables: tailnum <chr>, n <int>

weather |> 
  count(time_hour, origin) |> 
  filter(n > 1)
#> # A tibble: 0 × 3
#> # ℹ 3 variables: time_hour <dttm>, origin <chr>, n <int>

يجب عليك أيضاً التحقق من وجود قيم مفقودة في مفاتيحك الأساسية — إذا كانت القيمة مفقودة، فلا يمكنها تحديد ملاحظة!

planes |> 
  filter(is.na(tailnum))
#> # A tibble: 0 × 9
#> # ℹ 9 variables: tailnum <chr>, year <int>, type <chr>, manufacturer <chr>,
#> #   model <chr>, engines <int>, seats <int>, speed <int>, engine <chr>

weather |> 
  filter(is.na(time_hour) | is.na(origin))
#> # A tibble: 0 × 15
#> # ℹ 15 variables: origin <chr>, year <int>, month <int>, day <int>,
#> #   hour <int>, temp <dbl>, dewp <dbl>, humid <dbl>, wind_dir <dbl>, …

19.2.3 المفاتيح البديلة (Surrogate keys)

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

بعد القليل من التفكير والتجربة، حددنا أن هناك ثلاثة متغيرات تحدد كل رحلة بشكل فريد معاً:

flights |> 
  count(time_hour, carrier, flight) |> 
  filter(n > 1)
#> # A tibble: 0 × 4
#> # ℹ 4 variables: time_hour <dttm>, carrier <chr>, flight <int>, n <int>

هل غياب التكرارات يجعل time_hour-carrier-flight مفتاحاً أساسياً تلقائياً؟ إنها بالتأكيد بداية جيدة، لكنها لا تضمن ذلك. على سبيل المثال، هل الارتفاع وخط العرض مفتاح أساسي جيد لجدول airports؟

airports |> 
  count(alt, lat) |> 
  filter(n > 1)
#> # A tibble: 1 × 3
#>     alt   lat     n
#>   <dbl> <dbl> <int>
#> 1    13  40.6     2

تحديد المطار من خلال ارتفاعه وخط العرض هو بوضوح فكرة سيئة، وبشكل عام ليس من الممكن معرفة ما إذا كانت مجموعة من المتغيرات تشكل مفتاحاً أساسياً جيداً من البيانات وحدها. ولكن بالنسبة للرحلات الجوية، يبدو التجميع بين time_hour و carrier و flight معقولاً لأنه سيكون مربكاً حقاً لشركة الطيران وعملائها إذا كانت هناك رحلات متعددة بنفس رقم الرحلة في الجو في نفس الوقت.

ومع ذلك، قد نكون بأفضل حال عند تقديم مفتاح بديل عددي بسيط باستخدام رقم الصف:

flights2 <- flights |> 
  mutate(id = row_number(), .before = 1)
flights2
#> # A tibble: 336,776 × 20
#>      id  year month   day dep_time sched_dep_time dep_delay arr_time
#>   <int> <int> <int> <int>    <int>          <int>     <dbl>    <int>
#> 1     1  2013     1     1      517            515         2      830
#> 2     2  2013     1     1      533            529         4      850
#> 3     3  2013     1     1      542            540         2      923
#> 4     4  2013     1     1      544            545        -1     1004
#> 5     5  2013     1     1      554            600        -6      812
#> 6     6  2013     1     1      554            558        -4      740
#> # ℹ 336,770 more rows
#> # ℹ 12 more variables: sched_arr_time <int>, arr_delay <dbl>, …

يمكن أن تكون المفاتيح البديلة مفيدة بشكل خاص عند التواصل مع البشر الآخرين: إنه أسهل بكثير أن تقول لشخص ما أن ينظر إلى الرحلة رقم 2001 من أن تقول انظر إلى UA430 التي غادرت الساعة 9 صباحاً 2013-01-03.

19.2.4 تمارين

  1. نسينا رسم العلاقة بين weather و airports في الشكل 19.1. ما هي العلاقة وكيف ينبغي أن تظهر في المخطط؟

  2. يحتوي جدول weather فقط على معلومات لمطارات المغادرة الثلاثة في نيويورك. إذا كان يحتوي على سجلات الطقس لجميع المطارات في الولايات المتحدة، فما هو الربط الإضافي الذي سيجريه مع flights؟

  3. تشكل المتغيرات year و month و day و hour و origin مفتاحاً مركباً لجدول weather تقريباً، ولكن هناك ساعة واحدة تحتوي على ملاحظات مكررة. هل يمكنك معرفة ما المميز في تلك الساعة؟

  4. نعلم أن بعض أيام السنة مميزة ويسافر فيها عدد أقل من الناس عن المعتاد (على سبيل المثال، عشية الميلاد ويوم الميلاد). كيف يمكنك تمثيل تلك البيانات كإطار بيانات؟ ماذا سيكون المفتاح الأساسي؟ كيف سيتصل بإطارات البيانات الحالية؟

  5. ارسم مخططاً يوضح الروابط بين إطارات البيانات Batting و People و Salaries في حزمة Lahman. ارسم مخططاً آخر يوضح العلاقة بين People و Managers و AwardsManagers. كيف تصف العلاقة بين إطارات البيانات Batting و Pitching و Fielding؟

19.3 عمليات الدمج الأساسية

الآن بعد أن فهمت كيفية اتصال إطارات البيانات عبر المفاتيح، يمكننا البدء في استخدام عمليات الدمج لفهم مجموعة بيانات flights بشكل أفضل. توفر حزمة dplyr ست دوال للدمج: left_join() و inner_join() و right_join() و full_join() و semi_join() و anti_join(). جميعها لها نفس الواجهة: تأخذ زوجاً من إطارات البيانات (x و y) وترجع إطار بيانات. ترتيب الصفوف والأعمدة في المخرجات يتحدد بشكل أساسي بواسطة x.

في هذا القسم، ستتعلم كيفية استخدام نوع واحد من عمليات الدمج التحويرية وهو left_join()، ونوعين من عمليات الدمج الترشيحية وهما semi_join() و anti_join(). في القسم التالي، ستتعلم بالضبط كيفية عمل هذه الدوال، وعن الدوال المتبقية inner_join() و right_join() و full_join().

19.3.1 عمليات الدمج التحويرية (Mutating joins)

يسمح لك الدمج التحويلي (mutating join) بدمج المتغيرات من إطارين للبيانات (two data frames): حيث يقوم أولاً بمطابقة المشاهدات بناءً على مفاتيحها (keys)، ثم نسخ المتغيرات من أحد إطاري البيانات إلى الآخر. وعلى غرار الدالة mutate()، تضيف دالات الدمج المتغيرات جهة اليمين، لذا إذا كانت مجموعة البيانات لديك تحتوي على العديد من المتغيرات، فلن تتمكن من رؤية المتغيرات الجديدة. ومن أجل هذه الأمثلة، سنقوم بتسهيل رؤية ما يحدث عبر إنشاء مجموعة بيانات أكثر ضيقاً تحتوي على ستة متغيرات فقط1:

flights2 <- flights |> 
  select(year, time_hour, origin, dest, tailnum, carrier)
flights2
#> # A tibble: 336,776 × 6
#>    year time_hour           origin dest  tailnum carrier
#>   <int> <dttm>              <chr>  <chr> <chr>   <chr>  
#> 1  2013 2013-01-01 05:00:00 EWR    IAH   N14228  UA     
#> 2  2013 2013-01-01 05:00:00 LGA    IAH   N24211  UA     
#> 3  2013 2013-01-01 05:00:00 JFK    MIA   N619AA  AA     
#> 4  2013 2013-01-01 05:00:00 JFK    BQN   N804JB  B6     
#> 5  2013 2013-01-01 06:00:00 LGA    ATL   N668DN  DL     
#> 6  2013 2013-01-01 05:00:00 EWR    ORD   N39463  UA     
#> # ℹ 336,770 more rows

هناك أربعة أنواع من الدمج التحويلي (mutating join)، ولكن هناك نوعاً واحداً ستستخدمه في معظم الأوقات تقريباً: وهو left_join(). ما يميز هذا النوع هو أن المخرجات ستحتوي دائماً على نفس عدد صفوف إطار البيانات x (وهو إطار البيانات الذي تقوم بالدمج إليه)2. الاستخدام الأساسي للدالة left_join() هو إضافة بيانات وصفية (metadata) إضافية. على سبيل المثال، يمكننا استخدام left_join() لإضافة اسم شركة الطيران الكامل إلى بيانات flights2:

flights2 |> 
  left_join(airlines)
#> Joining with `by = join_by(carrier)`
#> # A tibble: 336,776 × 7
#>    year time_hour           origin dest  tailnum carrier name                
#>   <int> <dttm>              <chr>  <chr> <chr>   <chr>   <chr>               
#> 1  2013 2013-01-01 05:00:00 EWR    IAH   N14228  UA      United Air Lines In…
#> 2  2013 2013-01-01 05:00:00 LGA    IAH   N24211  UA      United Air Lines In…
#> 3  2013 2013-01-01 05:00:00 JFK    MIA   N619AA  AA      American Airlines I…
#> 4  2013 2013-01-01 05:00:00 JFK    BQN   N804JB  B6      JetBlue Airways     
#> 5  2013 2013-01-01 06:00:00 LGA    ATL   N668DN  DL      Delta Air Lines Inc.
#> 6  2013 2013-01-01 05:00:00 EWR    ORD   N39463  UA      United Air Lines In…
#> # ℹ 336,770 more rows

أو يمكننا معرفة درجة الحرارة وسرعة الرياح عندما غادرت كل طائرة:

flights2 |> 
  left_join(weather |> select(origin, time_hour, temp, wind_speed))
#> Joining with `by = join_by(time_hour, origin)`
#> # A tibble: 336,776 × 8
#>    year time_hour           origin dest  tailnum carrier  temp wind_speed
#>   <int> <dttm>              <chr>  <chr> <chr>   <chr>   <dbl>      <dbl>
#> 1  2013 2013-01-01 05:00:00 EWR    IAH   N14228  UA       39.0       12.7
#> 2  2013 2013-01-01 05:00:00 LGA    IAH   N24211  UA       39.9       15.0
#> 3  2013 2013-01-01 05:00:00 JFK    MIA   N619AA  AA       39.0       15.0
#> 4  2013 2013-01-01 05:00:00 JFK    BQN   N804JB  B6       39.0       15.0
#> 5  2013 2013-01-01 06:00:00 LGA    ATL   N668DN  DL       39.9       16.1
#> 6  2013 2013-01-01 05:00:00 EWR    ORD   N39463  UA       39.0       12.7
#> # ℹ 336,770 more rows

أو حجم الطائرة التي كانت تطير:

flights2 |> 
  left_join(planes |> select(tailnum, type, engines, seats))
#> Joining with `by = join_by(tailnum)`
#> # A tibble: 336,776 × 9
#>    year time_hour           origin dest  tailnum carrier type                
#>   <int> <dttm>              <chr>  <chr> <chr>   <chr>   <chr>               
#> 1  2013 2013-01-01 05:00:00 EWR    IAH   N14228  UA      Fixed wing multi en…
#> 2  2013 2013-01-01 05:00:00 LGA    IAH   N24211  UA      Fixed wing multi en…
#> 3  2013 2013-01-01 05:00:00 JFK    MIA   N619AA  AA      Fixed wing multi en…
#> 4  2013 2013-01-01 05:00:00 JFK    BQN   N804JB  B6      Fixed wing multi en…
#> 5  2013 2013-01-01 06:00:00 LGA    ATL   N668DN  DL      Fixed wing multi en…
#> 6  2013 2013-01-01 05:00:00 EWR    ORD   N39463  UA      Fixed wing multi en…
#> # ℹ 336,770 more rows
#> # ℹ 2 more variables: engines <int>, seats <int>

عندما تفشل left_join() في العثور على مطابقة لصف في x، فإنها تملأ المتغيرات الجديدة بقيم مفقودة. على سبيل المثال، لا توجد معلومات عن الطائرة برقم الذيل N3ALAA لذا ستكون type و engines و seats مفقودة:

flights2 |> 
  filter(tailnum == "N3ALAA") |> 
  left_join(planes |> select(tailnum, type, engines, seats))
#> Joining with `by = join_by(tailnum)`
#> # A tibble: 63 × 9
#>    year time_hour           origin dest  tailnum carrier type  engines seats
#>   <int> <dttm>              <chr>  <chr> <chr>   <chr>   <chr>   <int> <int>
#> 1  2013 2013-01-01 06:00:00 LGA    ORD   N3ALAA  AA      <NA>       NA    NA
#> 2  2013 2013-01-02 18:00:00 LGA    ORD   N3ALAA  AA      <NA>       NA    NA
#> 3  2013 2013-01-03 06:00:00 LGA    ORD   N3ALAA  AA      <NA>       NA    NA
#> 4  2013 2013-01-07 19:00:00 LGA    ORD   N3ALAA  AA      <NA>       NA    NA
#> 5  2013 2013-01-08 17:00:00 JFK    ORD   N3ALAA  AA      <NA>       NA    NA
#> 6  2013 2013-01-16 06:00:00 LGA    ORD   N3ALAA  AA      <NA>       NA    NA
#> # ℹ 57 more rows

سنعود إلى هذه المشكلة عدة مرات في بقية الفصل.

19.3.2 تحديد مفاتيح الدمج

افتراضياً، ستستخدم left_join() جميع المتغيرات التي تظهر في كلا إطاري البيانات كمفتاح للدمج، وهو ما يسمى الدمج الطبيعي (natural join). هذا أسلوب حدسي مفيد، ولكنه لا يعمل دائماً. على سبيل المثال، ماذا يحدث إذا حاولنا دمج flights2 مع مجموعة بيانات planes الكاملة؟

flights2 |> 
  left_join(planes)
#> Joining with `by = join_by(year, tailnum)`
#> # A tibble: 336,776 × 13
#>    year time_hour           origin dest  tailnum carrier type  manufacturer
#>   <int> <dttm>              <chr>  <chr> <chr>   <chr>   <chr> <chr>       
#> 1  2013 2013-01-01 05:00:00 EWR    IAH   N14228  UA      <NA>  <NA>        
#> 2  2013 2013-01-01 05:00:00 LGA    IAH   N24211  UA      <NA>  <NA>        
#> 3  2013 2013-01-01 05:00:00 JFK    MIA   N619AA  AA      <NA>  <NA>        
#> 4  2013 2013-01-01 05:00:00 JFK    BQN   N804JB  B6      <NA>  <NA>        
#> 5  2013 2013-01-01 06:00:00 LGA    ATL   N668DN  DL      <NA>  <NA>        
#> 6  2013 2013-01-01 05:00:00 EWR    ORD   N39463  UA      <NA>  <NA>        
#> # ℹ 336,770 more rows
#> # ℹ 5 more variables: model <chr>, engines <int>, seats <int>, …

نحصل على الكثير من التطابقات المفقودة لأن الدمج يحاول استخدام tailnum و year كمفتاح مركب. يحتوي كل من flights و planes على عمود year ولكنهما يعنيان أشياء مختلفة: flights$year هي السنة التي حدثت فيها الرحلة و planes$year هي السنة التي تم فيها تصنيع الطائرة. نريد فقط الدمج على tailnum لذا نحتاج إلى تقديم تحديد صريح باستخدام join_by():

flights2 |> 
  left_join(planes, join_by(tailnum))
#> # A tibble: 336,776 × 14
#>   year.x time_hour           origin dest  tailnum carrier year.y
#>    <int> <dttm>              <chr>  <chr> <chr>   <chr>    <int>
#> 1   2013 2013-01-01 05:00:00 EWR    IAH   N14228  UA        1999
#> 2   2013 2013-01-01 05:00:00 LGA    IAH   N24211  UA        1998
#> 3   2013 2013-01-01 05:00:00 JFK    MIA   N619AA  AA        1990
#> 4   2013 2013-01-01 05:00:00 JFK    BQN   N804JB  B6        2012
#> 5   2013 2013-01-01 06:00:00 LGA    ATL   N668DN  DL        1991
#> 6   2013 2013-01-01 05:00:00 EWR    ORD   N39463  UA        2012
#> # ℹ 336,770 more rows
#> # ℹ 7 more variables: type <chr>, manufacturer <chr>, model <chr>, …

لاحظ أنه تم توضيح متغيرات year في المخرجات بلاحقة (year.x و year.y)، والتي تخبرك ما إذا كان المتغير قادماً من المعامل x أو y. يمكنك إبطال اللواحق الافتراضية باستخدام المعامل suffix.

عبارة join_by(tailnum) هي اختصار لـ join_by(tailnum == tailnum). من المهم معرفة هذا الشكل الأكثر اكتمالاً لسببين. أولاً، يصف العلاقة بين الجدولين: يجب أن تكون المفاتيح متساوية. لهذا السبب يُطلق على هذا النوع من الدمج غالباً اسم الدمج المتكافئ (equi join). ستتعلم عن عمليات الدمج غير المتكافئة في قسم 19.5.

ثانياً، هذه هي الطريقة التي تحدد بها مفاتيح دمج مختلفة في كل جدول. على سبيل المثال، هناك طريقتان لدمج جدول flights2 وجدول airports: إما عن طريق dest أو origin:

flights2 |> 
  left_join(airports, join_by(dest == faa))
#> # A tibble: 336,776 × 13
#>    year time_hour           origin dest  tailnum carrier name                
#>   <int> <dttm>              <chr>  <chr> <chr>   <chr>   <chr>               
#> 1  2013 2013-01-01 05:00:00 EWR    IAH   N14228  UA      George Bush Interco…
#> 2  2013 2013-01-01 05:00:00 LGA    IAH   N24211  UA      George Bush Interco…
#> 3  2013 2013-01-01 05:00:00 JFK    MIA   N619AA  AA      Miami Intl          
#> 4  2013 2013-01-01 05:00:00 JFK    BQN   N804JB  B6      <NA>                
#> 5  2013 2013-01-01 06:00:00 LGA    ATL   N668DN  DL      Hartsfield Jackson …
#> 6  2013 2013-01-01 05:00:00 EWR    ORD   N39463  UA      Chicago Ohare Intl  
#> # ℹ 336,770 more rows
#> # ℹ 6 more variables: lat <dbl>, lon <dbl>, alt <dbl>, tz <dbl>, …

flights2 |> 
  left_join(airports, join_by(origin == faa))
#> # A tibble: 336,776 × 13
#>    year time_hour           origin dest  tailnum carrier name               
#>   <int> <dttm>              <chr>  <chr> <chr>   <chr>   <chr>              
#> 1  2013 2013-01-01 05:00:00 EWR    IAH   N14228  UA      Newark Liberty Intl
#> 2  2013 2013-01-01 05:00:00 LGA    IAH   N24211  UA      La Guardia         
#> 3  2013 2013-01-01 05:00:00 JFK    MIA   N619AA  AA      John F Kennedy Intl
#> 4  2013 2013-01-01 05:00:00 JFK    BQN   N804JB  B6      John F Kennedy Intl
#> 5  2013 2013-01-01 06:00:00 LGA    ATL   N668DN  DL      La Guardia         
#> 6  2013 2013-01-01 05:00:00 EWR    ORD   N39463  UA      Newark Liberty Intl
#> # ℹ 336,770 more rows
#> # ℹ 6 more variables: lat <dbl>, lon <dbl>, alt <dbl>, tz <dbl>, …

في الأكواد القديمة، قد ترى طريقة مختلفة لتحديد مفاتيح الدمج باستخدام متجه نصي:

  • by = "x" تتوافق مع join_by(x).
  • by = c("a" = "x") تتوافق مع join_by(a == x).

الآن بعد وجودها، نفضل join_by() لأنها توفر تحديداً أوضح وأكثر مرونة.

تمتلك inner_join() و right_join() و full_join() نفس واجهة left_join(). الفرق يكمن في الصفوف التي تحتفظ بها: يحتفظ الدمج الأيسر (left join) بجميع الصفوف في x، ويحتفظ الدمج الأيمن (right join) بجميع الصفوف في y، ويحتفظ الدمج الكامل (full join) بجميع الصفوف في أي من x أو y، بينما يحتفظ الدمج الداخلي (inner join) فقط بالصفوف التي تظهر في كل من x و y. سنعود إلى هذه التفاصيل بمزيد من العمق لاحقاً.

19.3.3 عمليات الدمج الترشيحية (Filtering joins)

كما قد تخمن، فإن الإجراء الأساسي لـ الدمج الترشيحي هو ترشيح الصفوف. هناك نوعان: الدمج شبه الداخلي (semi-joins) والدمج العكسي (anti-joins). تضم عمليات الدمج شبه الداخلي (Semi-joins) جميع الصفوف في x التي لها مطابقة في y. على سبيل المثال، يمكننا استخدام semi-join لترشيح مجموعة بيانات airports لإظهار مطارات المغادرة فقط:

airports |> 
  semi_join(flights2, join_by(faa == origin))
#> # A tibble: 3 × 8
#>   faa   name                  lat   lon   alt    tz dst   tzone           
#>   <chr> <chr>               <dbl> <dbl> <dbl> <dbl> <chr> <chr>           
#> 1 EWR   Newark Liberty Intl  40.7 -74.2    18    -5 A     America/New_York
#> 2 JFK   John F Kennedy Intl  40.6 -73.8    13    -5 A     America/New_York
#> 3 LGA   La Guardia           40.8 -73.9    22    -5 A     America/New_York

أو الوجهات فقط:

airports |> 
  semi_join(flights2, join_by(faa == dest))
#> # A tibble: 101 × 8
#>   faa   name                     lat    lon   alt    tz dst   tzone          
#>   <chr> <chr>                  <dbl>  <dbl> <dbl> <dbl> <chr> <chr>          
#> 1 ABQ   Albuquerque Internati…  35.0 -107.   5355    -7 A     America/Denver 
#> 2 ACK   Nantucket Mem           41.3  -70.1    48    -5 A     America/New_Yo…
#> 3 ALB   Albany Intl             42.7  -73.8   285    -5 A     America/New_Yo…
#> 4 ANC   Ted Stevens Anchorage…  61.2 -150.    152    -9 A     America/Anchor…
#> 5 ATL   Hartsfield Jackson At…  33.6  -84.4  1026    -5 A     America/New_Yo…
#> 6 AUS   Austin Bergstrom Intl   30.2  -97.7   542    -6 A     America/Chicago
#> # ℹ 95 more rows

عمليات الدمج العكسي (Anti-joins) هي العكس: فهي ترجع جميع الصفوف في x التي ليس لها مطابقة في y. إنها مفيدة لإيجاد القيم المفقودة الضمنية في البيانات، وهو موضوع قسم 18.3. القيم المفقودة ضمناً لا تظهر كـ NAs ولكنها توجد فقط كغياب. على سبيل المثال، يمكننا العثور على الصفوف المفقودة من airports من خلال البحث عن الرحلات الجوية التي ليس لها مطار وصول مطابق:

flights2 |> 
  anti_join(airports, join_by(dest == faa)) |> 
  distinct(dest)
#> # A tibble: 4 × 1
#>   dest 
#>   <chr>
#> 1 BQN  
#> 2 SJU  
#> 3 STT  
#> 4 PSE

أو يمكننا معرفة أرقام الذيل tailnum المفقودة من جدول planes:

flights2 |> 
  anti_join(planes, join_by(tailnum)) |> 
  distinct(tailnum)
#> # A tibble: 722 × 1
#>   tailnum
#>   <chr>  
#> 1 N3ALAA 
#> 2 N3DUAA 
#> 3 N542MQ 
#> 4 N730MQ 
#> 5 N9EAMQ 
#> 6 N532UA 
#> # ℹ 716 more rows

19.3.4 تمارين

  1. أوجد الـ 48 ساعة (على مدار العام بأكمله) التي شهدت أسوأ حالات التأخير. قارنها ببيانات الطقس weather. هل يمكنك رؤية أي أنماط؟

  2. تخيل أنك وجدت أفضل 10 وجهات الأكثر شعبية باستخدام هذا الكود:

    top_dest <- flights2 |> 
      count(dest, sort = TRUE) |> 
      head(10)

    كيف يمكنك العثور على جميع الرحلات الجوية إلى تلك الوجهات؟

  3. هل تمتلك كل رحلة مغادرة بيانات طقس مقابلة لتلك الساعة؟

  4. ما الذي تشترك فيه أرقام الذيل (tail numbers) التي ليس لها سجل مطابق في planes؟ (تلميح: متغير واحد يفسر ~90% من المشاكل.)

  5. أضف عموداً إلى planes يسرد كل شركة طيران carrier قامت بالتحليق بتلك الطائرة. قد تتوقع أن هناك علاقة ضمنية بين الطائرة وشركة الطيران، لأن كل طائرة تُطير بواسطة شركة طيران واحدة. أكد أو ارفض هذه الفرضية باستخدام الأدوات التي تعلمتها في الفصول السابقة.

  6. أضف خط العرض وخط الطول لمطار المغادرة و الوصول إلى flights. هل من الأسهل إعادة تسمية الأعمدة قبل أم بعد الدمج؟

  7. احسب متوسط التأخير حسب الوجهة، ثم ادمجه مع إطار البيانات airports حتى تتمكن من إظهار التوزيع المكاني للتأخيرات. إليك طريقة سهلة لرسم خريطة للولايات المتحدة:

    airports |> 
      semi_join(flights, join_by(faa == dest)) |> 
      ggplot(aes(x = lon, y = lat)) +
        borders("state") +
        geom_point() +
        coord_quickmap()

    قد ترغب في استخدام حجم size أو لون color النقاط لعرض متوسط التأخير لكل مطار.

  8. ماذا حدث في 13 يونيو 2013؟ ارسم خريطة للتأخيرات، ثم استخدم Google للمقارنة مع حالة الطقس.

19.4 كيف تعمل عمليات الدمج؟

الآن بعد أن استخدمت عمليات الدمج عدة مرات، حان الوقت لمعرفة المزيد عن كيفية عملها، مع التركيز على كيفية مطابقة كل صف في x لصفوف في y. سنبدأ بتقديم تمثيل بصري لعمليات الدمج، باستخدام جداول tibble البسيطة المحددة أدناه والموضحة في الشكل 19.2. في هذه الأمثلة، سنستخدم مفتاحاً واحداً يسمى key وعمود قيمة واحداً (val_x و val_y)، ولكن الأفكار تعمم جميعها على مفاتيح متعددة وقيم متعددة.

x <- tribble(
  ~key, ~val_x,
     1, "x1",
     2, "x2",
     3, "x3"
)
y <- tribble(
  ~key, ~val_y,
     1, "y1",
     2, "y2",
     4, "y3"
)
x و y هما إطارا بيانات بـ 2 أعمدة و 3 صفوف، مع محتويات كما هو موضح في النص. قيم المفاتيح ملونة: 1 باللون الأخضر، 2 باللون البنفسجي، 3 باللون البرتقالي، و 4 باللون الأصفر.
الشكل 19.2: تمثيل رسومي لجدولين بسيطين. ترسم أعمدة key الملونة لون الخلفية حسب قيمة المفتاح. تمثل الأعمدة الرمادية أعمدة “القيمة” التي تُحمل معها للرحلة.

يقدم الشكل 19.3 الأساس لتمثيلنا البصري. فهو يظهر جميع التطابقات المحتملة بين x و y كتقاطع بين الخطوط المرسومة من كل صف من x وكل صف من y. تتحدد الصفوف والأعمدة في المخرجات بشكل أساسي بواسطة x، لذا فإن الجدول x أفقي ومحاذٍ للمخرجات.

تم وضع x و y في زوايا قائمة، مع خطوط أفقية تمتد من x وخطوط عمودية تمتد من y. هناك 3 صفوف في x و 3 صفوف في y، مما يؤدي إلى تسعة تقاطعات تمثل تسع تطابقات محتملة.
الشكل 19.3: لفهم كيفية عمل عمليات الدمج، من المفيد التفكير في كل مطابقة محتملة. نعرض ذلك هنا بنسيج شبكي من خطوط التوصيل.

لوصف نوع معين من الدمج، نشير إلى التطابقات بنقاط. تحدد التطابقات الصفوف في المخرجات، وهو إطار بيانات جديد يحتوي على المفتاح وقيم x وقيم y. على سبيل المثال، يظهر الشكل 19.4 دمجاً داخلياً (inner join)، حيث يتم الاحتفاظ بالصفوف إذا وفقط إذا كانت المفاتيح متساوية.

تم وضع x و y في زوايا قائمة مع خطوط تشكل شبكة من التطابقات المحتملة. تظهر المفاتيح 1 و 2 في كل من x و y، لذا نحصل على مطابقة يشار إليها بنقطة. تتوافق كل نقطة مع صف في المخرجات، لذا فإن إطار البيانات المدمج الناتج يحتوي على صفين.
الشكل 19.4: يطابق الدمج الداخلي كل صف في x مع الصف في y الذي يحتوي على نفس قيمة key. تصبح كل مطابقة صفاً في المخرجات.

يمكننا تطبيق نفس المبادئ لشرح عمليات الدمج الخارجي (outer joins)، والتي تحتفظ بالملاحظات التي تظهر في إطار بيانات واحد على الأقل. تعمل عمليات الدمج هذه عن طريق إضافة ملاحظة “افتراضية” إضافية إلى كل إطار بيانات. تحتوي هذه الملاحظة على مفتاح يطابق إذا لم يطابق أي مفتاح آخر، وقيم مملوءة بـ NA. هناك ثلاثة أنواع من عمليات الدمج الخارجي:

  • الدمج الأيسر (Left join) يحتفظ بجميع الملاحظات في x، الشكل 19.5. يتم الحفاظ على كل صف من x في المخرجات لأنه يمكنه الاعتماد على مطابقة صف من NAs في y.
مقارنة بالمخطط السابق الذي يظهر دمجاً داخلياً، يحصل الجدول y على صف افتراضي جديد يحتوي على NA يطابق أي صف في x لم يطابق خلاف ذلك. هذا يعني أن المخرجات تحتوي الآن على ثلاثة صفوف. بالنسبة للمفتاح = 3، الذي يطابق هذا الصف الافتراضي، تأخذ val_y القيمة NA.
الشكل 19.5: تمثيل بصري للدمج الأيسر حيث يظهر كل صف في x في المخرجات.
  • الدمج الأيمن (Right join) يحتفظ بجميع الملاحظات في y، الشكل 19.6. يتم الحفاظ على كل صف من y في المخرجات لأنه يمكنه الاعتماد على مطابقة صف من NAs في x. تظل المخرجات تطابق x قدر الإمكان؛ وتُضاف أي صفوف إضافية من y إلى النهاية.
مقارنة بالمخطط السابق الذي يظهر دمجاً أيسر، يكتسب الجدول x الآن صفاً افتراضياً حتى يحصل كل صف في y على مطابقة في x. تحتوي val_x على NA للصف في y الذي لم يطابق x.
الشكل 19.6: تمثيل بصري للدمج الأيمن حيث يظهر كل صف من y في المخرجات.
  • الدمج الكامل (Full join) يحتفظ بجميع الملاحظات التي تظهر في x أو y، الشكل 19.7. يتم تضمين كل صف من x و y في المخرجات لأن كلاً من x و y يحتويان على صف احتياطي من NAs. مرة أخرى، تبدأ المخرجات بجميع الصفوف من x، تليها صفوف y المتبقية غير المطابقة.
الآن يمتلك كل من x و y صفاً افتراضياً يطابق دائماً. تحتوي النتيجة على 4 صفوف: المفاتيح 1 و 2 و 3 و 4 مع جميع القيم من val_x و val_y، ولكن المفتاح 2 في val_y والمفتاح 4 في val_x هما NAs لأنه لا توجد مطابقة لهذه المفاتيح في إطارات البيانات الأخرى.
الشكل 19.7: تمثيل بصري للدمج الكامل حيث يظهر كل صف في x و y في المخرجات.

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

مخططات فين لعمليات الدمج الداخلي والكامل والأيسر والأيمن. يمثل كل دمج بدائرتين متقاطعتين تمثلان إطاري البيانات x و y، مع وجود x على اليمين و y على اليسار. يشير التظليل إلى نتيجة الدمج.
الشكل 19.8: مخططات فين التي توضح الفرق بين عمليات الدمج الداخلي، والأيسر، والأيمن، والكامل.

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

19.4.1 مطابقة الصفوف

حتى الآن، استكشفنا ما يحدث إذا كان الصف في x يطابق صفراً أو صفاً واحداً في y. ماذا يحدث إذا كان يطابق أكثر من صف واحد؟ لفهم ما يحدث، دعنا نضيق تركيزنا أولاً على inner_join() ثم نرسم صورة، الشكل 19.9.

مخطط دمج حيث تحتوي x على قيم المفاتيح 1 و 2 و 3، وتحتوي y على قيم المفاتيح 1 و 2 و 2. تحتوي المخرجات على ثلاثة صفوف لأن المفتاح 1 يطابق صفاً واحداً، والمفتاح 2 يطابق صفين، والمفتاح 3 يطابق صفر صفوف.
الشكل 19.9: الطرق الثلاث التي يمكن أن يطابق بها صف في x. تطابق x1 صفاً واحداً في y، وتطابق x2 صفين في y، وتطابق x3 صفر صفوف في y. لاحظ أنه بينما يوجد ثلاثة صفوف في x وثلاثة صفوف في المخرجات، لا يوجد تناظر مباشر بين الصفوف.

هناك ثلاث نتائج محتملة لصف في x:

  • إذا لم يطابق أي شيء، يتم إسقاطه.
  • إذا كان يطابق صفاً واحداً في y، يتم الاحتفاظ به.
  • إذا كان يطابق أكثر من صف واحد في y، فيتم تكراره مرة واحدة لكل مطابقة.

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

df1 <- tibble(key = c(1, 2, 2), val_x = c("x1", "x2", "x3"))
df2 <- tibble(key = c(1, 2, 2), val_y = c("y1", "y2", "y3"))

بينما يطابق الصف الأول في df1 صفاً واحداً فقط في df2، فإن الصفين الثاني والثالث يطابقان صفين. يُطلق على هذا أحياناً اسم دمج متعدد إلى متعدد (many-to-many)، وسيؤدي إلى إصدار تحذير من dplyr:

df1 |> 
  inner_join(df2, join_by(key))
#> Warning in inner_join(df1, df2, join_by(key)): Detected an unexpected many-to-many relationship between `x` and `y`.
#> ℹ Row 2 of `x` matches multiple rows in `y`.
#> ℹ Row 2 of `y` matches multiple rows in `x`.
#> ℹ If a many-to-many relationship is expected, set `relationship =
#>   "many-to-many"` to silence this warning.
#> # A tibble: 5 × 3
#>     key val_x val_y
#>   <dbl> <chr> <chr>
#> 1     1 x1    y1   
#> 2     2 x2    y2   
#> 3     2 x2    y3   
#> 4     2 x3    y2   
#> 5     2 x3    y3

إذا كنت تفعل ذلك عن قصد، يمكنك ضبط relationship = "many-to-many"، كما يقترح التحذير.

19.4.2 عمليات الدمج الترشيحية (Filtering joins)

يحدد عدد التطابقات أيضاً سلوك عمليات الدمج الترشيحية. يحتفظ الدمج شبه الداخلي (semi-join) بالصفوف في x التي لها مطابقة واحدة أو أكثر في y، كما في الشكل 19.10. يحتفظ الدمج العكسي (anti-join) بالصفوف في x التي تطابق صفر صفوف في y، كما في الشكل 19.11. في كلتا الحالتين، وجود المطابقة فقط هو المهم؛ لا يهم عدد مرات تطابقها. هذا يعني أن عمليات الدمج الترشيحية لا تكرر الصفوف أبداً كما تفعل عمليات الدمج التحويرية.

مخطط دمج مع صديقينا القديمين x و y. في الدمج شبه الداخلي، يهم فقط وجود مطابقة، لذا تحتوي المخرجات على نفس أعمدة x.
الشكل 19.10: في الدمج شبه الداخلي (semi-join) يهم فقط وجود مطابقة؛ خلاف ذلك لا تؤثر القيم في y على المخرجات.
الدمج العكسي هو عكس الدمج شبه الداخلي، لذا تم رسم التطابقات بخطوط حمراء تشير إلى أنه سيتم إسقاطها من المخرجات.
الشكل 19.11: الدمج العكسي (anti-join) هو عكس الدمج شبه الداخلي، حيث يسقط الصفوف من x التي لها مطابقة في y.

19.5 عمليات الدمج غير المتكافئة (Non-equi joins)

حتى الآن رأيت فقط عمليات الدمج المتكافئة، وهي عمليات الدمج التي تتطابق فيها الصفوف إذا كان مفتاح x يساوي مفتاح y. الآن سنخفف هذا القيد ونناقش طرقاً أخرى لتحديد ما إذا كان زوج من الصفوف متطابقاً.

ولكن قبل أن نتمكن من القيام بذلك، نحتاج إلى مراجعة تبسيط قمنا به أعلاه. في عمليات الدمج المتكافئة تكون مفاتيح x و y متساوية دائماً، لذا نحتاج فقط إلى إظهار واحد منها في المخرجات. يمكننا طلب الاحتفاظ بكلا المفتاحين من dplyr باستخدام keep = TRUE، مما يؤدي إلى الكود أدناه وإعادة رسم inner_join() في الشكل 19.12.

x |> inner_join(y, join_by(key == key), keep = TRUE)
#> # A tibble: 2 × 4
#>   key.x val_x key.y val_y
#>   <dbl> <chr> <dbl> <chr>
#> 1     1 x1        1 y1   
#> 2     2 x2        2 y2
مخطط دمج يظهر دمجاً داخلياً بين x و y. تتضمن النتيجة الآن أربعة أعمدة: key.x و val_x و key.y و val_y. قيم key.x و key.y متطابقة، ولهذا السبب عادةً ما نظهر واحداً فقط.
الشكل 19.12: دمج داخلي يظهر كلاً من مفاتيح x و y في المخرجات.

عندما نبتعد عن عمليات الدمج المتكافئة سنظهر دائماً المفاتيح، لأن قيم المفاتيح غالباً ما تكون مختلفة. على سبيل المثال، بدلاً من المطابقة فقط عندما يكون x$key و y$key متساويين، يمكننا المطابقة كلما كان x$key أكبر من أو يساوي y$key، مما يؤدي إلى الشكل 19.13. تفهم دوال الدمج في dplyr هذا التمييز بين عمليات الدمج المتكافئة وغير المتكافئة، لذا ستظهر دائماً كلا المفتاحين عند إجراء دمج غير متكافئ.

مخطط دمج يوضح join_by(key >= key). تطابق الصف الأول من x صفاً واحداً من y وتطابق الصفوف الثانية والثالثة صفين لكل منهما. هذا يعني أن المخرجات تحتوي على خمسة صفوف تحتوي على كل من أزواج (key.x, key.y) التالية: (1, 1), (2, 1), (2, 2), (3, 1), (3, 2).
الشكل 19.13: دمج غير متكافئ حيث يجب أن يكون مفتاح x أكبر من أو يساوي مفتاح y. تولد العديد من الصفوف تطابقات متعددة.

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

  • عمليات الدمج التقاطعي (Cross joins). تطابق كل زوج من الصفوف.
  • عمليات الدمج غير المتساوية (Inequality joins). تستخدم <, <=, >, و >= بدلاً من ==.
  • عمليات الدمج المتدحرجة (Rolling joins). تشبه عمليات الدمج غير المتساوية ولكنها تجد فقط أقرب مطابقة.
  • عمليات دمج التداخل (Overlap joins). هي نوع خاص من الدمج غير المتساوي مصمم للعمل مع الفترات/المجالات.

يتم وصف كل منها بمزيد من التفصيل في الأقسام التالية.

19.5.1 عمليات الدمج التقاطعي (Cross joins)

يطابق الدمج التقاطعي كل شيء، كما في الشكل 19.14، مما يولد حاصل الضرب الديكارتي للصفوف. هذا يعني أن المخرجات ستكون تحتوي على عدد صفوف يساوي nrow(x) * nrow(y).

مخطط دمج يظهر نقطة لكل تجميعة من x و y.
الشكل 19.14: يطابق الدمج التقاطعي كل صف في x مع كل صف في y.

تكون عمليات الدمج التقاطعي مفيدة عند توليد التباديل. على سبيل المثال، يولد الكود أدناه كل زوج محتمل من الأسماء. بما أننا ندمج df مع نفسه، يطلق على هذا أحياناً اسم الدمج الذاتي (self-join). تستخدم عمليات الدمج التقاطعي دالة دمج مختلفة لأنه لا يوجد تمييز بين داخلي/أيسر/أيمن/كامل عندما تطابق كل صف.

df <- tibble(name = c("John", "Simon", "Tracy", "Max"))
df |> cross_join(df)
#> # A tibble: 16 × 2
#>   name.x name.y
#>   <chr>  <chr> 
#> 1 John   John  
#> 2 John   Simon 
#> 3 John   Tracy 
#> 4 John   Max   
#> 5 Simon  John  
#> 6 Simon  Simon 
#> # ℹ 10 more rows

19.5.2 عمليات الدمج غير المتساوية (Inequality joins)

تستخدم عمليات الدمج غير المتساوية <, <=, >=, أو > بتقييد مجموعة التطابقات المحتملة، كما في الشكل 19.13 و الشكل 19.15.

مخطط يوضح دمجاً غير متساوي حيث يتم دمج إطار البيانات x مع إطار البيانات y حيث تكون قيمة المفتاح لـ x أصغر من قيمة المفتاح لـ y، مما يؤدي إلى شكل مثلث في الزاوية العلوية اليسرى.
الشكل 19.15: دمج غير متساوي حيث يتم دمج x بـ y في الصفوف التي يكون فيها مفتاح x أصغر من مفتاح y. هذا يصنع شكلاً مثلثاً في الزاوية العلوية اليسرى.

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

df <- tibble(id = 1:4, name = c("John", "Simon", "Tracy", "Max"))

df |> inner_join(df, join_by(id < id))
#> # A tibble: 6 × 4
#>    id.x name.x  id.y name.y
#>   <int> <chr>  <int> <chr> 
#> 1     1 John       2 Simon 
#> 2     1 John       3 Tracy 
#> 3     1 John       4 Max   
#> 4     2 Simon      3 Tracy 
#> 5     2 Simon      4 Max   
#> 6     3 Tracy      4 Max

19.5.3 عمليات الدمج المتدحرجة (Rolling joins)

عمليات الدمج المتدحرجة هي نوع خاص من الدمج غير المتساوي حيث بدلاً من الحصول على كل صف يحقق المتباينة، تحصل فقط على أقرب صف، كما في الشكل 19.16. يمكنك تحويل أي دمج غير متساوي إلى دمج متدحرج عن طريق إضافة closest(). على سبيل المثال، join_by(closest(x <= y)) يطابق أصغر y أكبر من أو يساوي x، و join_by(closest(x > y)) يطابق أكبر y أصغر من x.

الدمج المتدحرج هو مجموعة جزئية من دمج عدم المساواة، لذا يتم تظليل بعض التطابقات باللون الرمادي للإشارة إلى عدم استخدامها لأنها ليست "الأقرب".
الشكل 19.16: يشبه الدمج المتدحرج دمج عدم المساواة من نوع أكبر من أو يساوي ولكنه يطابق القيمة الأولى فقط.

عمليات الدمج المتدحرجة مفيدة بشكل خاص عندما يكون لديك جدولان للتواريخ لا يتطابقان تماماً وتريد العثور على (على سبيل المثال) أقرب تاريخ في الجدول 1 يأتي قبل (أو بعد) تاريخ ما في الجدول 2.

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

parties <- tibble(
  q = 1:4,
  party = ymd(c("2022-01-10", "2022-04-04", "2022-07-11", "2022-10-03"))
)

الآن تخيل أن لديك جدولاً لأعياد ميلاد الموظفين:

set.seed(123)
employees <- tibble(
  name = sample(babynames::babynames$name, 100),
  birthday = ymd("2022-01-01") + (sample(365, 100, replace = TRUE) - 1)
)
employees
#> # A tibble: 100 × 2
#>   name     birthday  
#>   <chr>    <date>    
#> 1 Kemba    2022-01-22
#> 2 Orean    2022-06-26
#> 3 Kirstyn  2022-02-11
#> 4 Amparo   2022-11-11
#> 5 Belen    2022-03-25
#> 6 Rayshaun 2022-01-11
#> # ℹ 94 more rows

ولكل موظف نريد العثور على آخر تاريخ حفلة يأتي قبل (أو في) عيد ميلاده. يمكننا التعبير عن ذلك بدمج متدحرج:

employees |> 
  left_join(parties, join_by(closest(birthday >= party)))
#> # A tibble: 100 × 4
#>   name     birthday       q party     
#>   <chr>    <date>     <int> <date>    
#> 1 Kemba    2022-01-22     1 2022-01-10
#> 2 Orean    2022-06-26     2 2022-04-04
#> 3 Kirstyn  2022-02-11     1 2022-01-10
#> 4 Amparo   2022-11-11     4 2022-10-03
#> 5 Belen    2022-03-25     1 2022-01-10
#> 6 Rayshaun 2022-01-11     1 2022-01-10
#> # ℹ 94 more rows

ومع ذلك، هناك مشكلة واحدة في هذا النهج: الأشخاص الذين تقع أعياد ميلادهم قبل 10 يناير لا يحصلون على حفلة:

employees |> 
  anti_join(parties, join_by(closest(birthday >= party)))
#> # A tibble: 2 × 2
#>   name   birthday  
#>   <chr>  <date>    
#> 1 Maks   2022-01-07
#> 2 Nalani 2022-01-04

حل هذه المشكلة سنحتاج إلى معالجة المسألة بطريقة مختلفة، باستخدام عمليات دمج التداخل (overlap joins).

19.5.4 عمليات دمج التداخل (Overlap joins)

توفر عمليات دمج التداخل ثلاث دوال مساعدة تستخدم عمليات الدمج غير المتساوية لجعل العمل مع الفترات أسهل:

  • between(x, y_lower, y_upper) هي اختصار لـ x >= y_lower, x <= y_upper.
  • within(x_lower, x_upper, y_lower, y_upper) هي اختصار لـ x_lower >= y_lower, x_upper <= y_upper.
  • overlaps(x_lower, x_upper, y_lower, y_upper) هي اختصار لـ x_lower <= y_upper, x_upper >= y_lower.

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

parties <- tibble(
  q = 1:4,
  party = ymd(c("2022-01-10", "2022-04-04", "2022-07-11", "2022-10-03")),
  start = ymd(c("2022-01-01", "2022-04-04", "2022-07-11", "2022-10-03")),
  end = ymd(c("2022-04-03", "2022-07-11", "2022-10-02", "2022-12-31"))
)
parties
#> # A tibble: 4 × 4
#>       q party      start      end       
#>   <int> <date>     <date>     <date>    
#> 1     1 2022-01-10 2022-01-01 2022-04-03
#> 2     2 2022-04-04 2022-04-04 2022-07-11
#> 3     3 2022-07-11 2022-07-11 2022-10-02
#> 4     4 2022-10-03 2022-10-03 2022-12-31

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

parties |> 
  inner_join(parties, join_by(overlaps(start, end, start, end), q < q)) |> 
  select(start.x, end.x, start.y, end.y)
#> # A tibble: 1 × 4
#>   start.x    end.x      start.y    end.y     
#>   <date>     <date>     <date>     <date>    
#> 1 2022-04-04 2022-07-11 2022-07-11 2022-10-02

عفواً، هناك تداخل، لذا دعنا نصلح تلك المشكلة ونتابع:

parties <- tibble(
  q = 1:4,
  party = ymd(c("2022-01-10", "2022-04-04", "2022-07-11", "2022-10-03")),
  start = ymd(c("2022-01-01", "2022-04-04", "2022-07-11", "2022-10-03")),
  end = ymd(c("2022-04-03", "2022-07-10", "2022-10-02", "2022-12-31"))
)

الآن يمكننا مطابقة كل موظف مع حفلته. هذا مكان جيد لاستخدام unmatched = "error" لأننا نريد أن نعرف بسرعة ما إذا كان هناك أي موظفين لم يتم تعيين حفلة لهم.

employees |> 
  inner_join(parties, join_by(between(birthday, start, end)), unmatched = "error")
#> # A tibble: 100 × 6
#>   name     birthday       q party      start      end       
#>   <chr>    <date>     <int> <date>     <date>     <date>    
#> 1 Kemba    2022-01-22     1 2022-01-10 2022-01-01 2022-04-03
#> 2 Orean    2022-06-26     2 2022-04-04 2022-04-04 2022-07-10
#> 3 Kirstyn  2022-02-11     1 2022-01-10 2022-01-01 2022-04-03
#> 4 Amparo   2022-11-11     4 2022-10-03 2022-10-03 2022-12-31
#> 5 Belen    2022-03-25     1 2022-01-10 2022-01-01 2022-04-03
#> 6 Rayshaun 2022-01-11     1 2022-01-10 2022-01-01 2022-04-03
#> # ℹ 94 more rows

19.5.5 تمارين

  1. هل يمكنك شرح ما يحدث للمفاتيح في هذا الدمج المتكافئ؟ لماذا هي مختلفة؟
x |> full_join(y, join_by(key == key))
#> # A tibble: 4 × 3
#>     key val_x val_y
#>   <dbl> <chr> <chr>
#> 1     1 x1    y1   
#> 2     2 x2    y2   
#> 3     3 x3    <NA> 
#> 4     4 <NA>  y3

x |> full_join(y, join_by(key == key), keep = TRUE)
#> # A tibble: 4 × 4
#>   key.x val_x key.y val_y
#>   <dbl> <chr> <dbl> <chr>
#> 1     1 x1        1 y1   
#> 2     2 x2        2 y2   
#> 3     3 x3       NA <NA> 
#> 4    NA <NA>      4 y3
  1. عند العثور على ما إذا كانت أي فترة حفلة تتداخل مع فترة حفلة أخرى استخدمنا q < q في join_by()؟ لماذا؟ ماذا يحدث إذا قمت بإزالة عدم المساواة هذه؟

19.6 ملخص

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

يختتم هذا الفصل قسم “التحويل (Transform)” من الكتاب حيث كان التركيز على الأدوات التي يمكنك استخدامها مع الأعمدة الفردية وجداول tibbles. تعلمت عن dplyr والدوال الأساسية للعمل مع المتجهات المنطقية والأعداد والجداول الكاملة، ودوال stringr للعمل مع النصوص، ودوال lubridate للعمل مع التواريخ والأوقات، ودوال forcats للعمل مع العوامل (factors).

في الجزء التالي من الكتاب، ستتعلم المزيد عن إدخال أنواع مختلفة من البيانات إلى R بشكل منسق ونظيف (tidy form).


  1. تذكر أنه يمكنك أيضاً استخدام الدالة View() في RStudio لتجنب هذه المشكلة.↩︎

  2. هذا ليس صحيحاً بنسبة %100، ولكنك ستتلقى تحذيراً كلما لم يكن الأمر كذلك.↩︎