17  날짜와 시간

17.1 소개

이 장에서는 R에서 날짜와 시간으로 작업하는 방법을 보여줄 것입니다. 언뜻 보기에 날짜와 시간은 간단해 보입니다. 일상 생활에서 항상 사용하며 큰 혼란을 일으키지 않는 것 같습니다. 하지만 날짜와 시간에 대해 더 많이 알수록 더 복잡해지는 것 같습니다!

워밍업으로 1년에 며칠이 있는지, 하루에 몇 시간이 있는지 생각해 보세요. 대부분의 연도에는 365일이 있지만 윤년에는 366일이 있다는 것을 기억할 것입니다. 연도가 윤년인지 확인하는 전체 규칙을 알고 있습니까1? 하루의 시간 수는 조금 덜 분명합니다. 대부분의 날은 24시간이지만 일광 절약 시간제(DST)를 사용하는 곳에서는 매년 하루는 23시간이고 다른 하루는 25시간입니다.

날짜와 시간은 두 가지 물리적 현상(지구의 자전과 태양 공전)과 달, 시간대, DST를 포함한 수많은 지정학적 현상을 조화시켜야 하기 때문에 어렵습니다. 이 장에서는 날짜와 시간의 모든 세부 사항을 가르치지는 않지만 일반적인 데이터 분석 과제에 도움이 될 실용적인 기술의 탄탄한 기초를 제공할 것입니다.

다양한 입력에서 날짜-시간을 생성하는 방법을 보여주는 것으로 시작하여 날짜-시간이 생기면 연도, 월, 일과 같은 구성 요소를 추출하는 방법을 보여줄 것입니다. 그런 다음 수행하려는 작업에 따라 다양한 종류가 있는 시간 스팬(time spans)으로 작업하는 까다로운 주제에 대해 자세히 알아볼 것입니다. 시간대로 인해 발생하는 추가 문제에 대한 간략한 논의로 마무리하겠습니다.

17.1.1 선수 지식

이 장에서는 R에서 날짜와 시간을 더 쉽게 다룰 수 있게 해주는 lubridate 패키지에 초점을 맞출 것입니다. 최신 tidyverse 릴리스부터 lubridate는 핵심 tidyverse의 일부입니다. 연습 데이터로 nycflights13도 필요합니다.

library(tidyverse)
#> Warning: package 'ggplot2' was built under R version 4.5.2
#> Warning: package 'readr' was built under R version 4.5.2
library(nycflights13)

17.2 날짜/시간 생성

시간의 한 순간을 나타내는 세 가지 유형의 날짜/시간 데이터가 있습니다:

  • 날짜(Date). 티블은 이것을 <date>로 인쇄합니다.

  • 하루 중 시간(Time). 티블은 이것을 <time>으로 인쇄합니다.

  • 날짜-시간(Date-time) 은 날짜에 시간을 더한 것입니다. 시간의 한 순간을 고유하게 식별합니다(일반적으로 가장 가까운 초까지). 티블은 이것을 <dttm>으로 인쇄합니다. 기본 R은 이것을 POSIXct라고 부르지만 발음하기 쉽지는 않습니다.

이 장에서는 R에 시간을 저장하기 위한 기본 클래스가 없으므로 날짜와 날짜-시간에 집중할 것입니다. 필요한 경우 hms 패키지를 사용할 수 있습니다.

항상 필요에 맞는 가장 간단한 데이터 유형을 사용해야 합니다. 즉, 날짜-시간 대신 날짜를 사용할 수 있다면 그렇게 해야 합니다. 날짜-시간은 시간대를 처리해야 하므로 상당히 더 복잡합니다. 이 장의 마지막 부분에서 다시 다룰 것입니다.

현재 날짜나 날짜-시간을 얻으려면 today() 또는 now()를 사용할 수 있습니다:

today()
#> [1] "2025-12-25"
now()
#> [1] "2025-12-25 01:33:20 KST"

그렇지 않은 경우 다음 섹션에서는 날짜/시간을 생성할 가능성이 있는 네 가지 방법을 설명합니다:

  • readr로 파일을 읽는 동안.
  • 문자열에서.
  • 개별 날짜-시간 구성 요소에서.
  • 기존 날짜/시간 객체에서.

17.2.1 가져오는 동안

CSV에 ISO8601 날짜 또는 날짜-시간이 포함되어 있으면 아무것도 할 필요가 없습니다. readr이 자동으로 인식합니다:

csv <- "
  date,datetime
  2022-01-02,2022-01-02 05:12
"
read_csv(csv)
#> # A tibble: 1 × 2
#>   date       datetime           
#>   <date>     <dttm>             
#> 1 2022-01-02 2022-01-02 05:12:00

ISO8601을 들어본 적이 없다면 날짜의 구성 요소가 가장 큰 것부터 가장 작은 것 순으로 -로 구분되어 구성되는 날짜 쓰기 국제 표준2입니다. 예를 들어 ISO8601에서 2022년 5월 3일은 2022-05-03입니다. ISO8601 날짜에는 시, 분, 초가 :로 구분되고 날짜와 시간 구성 요소가 T 또는 공백으로 구분되는 시간도 포함될 수 있습니다. 예를 들어 2022년 5월 3일 오후 4시 26분을 2022-05-03 16:26 또는 2022-05-03T16:26으로 쓸 수 있습니다.

다른 날짜-시간 형식의 경우 날짜-시간 형식과 함께 col_typescol_date() 또는 col_datetime()을 사용해야 합니다. readr에서 사용하는 날짜-시간 형식은 많은 프로그래밍 언어에서 사용되는 표준이며 % 뒤에 단일 문자로 날짜 구성 요소를 설명합니다. 예를 들어 %Y-%m-%d는 연도, -, 월(숫자), -, 일인 날짜를 지정합니다. 표 Table 17.1 은 모든 옵션을 나열합니다.

Table 17.1: readr이 이해하는 모든 날짜 형식
유형 코드 의미
연도 %Y 4자리 연도 2021
%y 2자리 연도 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()와 친구들은 날짜를 생성합니다. 날짜-시간을 생성하려면 파싱 함수 이름에 밑줄과 “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의 4개 시간 열 각각에 대해 동일한 작업을 수행해 보겠습니다. 시간이 약간 이상한 형식으로 표현되어 있으므로 모듈러 연산을 사용하여 시와 분 구성 요소를 뽑아냅니다. 날짜-시간 변수를 생성한 후 이 장의 나머지 부분에서 탐색할 변수에 집중합니다.

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초 = 1일

x축에 출발 시간(2013년 1월-12월), y축에 항공편 수(0-1000)가 있는  빈도 다각형. 빈도 다각형은 일별로 비닝되어 일별 항공편 시계열을  볼 수 있습니다. 패턴은 주간 패턴이 지배적입니다. 주말에는 항공편이  더 적습니다. 2월 초, 7월 초, 11월 말, 12월 말에 놀랍도록 적은  항공편이 있는 날들이 눈에 띕니다.

또는 하루 안에:

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

x축에 출발 시간(1월 1일 오전 6시 - 자정), y축에 항공편 수(0-17)가 있는  빈도 다각형으로 10분 단위로 비닝되었습니다. 변동성이 커서 패턴을  보기 어렵지만 대부분의 빈에는 8-12편의 항공편이 있으며 오전 6시 이전과  오후 8시 이후에는 항공편이 현저히 적습니다.

숫자 맥락(예: 히스토그램)에서 날짜-시간을 사용할 때 1은 1초를 의미하므로 86400의 binwidth는 하루를 의미합니다. 날짜의 경우 1은 1일을 의미합니다.

17.2.4 다른 유형에서

날짜-시간과 날짜 사이를 전환하고 싶을 수 있습니다. 그것이 as_datetime()as_date()의 역할입니다:

as_datetime(today())
#> [1] "2025-12-25 UTC"
as_date(now())
#> [1] "2025-12-25"

때때로 “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"))
  2. today()tzone 인수는 무엇을 합니까? 왜 중요합니까?

  3. 다음 각 날짜-시간에 대해 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" # 2014년 12월 30일
    t1 <- "1705"
    t2 <- "11:15:10.12 PM"

17.3 날짜-시간 구성 요소

이제 R의 날짜-시간 데이터 구조로 날짜-시간 데이터를 가져오는 방법을 알았으니, 무엇을 할 수 있는지 살펴보겠습니다. 이 섹션에서는 개별 구성 요소를 가져오고 설정할 수 있는 접근자(accessor) 함수에 초점을 맞출 것입니다. 다음 섹션에서는 산술이 날짜-시간과 어떻게 작동하는지 살펴보겠습니다.

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

x축에 요일, y축에 항공편 수가 있는 막대 차트. 월요일-금요일은 약  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()

x축에 실제 출발 분(0-60), y축에 평균 지연(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()

x축에 예정된 출발 분(0-60), y축에 평균 지연(4-16)이 있는 선 차트.  패턴이 거의 없으며, 시간의 흐름에 따라 평균 지연이 약 10분에서 8분으로  감소한다는 작은 암시만 있습니다.

그렇다면 실제 출발 시간에서 왜 그런 패턴이 보일까요? 음, 사람이 수집한 많은 데이터와 마찬가지로 Figure 17.1 에서 볼 수 있듯이 “좋은” 출발 시간에 항공편이 출발하는 쪽으로 강한 편향이 있습니다. 사람의 판단이 포함된 데이터로 작업할 때는 항상 이런 종류의 패턴에 주의하세요!

x축에 출발 분(0-60), y축에 항공편 수(0-60000)가 있는 선 플롯.  대부분의 항공편은 정시(약 60,000) 또는 30분(약 35,000)에 출발하도록  예정되어 있습니다. 그렇지 않으면 거의 모든 항공편이 5의 배수에  출발하도록 예정되어 있으며 15, 45, 55분에 약간 더 많습니다.
Figure 17.1: 매 시간 출발하도록 예정된 항공편 수를 보여주는 빈도 다각형. 0과 30과 같은 둥근 숫자에 대한 선호도가 강하고 일반적으로 5의 배수인 숫자에 대한 선호도가 강함을 알 수 있습니다.

17.3.2 반올림

개별 구성 요소를 플롯하는 대안적인 접근 방식은 floor_date(), round_date(), ceiling_date()를 사용하여 날짜를 가까운 시간 단위로 반올림하는 것입니다. 각 함수는 조정할 날짜 벡터와 내림(floor), 올림(ceiling) 또는 반올림(round)할 단위 이름을 취합니다. 예를 들어 이를 통해 주당 항공편 수를 플롯할 수 있습니다:

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

x축에 주(2013년 1월-12월), y축에 항공편 수(2,000-7,000)가 있는 선  플롯. 패턴은 2월부터 11월까지 주당 약 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.

x축에 출발 시간이 있는 선 플롯. 이것은 자정 이후 초 단위이므로  해석하기 어렵습니다.

한 쌍의 날짜-시간 간의 차이를 계산하면 difftime이 생성됩니다(Section 17.4.3 에서 자세히 설명). 더 유용한 x축을 얻기 위해 이를 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)

x축에 출발 시간(자정부터 자정), y축에 항공편 수(0-15,000)가 있는  선 플롯. 오전 5시 이전에는 항공편이 매우 적습니다(<100). 그런 다음  항공편 수는 시간당 12,000편으로 급격히 증가하여 오전 9시에 15,000편으로  정점을 찍은 다음 오전 10시부터 오후 2시까지 시간당 약 8,000편으로  떨어집니다. 항공편 수는 오후 8시까지 시간당 약 12,000편으로 증가했다가  다시 급격히 감소합니다.

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"

값이 너무 크면 롤오버됩니다:

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$caratflights$sched_dep_time의 분포를 유사하게 만드는 것은 무엇입니까?

  7. 20-30분 및 50-60분의 항공편 조기 출발이 일찍 출발하는 예정된 항공편으로 인해 발생한다는 가설을 확인하세요. 힌트: 항공편이 지연되었는지 여부를 알려주는 이진 변수를 만드세요.

17.4 시간 스팬(Time spans)

다음으로 뺄셈, 덧셈, 나눗셈을 포함하여 날짜 산술이 어떻게 작동하는지 배울 것입니다. 그 과정에서 시간 스팬을 나타내는 세 가지 중요한 클래스에 대해 배우게 됩니다:

  • 지속 시간(Durations), 정확한 초 수를 나타냅니다.
  • 기간(Periods), 주 및 월과 같은 인간 단위를 나타냅니다.
  • 구간(Intervals), 시작점과 끝점을 나타냅니다.

지속 시간, 기간, 구간 중에서 어떻게 선택합니까? 항상 그렇듯이 문제를 해결하는 가장 간단한 데이터 구조를 선택하세요. 물리적 시간만 중요하다면 지속 시간을 사용하세요. 인간 시간을 추가해야 한다면 기간을 사용하세요. 스팬이 인간 단위로 얼마나 긴지 파악해야 한다면 구간을 사용하세요.

17.4.1 지속 시간(Durations)

R에서 두 날짜를 빼면 difftime 객체를 얻습니다:

# 해들리는 몇 살입니까?
h_age <- today() - ymd("1979-10-14")
h_age
#> Time difference of 16874 days

difftime 클래스 객체는 초, 분, 시, 일 또는 주 단위의 시간 스팬을 기록합니다. 이 모호함은 difftime 작업을 약간 고통스럽게 만들 수 있으므로 lubridate는 항상 초를 사용하는 대안인 지속 시간(duration) 을 제공합니다.

as.duration(h_age)
#> [1] "1457913600s (~46.2 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)"

지속 시간은 항상 시간 스팬을 초 단위로 기록합니다. 더 큰 단위는 분, 시, 일, 주, 연을 초로 변환하여 생성됩니다. 1분은 60초, 1시간은 60분, 하루는 24시간, 1주는 7일입니다. 더 큰 시간 단위는 더 문제가 됩니다. 1년은 “평균” 일 수, 즉 365.25일을 사용합니다. 변동이 너무 크기 때문에 1월을 지속 시간으로 변환할 방법이 없습니다.

지속 시간을 더하고 곱할 수 있습니다:

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"

왜 3월 8일 오전 1시의 하루 뒤가 3월 9일 오전 2시일까요? 날짜를 주의 깊게 보면 시간대가 변경되었음을 알 수 있습니다. 3월 8일은 DST가 시작되는 날이기 때문에 23시간밖에 없습니다. 따라서 하루 전체 분량의 초를 더하면 다른 시간이 됩니다.

17.4.2 기간(Periods)

이 문제를 해결하기 위해 lubridate는 기간(periods) 을 제공합니다. 기간은 시간 스팬이지만 초 단위의 고정된 길이가 없으며 대신 일 및 월과 같은 “인간” 시간으로 작동합니다. 이를 통해 더 직관적인 방식으로 작동할 수 있습니다:

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

지속 시간과 마찬가지로 기간은 여러 친근한 생성자 함수로 만들 수 있습니다.

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"

그리고 물론 날짜에 더할 수 있습니다. 지속 시간과 비교할 때 기간은 예상한 대로 수행될 가능성이 더 큽니다:

# 윤년
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 구간(Intervals)

dyears(1) / ddays(365)는 무엇을 반환합니까? dyears()는 평균 연도당 초 수(365.25일)로 정의되므로 1이 아닙니다.

years(1) / days(1)은 무엇을 반환합니까? 음, 연도가 2015년이었다면 365를 반환해야 하지만 2016년이었다면 366을 반환해야 합니다! lubridate가 명확한 단일 답변을 제공하기에는 정보가 충분하지 않습니다. 대신 추정치를 제공합니다:

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

더 정확한 측정을 원한다면 구간(interval) 을 사용해야 합니다. 구간은 시작 및 종료 날짜 시간의 쌍이거나 시작점이 있는 지속 시간으로 생각할 수 있습니다.

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. R을 막 배우기 시작한 사람에게 days(!overnight)days(overnight)를 설명하세요. 알아야 할 핵심 사실은 무엇입니까?

  2. 2015년 매월 1일인 날짜 벡터를 만드세요. 현재 연도의 매월 1일인 날짜 벡터를 만드세요.

  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년에 미시간(디트로이트가 위치한 주)은 DST를 따르지 않았으므로 다른 이름이 필요합니다. 이 이야기 중 일부를 읽으려면 원시 시간대 데이터베이스(https://www.iana.org/time-zones에서 사용 가능)를 읽을 가치가 있습니다!

Sys.timezone()으로 R이 생각하는 현재 시간대를 알 수 있습니다:

Sys.timezone()
#> [1] "Asia/Seoul"

(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에서 시간대는 인쇄만 제어하는 날짜-시간의 속성입니다. 예를 들어 다음 세 객체는 동일한 순간을 나타냅니다:

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. 경도 시스템을 만든 나라가 어디인지 맞추는 데 상은 없습니다.↩︎