Mastering Datetime with Lubridate

Handling Dates, Times, and Spans in R

Author

Abdullah Al Shamim

Published

February 13, 2026

What will we learn?

  • Creation: How to build date-time objects from separate columns or strings.
  • Extraction: Pulling specific details like weekdays, months, or years from a date.
  • Arithmetic: Adding or subtracting time using Durations, Periods, and Intervals.
  • Visualization: Plotting time-series data and frequency patterns using ggplot2.

1. Environment Setup and Data Creation

Working with dates in R is significantly easier using the lubridate package. We will use the nycflights13 dataset for our practical analysis.

Code
# Load required libraries
library(tidyverse)
library(lubridate)
library(nycflights13)

# Check the current date and time
now()
[1] "2026-02-13 10:42:13 +06"
Code
# Overview of the flights dataset
glimpse(flights)
Rows: 336,776
Columns: 19
$ year           <int> 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2…
$ month          <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
$ day            <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
$ dep_time       <int> 517, 533, 542, 544, 554, 554, 555, 557, 557, 558, 558, …
$ sched_dep_time <int> 515, 529, 540, 545, 600, 558, 600, 600, 600, 600, 600, …
$ dep_delay      <dbl> 2, 4, 2, -1, -6, -4, -5, -3, -3, -2, -2, -2, -2, -2, -1…
$ arr_time       <int> 830, 850, 923, 1004, 812, 740, 913, 709, 838, 753, 849,…
$ sched_arr_time <int> 819, 830, 850, 1022, 837, 728, 854, 723, 846, 745, 851,…
$ arr_delay      <dbl> 11, 20, 33, -18, -25, 12, 19, -14, -8, 8, -2, -3, 7, -1…
$ carrier        <chr> "UA", "UA", "AA", "B6", "DL", "UA", "B6", "EV", "B6", "…
$ flight         <int> 1545, 1714, 1141, 725, 461, 1696, 507, 5708, 79, 301, 4…
$ tailnum        <chr> "N14228", "N24211", "N619AA", "N804JB", "N668DN", "N394…
$ origin         <chr> "EWR", "LGA", "JFK", "JFK", "LGA", "EWR", "EWR", "LGA",…
$ dest           <chr> "IAH", "IAH", "MIA", "BQN", "ATL", "ORD", "FLL", "IAD",…
$ air_time       <dbl> 227, 227, 160, 183, 116, 150, 158, 53, 140, 138, 149, 1…
$ distance       <dbl> 1400, 1416, 1089, 1576, 762, 719, 1065, 229, 944, 733, …
$ hour           <dbl> 5, 5, 5, 5, 6, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 6, 6, 6…
$ minute         <dbl> 15, 29, 40, 45, 0, 58, 0, 0, 0, 0, 0, 0, 0, 0, 0, 59, 0…
$ time_hour      <dttm> 2013-01-01 05:00:00, 2013-01-01 05:00:00, 2013-01-01 0…

Building Datetime Objects

Often, date information is split across multiple columns (year, month, day). We can combine them into a single POSIXct object.

Code
# Option 1: Using paste() and ymd_hm()
flights %>% 
  select(origin, year, month, day, hour, minute) %>% 
  mutate(flight_date = ymd_hm(paste(year, month, day, hour, minute))) %>% 
  select(origin, flight_date) %>%
  head(4)
# A tibble: 4 × 2
  origin flight_date        
  <chr>  <dttm>             
1 EWR    2013-01-01 05:15:00
2 LGA    2013-01-01 05:29:00
3 JFK    2013-01-01 05:40:00
4 JFK    2013-01-01 05:45:00
Code
# Option 2: Using the cleaner make_datetime() function
flights %>% 
  mutate(flight_date = make_datetime(year, month, day, hour, minute)) %>% 
  select(origin, dest, flight_date) %>%
  head(4)
# A tibble: 4 × 3
  origin dest  flight_date        
  <chr>  <chr> <dttm>             
1 EWR    IAH   2013-01-01 05:15:00
2 LGA    IAH   2013-01-01 05:29:00
3 JFK    MIA   2013-01-01 05:40:00
4 JFK    BQN   2013-01-01 05:45:00

2. Extracting Datetime Data

Once you have a valid datetime object, you can easily pull out labels like the day of the week or the name of the month.

Code
flights %>% 
  mutate(flight_date = make_datetime(year, month, day)) %>% 
  mutate(weekday = wday(flight_date, label = TRUE),
         month_name = month(flight_date, label = TRUE)) %>% 
  select(origin, flight_date, weekday, month_name) %>%
  head(4)
# A tibble: 4 × 4
  origin flight_date         weekday month_name
  <chr>  <dttm>              <ord>   <ord>     
1 EWR    2013-01-01 00:00:00 Tue     Jan       
2 LGA    2013-01-01 00:00:00 Tue     Jan       
3 JFK    2013-01-01 00:00:00 Tue     Jan       
4 JFK    2013-01-01 00:00:00 Tue     Jan       

3. Arithmetic with Date-Time Data

Lubridate allows you to perform mathematical operations on time objects easily.

Code
# Calculating the date and time exactly 30 years from now
now() + years(30)
[1] "2056-02-13 10:42:20 +06"
Code
# Filtering data for a specific range (The month of September)
flights %>% 
  filter(time_hour >= ymd("2013-09-01") &
           time_hour < ymd("2013-10-01")) %>% 
  select(origin, month) %>%
  head(4)
# A tibble: 4 × 2
  origin month
  <chr>  <int>
1 JFK        9
2 JFK        9
3 EWR        9
4 JFK        9

4. Time Spans: Durations, Periods, and Intervals

Lubridate provides three distinct ways to handle time spans depending on whether you need exact seconds or human-readable units.

Durations and Periods

  • Durations: Measure exact seconds (ignores leap years/daylight savings).
  • Periods: Measure human-clock time (accounts for leap years).
Code
# Creating Durations
my_duration <- dseconds(15)
print(my_duration)
[1] "15s"
Code
dminutes(4)
[1] "240s (~4 minutes)"
Code
# Arithmetic: Duration (exactly 365 days) vs Period (1 calendar year)
ymd("2016-01-01") + dyears(1)
[1] "2016-12-31 06:00:00 UTC"
Code
ymd("2016-01-01") + years(1)
[1] "2017-01-01"
Code
# Adding and Multiplying Periods
months(3) + days(1) + minutes(8)
[1] "3m 1d 0H 8M 0S"
Code
months(3) * 2
[1] "6m 0d 0H 0M 0S"

Intervals and Comparisons

Intervals represent a specific range between a start and end point.

Code
start <- ymd_hms("2024-01-01 12:00:00")
end <- ymd_hms("2024-05-01 12:00:00")
iv <- interval(start, end)

# Inclusion check: Does a date fall within this interval?
ymd_hms("2024-04-01 12:00:00") %within% iv
[1] TRUE
Code
# Mathematical comparison of intervals
start2 <- ymd_hms("2024-01-01 12:00:00")
end2 <- ymd_hms("2024-05-01 12:00:00")
iv2 <- interval(start2, end2)
iv > iv2
[1] FALSE
Code
# Convert Interval into Duration
as.duration(iv)
[1] "10454400s (~17.29 weeks)"

5. Working with ggplot2

Dates are most useful when visualized. We can use Lubridate functions directly inside ggplot2 calls to analyze trends.

Code
# Bar chart showing flights by carrier and weekday
flights %>% 
  filter(carrier %in% c("9E", "US", "AA", "MQ")) %>% 
  mutate(weekday = wday(time_hour, label = TRUE)) %>% 
  ggplot(aes(weekday)) +
  geom_bar(fill = "steelblue", alpha = 0.8) +
  facet_wrap(~carrier) +
  theme_bw() +
  labs(title = "Number of flights by carrier and weekday",
       x = "Weekdays",
       y = "Count") +
  theme(plot.title = element_text(hjust = 0.5))

Frequency Plot

Code
# Visualizing flight frequency over time using geom_freqpoly
flights %>% 
  filter(time_hour < ymd("2013-10-01"),
         carrier %in% c("9E", "US", "AA", "MQ")) %>% 
  ggplot(aes(time_hour, color = carrier)) +
  geom_freqpoly(linewidth = 1.2) +
  theme_bw() +
  labs(title = "Number of flights by carrier",
       x = "Time",
       y = "Frequency") +
  theme(plot.title = element_text(hjust = 0.5))


Systematic Checklist (Cheat Sheet):

  • Creation: ymd(), make_datetime()
  • Extraction: year(), month(), wday()
  • Spans: dyears() (Duration), years() (Period), interval()
  • Operators: %within% (Check inclusion)

Excellent work! You have successfully mastered the fundamental tools for handling date-time data in R.