17  التواريخ والأوقات

17.1 مقدمة

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

كنوع من الإحماء، فكر في عدد الأيام الموجودة في السنة، وعدد الساعات في اليوم. ربما تتذكر أن معظم السنوات تحتوي على 365 يوماً، لكن السنوات الكبيسة تحتوي على 366 يوماً. هل تعرف القاعدة الكاملة لتحديد ما إذا كانت السنة كبيسة أم لا1؟ أما عدد الساعات في اليوم فأقل وضوحاً: تحتوي معظم الأيام على 24 ساعة، ولكن في الأماكن التي تستخدم التوقيت الصيفي (DST)، يحتوي يوم واحد كل عام على 23 ساعة ويوم آخر على 25 ساعة.

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

سَنستهلُّ العرض ببيان كيفية إنشاء كائنات التاريخ والوقت (date-times) من مُدخلات مُتنوّعة، وتوضيح كيفية استخراج مُكوّناتها كالعام والشهر واليوم بمجرد تكوينها. بَعْدها، سننتقل إلى تفصيل المسألة الدقيقة المُتعلّقة بالتعامل مع الفترات الزمنية (time spans)، والتي تتخذ أشكالاً (أو تصنيفات) عِدّة تَبَعاً للغرض المرجوّ من تحليلها. وفي الختام، سنستعرض مُناقشة موجزة حول التحدّيات الإضافية التي تطرحها النطاقات الزمنية (time zones).

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

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

17.2 إنشاء التواريخ/الأوقات

هناك ثلاثة أنواع من بيانات التاريخ/الوقت التي تشير إلى لحظة زمنية معينة:

  • تاريخ (Date). تطبعه إطارات البيانات (Tibbles) بالشكل <date>.

  • وقت (Time) خلال اليوم. تطبعه إطارات البيانات بالشكل <time>.

  • تاريخ-وقت (Date-time) وهو تاريخ بالإضافة إلى وقت: حيث يحدد لحظة زمنية فريدة (عادةً لأقرب ثانية). تطبعه إطارات البيانات بالشكل <dttm>. تطلق عليه لغة R الأساسية (Base R) اسم POSIXct، لكن هذا الاسم ليس سهلاً في النطق.

في هذا الفصل سنركز على التواريخ والتواريخ-الأوقات لأن R لا تحتوي على فئة برمجية مدمجة لخزن الأوقات بمفردها. إذا كنت بحاجة إلى واحدة، يمكنك استخدام حزمة hms.

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

للحصول على التاريخ الحالي أو التاريخ والوقت الحاليين، يمكنك استخدام today() أو now():

today()
#> [1] "2026-09-08"
now()
#> [1] "2026-09-08 00:57:00 UTC"

بخلاف ذلك، تصف الأقسام التالية الطرق الأربع التي من المحتمل أن تستخدمها لإنشاء تاريخ/وقت:

  • أثناء قراءة ملف باستخدام readr.
  • من سلسلة نصية (string).
  • من مكونات منفردة للتاريخ والوقت.
  • من كائن تاريخ/وقت حالي.

17.2.1 أثناء الاستيراد

إذا كان ملف CSV الخاص بك يحتوي على تاريخ أو تاريخ-وقت بتنسيق ISO8601، فلن تحتاج إلى فعل أي شيء؛ ستتعرف عليه حزمة readr تلقائياً:

csv <- "
  date,datetime
  2022-01-02,2022-01-02 05:12
"
read_csv(csv)
#> Warning: The `file` argument of `read_csv()` should use `I()` for literal data as of
#> readr 2.2.0.
#>   
#>   # Bad (for example):
#>   read_csv("x,y\n1,2")
#>   
#>   # Good:
#>   read_csv(I("x,y\n1,2"))
#> # A tibble: 1 × 2
#>   date       datetime           
#>   <date>     <dttm>             
#> 1 2022-01-02 2022-01-02 05:12:00

إذا لم تكن قد سمعت بـ ISO8601 من قبل، فهو معيار دولي2 لكتابة التواريخ حيث تُنظم مكونات التاريخ من الأكبر إلى الأصغر تفصيلاً وبينها مفصل -. على سبيل المثال، في ISO8601 يكتب 3 مايو 2022 بالشكل 2022-05-03. يمكن أن تتضمن تواريخ ISO8601 أوقاتاً أيضاً، حيث يفصل بين الساعة والدقيقة والثانية باستخدام :، ويتم الفصل بين مكونات التاريخ والوقت إما بـ T أو بمسافة. على سبيل المثال، يمكنك كتابة الساعة 4:26 مساءً في 3 مايو 2022 إما بالشكل 2022-05-03 16:26 أو 2022-05-03T16:26.

بالنسبة لتنسيقات التاريخ والوقت الأخرى، ستلاحظ حاجتك لاستخدام col_types بالإضافة إلى col_date() أو col_datetime() جنباً إلى جنب مع تنسيق التاريخ والوقت. تنسيق التاريخ والوقت المستخدم بواسطة readr هو معيار مستخدم عبر العديد من لغات البرمجة، حيث يصف مكون التاريخ باستخدام % متبوعاً بحرف واحد. على سبيل المثال، يحدد %Y-%m-%d تاريخاً يتكون من سنة، -، شهر (كرقم) -، يوم. يعرض الجدول 17.1 جميع الخيارات المتاحة.

الجدول 17.1: جميع تنسيقات التواريخ التي تفهمها readr
النوع الرمز المعنى مثال
السنة %Y سنة من 4 أرقام 2021
%y سنة من رقمين 21
الشهر %m رقم 2
%b الاسم المختصر Feb
%B الاسم الكامل February
اليوم %d رقم أو رقمين 2
%e رقمين 02
الوقت %H ساعة بنظام 24 ساعة 13
%I ساعة بنظام 12 ساعة 1
%p صباحاً/مساءً (AM/PM) pm
%M دقائق 35
%S ثواني 45
%OS ثواني مع أجزاء عشرية 45.35
%Z اسم المنطقة الزمنية America/Chicago
%z الفارق الزمني عن UTC +0800
أخرى %. تخطي رمز واحد غير رقمي :
%* تخطي أي عدد من الرموز غير الرقمية

ويعرض هذا الكود بضعة خيارات مطبقة على تاريخ غامض للغاية:

csv <- "
  date
  01/02/15
"

read_csv(csv, col_types = cols(date = col_date("%m/%d/%y")))
#> # A tibble: 1 × 1
#>   date      
#>   <date>    
#> 1 2015-01-02

read_csv(csv, col_types = cols(date = col_date("%d/%m/%y")))
#> # A tibble: 1 × 1
#>   date      
#>   <date>    
#> 1 2015-02-01

read_csv(csv, col_types = cols(date = col_date("%y/%m/%d")))
#> # A tibble: 1 × 1
#>   date      
#>   <date>    
#> 1 2001-02-15

لاحظ أنه بغض النظر عن كيفية تحديد تنسيق التاريخ، فإنه يُعرض دائماً بنفس الطريقة بمجرد إدخاله إلى R.

إذا كنت تستخدم %b أو %B وتعمل مع تواريخ بلغات غير الإنجليزية، فستحتاج أيضاً إلى توفير بيئة محليّة locale(). راجع قائمة اللغات المدمجة في date_names_langs()، أو أنشئ لغتك الخاصة بـ date_names().

17.2.2 من النصوص

لغة تحديد التاريخ والوقت قوية، ولكنها تتطلب تحليلاً دقيقاً لتنسيق التاريخ. هناك نهج بديل يتمثل في استخدام الدوال المساعدة لـ lubridate والتي تحاول تحديد التنسيق تلقائياً بمجرد تحديد ترتيب المكونات. لاستخدامها، حدد الترتيب الذي تظهر به السنة والشهر واليوم في تواريخك، ثم رتب “y” و “m” و “d” بنفس الترتيب. يعطيك هذا اسم دالة lubridate التي ستقوم بتحليل تاريخك. على سبيل المثال:

ymd("2017-01-31")
#> [1] "2017-01-31"
mdy("January 31st, 2017")
#> [1] "2017-01-31"
dmy("31-Jan-2017")
#> [1] "2017-01-31"

ymd() وأخواتها تقوم بإنشاء تواريخ. ولإنشاء تاريخ-وقت، أضف خطاً سفلياً (underscore) وواحداً أو أكثر من الأحرف “h” و “m” و “s” إلى اسم دالة التحليل:

ymd_hms("2017-01-31 20:11:59")
#> [1] "2017-01-31 20:11:59 UTC"
mdy_hm("01/31/2017 08:01")
#> [1] "2017-01-31 08:01:00 UTC"

يمكنك أيضاً إجبار إنشاء تاريخ-وقت من تاريخ ببساطة عن طريق تزويده بمنطقة زمنية:

ymd("2017-01-31", tz = "UTC")
#> [1] "2017-01-31 UTC"

هنا استخدمت المنطقة الزمنية UTC3 والتي قد تعرفها أيضاً باسم GMT، أو توقيت غرينتش، وهو الوقت عند خط طول 0°4. وهي لا تستخدم التوقيت الصيفي، مما يجعل الحساب بها أسهل قليلاً.

17.2.3 من المكونات المنفردة

بدلاً من سلسلة نصية واحدة، ستكون لديك أحياناً المكونات المنفردة للتاريخ والوقت موزعة عبر أعمدة متعددة. هذا هو ما لدينا في بيانات flights:

flights |> 
  select(year, month, day, hour, minute)
#> # A tibble: 336,776 × 5
#>    year month   day  hour minute
#>   <int> <int> <int> <dbl>  <dbl>
#> 1  2013     1     1     5     15
#> 2  2013     1     1     5     29
#> 3  2013     1     1     5     40
#> 4  2013     1     1     5     45
#> 5  2013     1     1     6      0
#> 6  2013     1     1     5     58
#> # ℹ 336,770 more rows

لإنشاء تاريخ/وقت من هذا النوع من المدخلات، استخدم make_date() للتواريخ، أو make_datetime() للتواريخ-الأوقات:

flights |> 
  select(year, month, day, hour, minute) |> 
  mutate(departure = make_datetime(year, month, day, hour, minute))
#> # A tibble: 336,776 × 6
#>    year month   day  hour minute departure          
#>   <int> <int> <int> <dbl>  <dbl> <dttm>             
#> 1  2013     1     1     5     15 2013-01-01 05:15:00
#> 2  2013     1     1     5     29 2013-01-01 05:29:00
#> 3  2013     1     1     5     40 2013-01-01 05:40:00
#> 4  2013     1     1     5     45 2013-01-01 05:45:00
#> 5  2013     1     1     6      0 2013-01-01 06:00:00
#> 6  2013     1     1     5     58 2013-01-01 05:58:00
#> # ℹ 336,770 more rows

دعنا نفعل الشيء نفسه لكل أعمدة الوقت الأربعة في flights. تم تمثيل الأوقات بتنسيق غريب قليلاً، لذا نستخدم حساب باقي القسمة (modulus arithmetic) لاستخراج مكونات الساعة والدقيقة. بمجرد إنشاء متغيرات التاريخ والوقت، نركز على المتغيرات التي سنستكشفها في بقية الفصل.

make_datetime_100 <- function(year, month, day, time) {
  make_datetime(year, month, day, time %/% 100, time %% 100)
}

flights_dt <- flights |> 
  filter(!is.na(dep_time), !is.na(arr_time)) |> 
  mutate(
    dep_time = make_datetime_100(year, month, day, dep_time),
    arr_time = make_datetime_100(year, month, day, arr_time),
    sched_dep_time = make_datetime_100(year, month, day, sched_dep_time),
    sched_arr_time = make_datetime_100(year, month, day, sched_arr_time)
  ) |> 
  select(origin, dest, ends_with("delay"), ends_with("time"))

flights_dt
#> # A tibble: 328,063 × 9
#>   origin dest  dep_delay arr_delay dep_time            sched_dep_time     
#>   <chr>  <chr>     <dbl>     <dbl> <dttm>              <dttm>             
#> 1 EWR    IAH           2        11 2013-01-01 05:17:00 2013-01-01 05:15:00
#> 2 LGA    IAH           4        20 2013-01-01 05:33:00 2013-01-01 05:29:00
#> 3 JFK    MIA           2        33 2013-01-01 05:42:00 2013-01-01 05:40:00
#> 4 JFK    BQN          -1       -18 2013-01-01 05:44:00 2013-01-01 05:45:00
#> 5 LGA    ATL          -6       -25 2013-01-01 05:54:00 2013-01-01 06:00:00
#> 6 EWR    ORD          -4        12 2013-01-01 05:54:00 2013-01-01 05:58:00
#> # ℹ 328,057 more rows
#> # ℹ 3 more variables: arr_time <dttm>, sched_arr_time <dttm>, …

باستخدام هذه البيانات، يمكننا تصور توزيع أوقات المغادرة على مدار العام:

flights_dt |> 
  ggplot(aes(x = dep_time)) + 
  geom_freqpoly(binwidth = 86400) # 86400 seconds = 1 day

مضلع تكراري يوضح وقت المغادرة (يناير - ديسمبر 2013) على المحور الأفقي وعدد الرحلات الجوية على المحور الرأسي (0-1000). المضلع التكراري مقسم حسب اليوم بحيث ترى سلسلة زمنية للرحلات الجوية يومياً. النمط يهيمن عليه نمط أسبوعي؛ حيث توجد رحلات أقل في عطلات نهاية الأسبوع. هناك أيام قليلة تبرز بحصولها على عدد قليل بشكل مفاجئ من الرحلات في أوائل فبراير، وأوائل يوليو، وأواخر نوفمبر، وأواخر ديسمبر.

أو خلال يوم واحد:

flights_dt |> 
  filter(dep_time < ymd(20130102)) |> 
  ggplot(aes(x = dep_time)) + 
  geom_freqpoly(binwidth = 600) # 600 seconds = 10 minutes

مضلع تكراري يوضح وقت المغادرة (من 6 صباحاً إلى منتصف الليل في 1 يناير) على المحور الأفقي، وعدد الرحلات الجوية على المحور الرأسي (0-17)، مقسماً إلى فترات من 10 دقائق. من الصعب رؤية نمط واضح بسبب التباين العالي، ولكن معظم الفترات تحتوي على 8-12 رحلة، وهناك رحلات أقل بكثير قبل الساعة 6 صباحاً وبعد الساعة 8 مساءً.

لاحظ أنه عند استخدام التواريخ-الأوقات في سياق عددي (كما هو الحال في الرسم البياني التكراري)، فإن الرقم 1 يعني ثانية واحدة، لذا فإن اتساع الفئة (binwidth) البالغ 86400 يعني يوماً واحداً. أما بالنسبة للتواريخ، فإن 1 يعني يوماً واحداً.

17.2.4 من أنواع أخرى

قد ترغب في التحويل بين تاريخ-وقت وتاريخ. هذه هي وظيفة as_datetime() و as_date():

as_datetime(today())
#> [1] "2026-09-08 UTC"
as_date(now())
#> [1] "2026-09-08"

في بعض الأحيان ستحصل على التواريخ/الأوقات كقيم عددية تزاح عن “حقبة يونكس” (Unix Epoch)، وهي 1970-01-01. إذا كانت إزاحة التوقيت بالثواني، استخدم as_datetime()؛ وإذا كانت بالأيام، استخدم as_date().

as_datetime(60 * 60 * 10)
#> [1] "1970-01-01 10:00:00 UTC"
as_date(365 * 10 + 2)
#> [1] "1980-01-01"

17.2.5 تمارين

  1. ماذا يحدث إذا قمت بتحليل سلسلة نصية تحتوي على تواريخ غير صالحة؟
ymd(c("2010-10-10", "bananas"))
  1. ماذا يفعل المعامل tzone في الدالة today()؟ لماذا هو مهم؟

  2. لكل من التواريخ والأوقات التالية، وضح كيف ستحدد تحليلها باستخدام مواصفات أعمدة readr ودالة من lubridate.

d1 <- "January 1, 2010"
d2 <- "2015-Mar-07"
d3 <- "06-Jun-2017"
d4 <- c("August 19 (2015)", "July 1 (2015)")
d5 <- "12/30/14" # Dec 30, 2014
t1 <- "1705"
t2 <- "11:15:10.12 PM"

17.3 مكونات التاريخ والوقت

الآن بعد أن عرفت كيفية إدخال بيانات التاريخ والوقت في هياكل بيانات R، دعنا نستكشف ما يمكنك فعله بها. سيركز هذا القسم على دوال الوصول (accessor functions) التي تتيح لك الحصول على المكونات الفردية وتعديلها. بينما سينظر القسم التالي في كيفية عمل العمليات الحسابية مع التواريخ والأوقات.

17.3.1 الحصول على المكونات

يمكنك استخراج أجزاء منفردة من التاريخ باستخدام دوال الوصول مثل year() (السنة)، و month() (الشهر)، و mday() (يوم الشهر)، و yday() (يوم السنة)، و wday() (يوم الأسبوع)، و hour() (الساعة)، و minute() (الدقيقة)، و second() (الثانية). تعتبر هذه الدوال عملياً عكس دالة make_datetime().

datetime <- ymd_hms("2026-07-08 12:34:56")

year(datetime)
#> [1] 2026
month(datetime)
#> [1] 7
mday(datetime)
#> [1] 8

yday(datetime)
#> [1] 189
wday(datetime)
#> [1] 4

بالنسبة للدالتين month() و wday()، يمكنك ضبط label = TRUE لإرجاع الاسم المختصر للشهر أو اليوم. وضبط abbr = FALSE لإرجاع الاسم الكامل.

month(datetime, label = TRUE)
#> [1] Jul
#> 12 Levels: Jan < Feb < Mar < Apr < May < Jun < Jul < Aug < Sep < ... < Dec
wday(datetime, label = TRUE, abbr = FALSE)
#> [1] Wednesday
#> 7 Levels: Sunday < Monday < Tuesday < Wednesday < Thursday < ... < Saturday

يمكننا استخدام wday() لرؤية أن المزيد من الرحلات الجوية تغادر خلال أيام الأسبوع مقارنة بعطلة نهاية الأسبوع:

flights_dt |> 
  mutate(wday = wday(dep_time, label = TRUE)) |> 
  ggplot(aes(x = wday)) +
  geom_bar()

مخطط شريطي مع أيام الأسبوع على المحور الأفقي وعدد الرحلات الجوية على المحور الرأسي. الأيام من الاثنين إلى الجمعة لديها تقريباً نفس عدد الرحلات، ~48,000، مع انخفاض طفيف على مدار الأسبوع. الأحد أقل قليلاً (~45,000)، والسبت أقل بكثير (~38,000).

يمكننا أيضاً النظر في متوسط تأخير المغادرة حسب الدقيقة داخل الساعة. هناك نمط مثير للاهتمام: الرحلات المغادرة في الدقائق 20-30 و 50-60 لديها تأخيرات أقل بكثير من بقية الساعة!

flights_dt |> 
  mutate(minute = minute(dep_time)) |> 
  group_by(minute) |> 
  summarize(
    avg_delay = mean(dep_delay, na.rm = TRUE),
    n = n()
  ) |> 
  ggplot(aes(x = minute, y = avg_delay)) +
  geom_line()

مخطط خطي مع دقيقة المغادرة الفعلية (0-60) على المحور الأفقي و متوسط التأخير (4-20) على المحور الرأسي. يتبين أن متوسط التأخير يبدأ عند (0, 12)، ويرتفع بثبات إلى (18, 20)، ثم ينخفض بشكل حاد ليبلغ أدنى مستوى له عند ~23 دقيقة بعد الساعة و 9 دقائق من التأخير. ثم يرتفع مرة أخرى إلى (17, 35)، وينخفض بحدة إلى (55, 4). وينتهي بزيادة إلى (60, 9).

ومن المثير للاهتمام أننا إذا نظرنا إلى وقت المغادرة المجدول، فلن نرى مثل هذا النمط القوي:

sched_dep <- flights_dt |> 
  mutate(minute = minute(sched_dep_time)) |> 
  group_by(minute) |> 
  summarize(
    avg_delay = mean(arr_delay, na.rm = TRUE),
    n = n()
  )

ggplot(sched_dep, aes(x = minute, y = avg_delay)) +
  geom_line()

مخطط خطي مع دقيقة المغادرة المجدولة (0-60) على المحور الأفقي ومتوسط التأخير (4-16). هناك نمط غير واضح نسبيًا، فقط إشارة صغيرة إلى أن متوسط التأخير ينخفض من ربما 10 دقائق إلى 8 دقائق على مدار الساعة.

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

مخطط خطي مع دقيقة المغادرة (0-60) على المحور الأفقي وعدد الرحلات الجوية (0-60000) على المحور الرأسي. تم جدولة معظم الرحلات للمغادرة إما في رأس الساعة (~60,000) أو نصف الساعة (~35,000). بخلاف ذلك، تقريباً كل الرحلات مجدولة للمغادرة عند مضاعفات الخمسة، مع وجود عدد إضافي قليل عند الدقائق 15 و 45 و 55.
الشكل 17.1: مضلع تكراري يوضح عدد الرحلات المجدولة لـ للمغادرة كل ساعة. يمكنك رؤية تفضيل قوي للأرقام المدورة مثل 0 و 30 وبشكل عام للأرقام التي تضاعف الخمسة.

17.3.2 التقريب

هناك نهج بديل لرسم المكونات الفردية وهو تقريب التاريخ إلى وحدة زمنية قريبة، وذلك باستخدام floor_date()، و round_date()، و ceiling_date(). تأخذ كل دالة متجهاً من التواريخ لضبطه، ثم اسم الوحدة للتقريب للأدنى (floor)، أو للأعلى (ceiling)، أو للتقريب الأقرب. هذا يتلاءم، على سبيل المثال، مع رسم عدد الرحلات الجوية الأسبوعية:

flights_dt |> 
  count(week = floor_date(dep_time, "week")) |> 
  ggplot(aes(x = week, y = n)) +
  geom_line() + 
  geom_point()

مخطط خطي يوضح الأسبوع (يناير - ديسمبر 2013) على المحور الأفقي وعدد الرحلات (2,000-7,000) على المحور الرأسي. النمط مستقر إلى حد ما من فبراير إلى نوفمبر مع حوالي 7,000 رحلة أسبوعياً. هناك رحلات أقل بكثير في الأسبوع الأول (حوالي 4,500 رحلة) والأخير من السنة (حوالي 2,500 رحلة).

يمكنك استخدام التقريب لإظهار توزيع الرحلات على مدار اليوم عن طريق حساب الفرق بين dep_time وأبكر لحظة في ذلك اليوم:

flights_dt |> 
  mutate(dep_hour = dep_time - floor_date(dep_time, "day")) |> 
  ggplot(aes(x = dep_hour)) +
  geom_freqpoly(binwidth = 60 * 30)
#> Don't know how to automatically pick scale for object of type <difftime>.
#> Defaulting to continuous.

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

ينتج عن حساب الفرق بين زوج من التواريخ-الأوقات كائن من نوع difftime (المزيد حول ذلك في قسم 17.4.3). يمكننا تحويل ذلك إلى كائن hms للحصول على محور أفقي أكثر فائدة:

flights_dt |> 
  mutate(dep_hour = hms::as_hms(dep_time - floor_date(dep_time, "day"))) |> 
  ggplot(aes(x = dep_hour)) +
  geom_freqpoly(binwidth = 60 * 30)

مخطط خطي مع وقت المغادرة (من منتصف الليل إلى منتصف الليل) على المحور الأفقي وعدد الرحلات على المحور الرأسي (0 إلى 15,000). هناك عدد قليل جداً (<100) من الرحلات قبل الساعة 5 صباحاً. ثم يرتفع عدد الرحلات بسرعة إلى 12,000 / ساعة، ليصل إلى ذروته عند 15,000 في الساعة 9 صباحاً، قبل أن ينخفض إلى حوالي 8,000 / ساعة من الساعة 10 صباحاً حتى 2 ظهراً. ثم يرتفع عدد الرحلات إلى حوالي 12,000 في الساعة حتى الساعة 8 مساءً، عندما ينخفض بسرعة مرة أخرى.

17.3.3 تعديل المكونات

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

(datetime <- ymd_hms("2026-07-08 12:34:56"))
#> [1] "2026-07-08 12:34:56 UTC"

year(datetime) <- 2030
datetime
#> [1] "2030-07-08 12:34:56 UTC"
month(datetime) <- 01
datetime
#> [1] "2030-01-08 12:34:56 UTC"
hour(datetime) <- hour(datetime) + 1
datetime
#> [1] "2030-01-08 13:34:56 UTC"

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

update(datetime, year = 2030, month = 2, mday = 2, hour = 2)
#> [1] "2030-02-02 02:34:56 UTC"

إذا كانت القيم كبيرة جداً، فسوف تتجاوز وتنتقل إلى المكون الأعلى (roll-over):

update(ymd("2023-02-01"), mday = 30)
#> [1] "2023-03-02"
update(ymd("2023-02-01"), hour = 400)
#> [1] "2023-02-17 16:00:00 UTC"

17.3.4 تمارين

  1. كيف يتغير توزيع أوقات الرحلات الجوية خلال اليوم على مدار العام؟

  2. قارن بين dep_time و sched_dep_time و dep_delay. هل هي متسقة؟ اشرح ما وجدته.

  3. قارن بين air_time والمدة الزمنية بين المغادرة والوصول. اشرح ما وجدته. (تلميح: ألقِ نظرة على موقع المطار.)

  4. كيف يتغير متوسط وقت التأخير على مدار اليوم؟ هل يجب عليك استخدام dep_time أم sched_dep_time؟ ولماذا؟

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

  6. ما الذي يجعل توزيع diamonds$carat و flights$sched_dep_time متشابهاً؟

  7. تأكد من صحة فرضيتنا القائلة بأن المغادرة المبكرة للرحلات في الدقائق 20-30 و 50-60 ناتجة عن رحلات مجدولة غادرت مبكراً. تلميح: أنشئ متغيراً ثنائياً (binary variable) يخبرك ما إذا كانت الرحلة قد تأخرت أم لا.

17.4 الفترات الزمنية

بعد ذلك، ستتعلم كيف تعمل العمليات الحسابية مع التواريخ، بما في ذلك الطرح والجمع والقسمة. وفي الطريق، ستتعلم عن ثلاث فئات مهمة تمثل الفترات الزمنية:

  • المدد (Durations)، والتي تمثل عدداً دقيقاً من الثواني.
  • المُدد الشخصية/التقويمية (Periods)، والتي تمثل الوحدات البشرية مثل الأسابيع والأشهر.
  • الفواصل الزمنية (Intervals)، والتي تمثل نقطة بداية ونهاية.

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

17.4.1 المدد (Durations)

في R، عندما تطرح تاريخين، تحصل على كائن من نوع difftime:

# كم عمر هادلي؟
h_age <- today() - ymd("1979-10-14")
h_age
#> Time difference of 17131 days

يسجل كائن فئة difftime فترة زمنية بالثواني، الدقائق، الساعات، الأيام، أو الأسابيع. هذا الغموض يمكن أن يجعل التعامل مع difftimes مزعجاً قليلاً، لذا توفر lubridate بديلاً يستخدم الثواني دائماً: duration.

as.duration(h_age)
#> [1] "1480118400s (~46.9 years)"

تأتي المدد مع مجموعة من دوال الإنشاء المريحة:

dseconds(15)
#> [1] "15s"
dminutes(10)
#> [1] "600s (~10 minutes)"
dhours(c(12, 24))
#> [1] "43200s (~12 hours)" "86400s (~1 days)"
ddays(0:5)
#> [1] "0s"                "86400s (~1 days)"  "172800s (~2 days)"
#> [4] "259200s (~3 days)" "345600s (~4 days)" "432000s (~5 days)"
dweeks(3)
#> [1] "1814400s (~3 weeks)"
dyears(1)
#> [1] "31557600s (~1 years)"

تسجل المدد دائماً الفترة الزمنية بالثواني. تُنشأ الوحدات الأكبر عن طريق تحويل الدقائق، الساعات، الأيام، الأسابيع، والسنوات إلى ثوانٍ: 60 ثانية في الدقيقة، 60 دقيقة في الساعة، 24 ساعة في اليوم، و7 أيام في الأسبوع. الوحدات الزمنية الأكبر أكثر إشكالية. تستخدم السنة عدد الأيام “المتوسط” في السنة، أي 365.25. لا توجد طريقة لتحويل شهر إلى مدة (duration) لأن هناك الكثير من التباين.

يمكنك جمع المدد ومضاعفتها:

2 * dyears(1)
#> [1] "63115200s (~2 years)"
dyears(1) + dweeks(12) + dhours(15)
#> [1] "38869200s (~1.23 years)"

يمكنك إضافة وطرح المدد من وإلى التواريخ:

tomorrow <- today() + ddays(1)
last_year <- today() - dyears(1)

ومع ذلك، لأن المدد تمثل عدداً دقيقاً من الثواني، قد تحصل أحياناً على نتيجة غير متوقعة:

one_am <- ymd_hms("2026-03-08 01:00:00", tz = "America/New_York")

one_am
#> [1] "2026-03-08 01:00:00 EST"
one_am + ddays(1)
#> [1] "2026-03-09 02:00:00 EDT"

لماذا أصبح اليوم التالي لـ 8 مارس الساعة 1 صباحاً هو 9 مارس الساعة 2 صباحاً؟ إذا نظرت بعناية إلى التاريخ، فقد تلاحظ أيضاً أن المناطق الزمنية قد تغيرت. يحتوي يوم 8 مارس على 23 ساعة فقط لأنه اليوم الذي يبدأ فيه التوقيت الصيفي (DST)، لذا إذا أضفنا ثواني يوم كامل، فسننتهي بوقت مختلف.

17.4.2 المُدد التقويمية (Periods)

لحل هذه المشكلة، توفر lubridate periods. الفترات (Periods) هي مدد زمنية ولكن ليس لها طول ثابت بالثواني، وبدلاً من ذلك تعمل مع الأوقات “البشرية”، مثل الأيام والأشهر. يتيح لها ذلك العمل بطريقة أكثر سهولة وبداهة:

one_am
#> [1] "2026-03-08 01:00:00 EST"
one_am + days(1)
#> [1] "2026-03-09 01:00:00 EDT"

مثل المدد (durations)، يمكن إنشاء الفترات (periods) باستخدام عدد من دوال الإنشاء السهلة.

hours(c(12, 24))
#> [1] "12H 0M 0S" "24H 0M 0S"
days(7)
#> [1] "7d 0H 0M 0S"
months(1:6)
#> [1] "1m 0d 0H 0M 0S" "2m 0d 0H 0M 0S" "3m 0d 0H 0M 0S" "4m 0d 0H 0M 0S"
#> [5] "5m 0d 0H 0M 0S" "6m 0d 0H 0M 0S"

يمكنك جمع ومضاعفة الفترات:

10 * (months(6) + days(1))
#> [1] "60m 10d 0H 0M 0S"
days(50) + hours(25) + minutes(2)
#> [1] "50d 25H 2M 0S"

وبالطبع، إضافتها إلى التواريخ. مقارنة بالمدد (durations)، من المرجح أن تفعل الفترات (periods) ما تتوقعه تماماً:

# سنة كبيسة
ymd("2024-01-01") + dyears(1)
#> [1] "2024-12-31 06:00:00 UTC"
ymd("2024-01-01") + years(1)
#> [1] "2025-01-01"

# التوقيت الصيفي
one_am + ddays(1)
#> [1] "2026-03-09 02:00:00 EDT"
one_am + days(1)
#> [1] "2026-03-09 01:00:00 EDT"

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

flights_dt |> 
  filter(arr_time < dep_time) 
#> # A tibble: 10,633 × 9
#>   origin dest  dep_delay arr_delay dep_time            sched_dep_time     
#>   <chr>  <chr>     <dbl>     <dbl> <dttm>              <dttm>             
#> 1 EWR    BQN           9        -4 2013-01-01 19:29:00 2013-01-01 19:20:00
#> 2 JFK    DFW          59        NA 2013-01-01 19:39:00 2013-01-01 18:40:00
#> 3 EWR    TPA          -2         9 2013-01-01 20:58:00 2013-01-01 21:00:00
#> 4 EWR    SJU          -6       -12 2013-01-01 21:02:00 2013-01-01 21:08:00
#> 5 EWR    SFO          11       -14 2013-01-01 21:08:00 2013-01-01 20:57:00
#> 6 LGA    FLL         -10        -2 2013-01-01 21:20:00 2013-01-01 21:30:00
#> # ℹ 10,627 more rows
#> # ℹ 3 more variables: arr_time <dttm>, sched_arr_time <dttm>, …

هذه رحلات ليلية (تستمر عبر الليل). لقد استخدمنا نفس معلومات التاريخ لكل من أوقات المغادرة والوصول، لكن هذه الرحلات وصلت في اليوم التالي. يمكننا إصلاح ذلك عن طريق إضافة days(1) إلى وقت الوصول لكل رحلة ليلية.

flights_dt <- flights_dt |> 
  mutate(
    overnight = arr_time < dep_time,
    arr_time = arr_time + days(overnight),
    sched_arr_time = sched_arr_time + days(overnight)
  )

الآن تخضع جميع رحلاتنا لقوانين الفيزياء.

flights_dt |> 
  filter(arr_time < dep_time) 
#> # A tibble: 0 × 10
#> # ℹ 10 variables: origin <chr>, dest <chr>, dep_delay <dbl>,
#> #   arr_delay <dbl>, dep_time <dttm>, sched_dep_time <dttm>, …

17.4.3 الفواصل الزمنية

ماذا ترجع نتيجة dyears(1) / ddays(365)؟ إنها ليست واحداً تماماً، لأن dyears() مُعرَّفة بعدد الثواني في متوسط السنة، وهو 365.25 يوماً.

ماذا ترجع نتيجة years(1) / days(1)؟ حسناً، إذا كانت السنة 2015، فيجب أن ترجع 365، ولكن إذا كانت 2016، فيجب أن ترجع 366! لا توجد معلومات كافية لـ lubridate لإعطاء إجابة واحدة واضحة. ما تفعله بدلاً من ذلك هو إعطاء تقدير:

years(1) / days(1)
#> [1] 365.25

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

يمكنك إنشاء فاصل زمني عن طريق كتابة start %--% end:

y2023 <- ymd("2023-01-01") %--% ymd("2024-01-01")
y2024 <- ymd("2024-01-01") %--% ymd("2025-01-01")

y2023
#> [1] 2023-01-01 UTC--2024-01-01 UTC
y2024
#> [1] 2024-01-01 UTC--2025-01-01 UTC

يمكنك بعد ذلك قسمته على days() لمعرفة عدد الأيام التي تناسب السنة:

y2023 / days(1)
#> [1] 365
y2024 / days(1)
#> [1] 366

17.4.4 تمارين

  1. اشرح معنى days(!overnight) و days(overnight) لشخص بدأ للتو في تعلم R. ما هي الحقيقة الرئيسية التي تحتاج إلى معرفتها؟

  2. أنشئ متجهاً من التواريخ يعطي اليوم الأول من كل شهر في عام 2015. أنشئ متجهاً من التواريخ يعطي اليوم الأول من كل شهر في السنة الحالية.

  3. اكتب دالة تتلقى تاريخ ميلادك (كتاريخ)، وترجع عمرك بالسنين.

17.5 المناطق الزمنية

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

التحدي الأول هو أن الأسماء اليومية للمناطق الزمنية تميل إلى أن تكون غامضة. على سبيل المثال، إذا كنت أمريكياً، فأنت على الأرجح معتاد على EST، أو التوقيت القياسي الشرقي. ومع ذلك، تمتلك كل من أستراليا وكندا أيضاً EST! لتجنب الارباك، تستخدم R مناطق IANA الزمنية المعيارية الدولية. تستخدم هذه الأسماء مخطط تسمية متسق وهو {area}/{location}، وعادة ما تكون بالشكل {continent}/{city} أو {ocean}/{city}. وتشمل الأمثلة “America/New_York” و “Europe/Paris” و “Pacific/Auckland”.

قد تتساءل لماذا تستخدم المنطقة الزمنية اسم مدينة، بينما تفكر عادة في المناطق الزمنية كشيء مرتبط بدولة أو منطقة داخل دولة. هذا لأن قاعدة بيانات IANA يجب أن تسجل قواعد المناطق الزمنية لعقود من الزمان. على مدار العقود، تتغير أسماء الدول (أو تتفكك) بشكل متكرر، لكن أسماء المدن تميل إلى البقاء كما هي. مشكلة أخرى هي أن الاسم يجب أن يعكس ليس فقط السلوك الحالي، ولكن أيضاً التاريخ الكامل. على سبيل المثال، هناك مناطق زمنية لكل من “America/New_York” و “America/Detroit”. تستخدم هاتان المدينتان حالياً التوقيت القياسي الشرقي، ولكن في 1969-1972، لم تتبع ميشيغان (الولاية التي تقع فيها ديترويت) التوقيت الصيفي، لذا كانت بحاجة إلى اسم مختلف. من المفيد قراءة قاعدة بيانات المناطق الزمنية الخام (المتاحة في https://www.iana.org/time-zones) لمجرد قراءة بعض هذه القصص!

يمكنك معرفة ما تعتقده R حول منطقتك الزمنية الحالية باستخدام Sys.timezone():

Sys.timezone()
#> [1] "UTC"

(إذا كانت R لا تعرف، فستحصل على NA.)

ولرؤية القائمة الكاملة لجميع أسماء المناطق الزمنية استخدم OlsonNames():

length(OlsonNames())
#> [1] 598
head(OlsonNames())
#> [1] "Africa/Abidjan"     "Africa/Accra"       "Africa/Addis_Ababa"
#> [4] "Africa/Algiers"     "Africa/Asmara"      "Africa/Asmera"

في R، المنطقة الزمنية هي خاصية (attribute) للتاريخ والوقت تتحكم فقط في طريقة الطباعة والعرض. على سبيل المثال، تمثل هذه الكائنات الثلاثة نفس اللحظة الزمنية:

x1 <- ymd_hms("2024-06-01 12:00:00", tz = "America/New_York")
x1
#> [1] "2024-06-01 12:00:00 EDT"

x2 <- ymd_hms("2024-06-01 18:00:00", tz = "Europe/Copenhagen")
x2
#> [1] "2024-06-01 18:00:00 CEST"

x3 <- ymd_hms("2024-06-02 04:00:00", tz = "Pacific/Auckland")
x3
#> [1] "2024-06-02 04:00:00 NZST"

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

x1 - x2
#> Time difference of 0 secs
x1 - x3
#> Time difference of 0 secs

ما لم يُحدد خلاف ذلك، تستخدم lubridate دائماً UTC. UTC (التوقيت العالمي المنسق) هو المنطقة الزمنية القياسية المستخدمة من قبل المجتمع العلمي وتكافئ تقريباً GMT (توقيت غرينتش). وهي لا تحتوي على توقيت صيفي (DST)، مما يجعلها تمثيلاً مريحاً للحسابات. العمليات التي تجمع بين التواريخ والأوقات، مثل c()، غالباً ما تتجاهل المنطقة الزمنية. في هذه الحالة، ستُعرض التواريخ والأوقات في المنطقة الزمنية للعنصر الأول:

x4 <- c(x1, x2, x3)
x4
#> [1] "2024-06-01 12:00:00 EDT" "2024-06-01 12:00:00 EDT"
#> [3] "2024-06-01 12:00:00 EDT"

يمكنك تغيير المنطقة الزمنية بطريقتين:

  • الحفاظ على اللحظة الزمنية كما هي، وتغيير طريقة عرضها فقط. استخدم هذا عندما تكون اللحظة صحيحة، ولكنك تريد عرضاً أكثر طبيعية.
x4a <- with_tz(x4, tzone = "Australia/Lord_Howe")
x4a
#> [1] "2024-06-02 02:30:00 +1030" "2024-06-02 02:30:00 +1030"
#> [3] "2024-06-02 02:30:00 +1030"
x4a - x4
#> Time differences in secs
#> [1] 0 0 0

(يوضح هذا أيضاً تحدياً آخر للمناطق الزمنية: ليست كلها فروق ساعات صحيحة!)

  • تغيير اللحظة الزمنية الأساسية نفسها. استخدم هذا عندما يكون لديك لحظة تم تسميتها بمنطقة زمنية غير صحيحة، وتحتاج إلى إصلاحها.
x4b <- force_tz(x4, tzone = "Australia/Lord_Howe")
x4b
#> [1] "2024-06-01 12:00:00 +1030" "2024-06-01 12:00:00 +1030"
#> [3] "2024-06-01 12:00:00 +1030"
x4b - x4
#> Time differences in hours
#> [1] -14.5 -14.5 -14.5

17.6 ملخص

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

يقدم الفصل التالي مراجعة وشرحاً للقيم المفقودة. لقد رأيتها في أجزاء قليلة وواجهتها بلا شك في تحليلك الخاص، وقد حان الوقت الآن لتقديم مجموعة من التقنيات المفيدة للتعامل معها.


  1. تكون السنة كبيسة إذا كانت تقبل القسمة على 4، إلا إذا كانت تقبل القسمة أيضاً على 100، ما لم تكن تقبل القسمة على 400 أيضاً. بمعنى آخر، في كل مجموعة من 400 سنة، هناك 97 سنة كبيسة.↩︎

  2. https://xkcd.com/1179/↩︎

  3. قد تتساءل عما يرمز إليه الاختصار UTC. إنه تسوية بين الاسم الإنجليزي “Coordinated Universal Time” والفرنسي “Temps Universel Coordonné”.↩︎

  4. لا توجد جوائز لمن يحزر الدولة التي ابتكرت نظام خطوط الطول.↩︎