聰明不如鈍筆
총명불여둔필
assignment KindeR

R에서 깔끔하게 날짜·시간 데이터 처리하기(feat. lubridate)

영어 낱말 'lubricate'는 '윤활유를 바르다', '기름을 치다'는 뜻입니다.

 

아마도 그래서 tidyverse 생태계에서 날짜와 시간 처리를 맡고 있는 패키지 이름이 lubridate일 겁니다.

 

해들리 위컴 박사가 'R for Data Science'에 남긴 말을 그대로 인용하면 R에서 날짜와 시간 데이터를 처리하는 일은 때로 실망스럽기까지 합니다.

 

다른 tidyverse 계열 패키지가 그런 것처럼 lubridate는 이런 데이터를 '깔끔하게' 처리할 수 있도록 도와줍니다.

 

이번 포스트에서는 이 패키지를 써서 날짜와 시간 데이터를 어떻게 처리하는지 알아보도록 하겠습니다.

 

언제나 그렇듯 제일 먼저 할 일은 tidyverse 패키지 (설치하고) 불러오기. 

#install.packages('tidyverse')
library('tidyverse')
## -- Attaching packages ---------------------------------------------------------------- tidyverse 1.2.1 --
## √ ggplot2 3.2.1     √ purrr   0.3.2
## √ tibble  2.1.3     √ dplyr   0.8.3
## √ tidyr   0.8.3     √ stringr 1.4.0
## √ readr   1.3.1     √ forcats 0.3.0
## -- Conflicts ------------------------------------------------------------------- tidyverse_conflicts() --
## x dplyr::filter() masks stats::filter()
## x dplyr::lag()    masks stats::lag()

 

lubridate는 tidyverse 계열이기는 하지만 날짜와 시간 데이터를 처리할 때만 필요하기 때문에 tidyverse를 불러올 때 같이 따라오지 않습니다.

 

그래서 따로 (설치하고) 불러와야 합니다.

#install.packages('lubridate')
library('lubridate')
## 
## Attaching package: 'lubridate'
## The following object is masked from 'package:base':
## 
##     date

 

준비를 마쳤으니 먼저 날짜 데이터를 쓰고 필요한 정보를 얻어내는 방법을 알아보겠습니다.

 

 

날짜 데이터 입력하기

R에서는 기본적으로 as.Date() 함수를 써서 일반 텍스트 데이터를 날짜 데이터로 바꿉니다. 예컨대 오늘 날짜는 이렇게 표현할 수 있습니다.

as.Date('2020-01-10')
## [1] "2020-01-10"

 

다시 말씀드리지만 as.Date()는 '텍스트 데이터'를 날짜 데이터로 바꿉니다. 그래서 따옴표 없이 쓰면 날짜로 받아들이지 못합니다.

as.Date(2020-01-10)
## Error in as.Date.numeric(20200110): 'origin' must be supplied

 

빼기(-) 부호 없이 숫자로만 써도 마찬가지 결과가 나타납니다.

as.Date(20200110)
## Error in as.Date.numeric(20200110): 'origin' must be supplied

 

따옴표를 써서 문자 데이터로 표시할 때도 미리 약속한 패턴과 다르면 역시 에러가 나옵니다.

as.Date('20200110')
## Error in charToDate(x): character string is not in a standard unambiguous format

 

반면 lubridate에 들어 있는 ymd() 함수는 어떤 모양이든 이를 날짜로 받아들입니다.

ymd('20200110')
## [1] "2020-01-10"

 

ymd는 연(year) 월(month) 일(day) 순서로 날짜를 입력했다는 뜻입니다. 서양식으로 월일연 순서로 표시하고 싶을 때는 mdy() 함수를 쓰시면 됩니다.

mdy('January 10th 2020')
## [1] "2020-01-10"

 

네, 월을 숫자가 아니라 영어 낱말로 표시해도 잘 알아 듣습니다.

 

ymd(), mdy()가 있는데 dmy()라고 없으란 법은 없겠죠?

dmy('10-jan-2020')
## [1] "2020-01-10"

 

이번에는 'January' 대신 'jan'이라고만 썼는데도 잘 알아 듣습니다.

 

굳이 연도를 네 자리로 쓸 필요는 없습니다.

 

예컨대 1982년 3월 27일을 입력하고 싶을 때는 아래처럼 쓰면 됩니다.

ymd('820327')
## [1] "1982-03-27"

 

물론 따옴표를 쓰지 않아도 잘 알아 듣습니다.

ymd(820327)
## [1] "1982-03-27"

 

 

날짜 데이터 뽑아내기

참고로 1982년 3월 27일은 프로야구 역사상 첫 번째 경기를 치른 날입니다.

 

이 날짜를 very_first_game_of_kbo라는 변수에 넣어보겠습니다.

very_first_game_of_kbo <- ymd(820327)

 

이렇게 날짜 데이터가 있을 때 연도만 따로 빼고 싶으면 year() 함수를 쓰시면 됩니다.

year(very_first_game_of_kbo)
## [1] 1982

 

달만 뽑고 싶을 때는 month()를 쓰면 되겠죠?

month(very_first_game_of_kbo)
## [1] 3

 

그렇다면 날짜는? 네, day()가 정답입니다.

day(very_first_game_of_kbo)
## [1] 27

 

뿐만 아니라 week() 함수로 이 날짜가 그해 몇 번째 주(週)였는지도 알아낼 수 있습니다.

week(very_first_game_of_kbo)
## [1] 13

 

당연히 요일도 알 수 있습니다. wday() 함수를 쓰시면 됩니다.

wday(very_first_game_of_kbo)
## [1] 7

 

wday() 함수는 기본적으로 일요일~토요일을 1~7로 표시합니다. 

 

그러니까 이날은 토요일이었습니다.

 

만약 텍스트로 받아 보고 싶으시다면 'label=TRUE' 속성을 추가하시면 됩니다.

wday(very_first_game_of_kbo, label = TRUE)
## [1] 토
## Levels: 일 < 월 < 화 < 수 < 목 < 금 < 토

 

아, 이날이 그해 몇 번째 날이었는지 알고 싶으시다면 yday() 함수가 기다리고 있습니다.

yday(very_first_game_of_kbo)
## [1] 86

 

그밖에 상·하반기를 구분하고 싶으실 때는 semester()

semester(very_first_game_of_kbo)
## [1] 1

 

4분기 구분이 필요하실 때는 quarter() 함수를 쓰시면 됩니다.

quarter(very_first_game_of_kbo)
## [1] 1

 

이 정도면 lubridate가 어떤 패지키인지 감은 잡으셨을 거라고 믿습니다.

 

 

날짜로 각종 계산하기

이 프로야구 개막일로부터 1만 일이 지난 날은 언제였을까요?

 

그냥 이 변수에 '+10000'을 더하시면 정답이 나옵니다.

very_first_game_of_kbo + 10000
## [1] "2009-08-12"

 

lubridate는 기본적으로 이렇게 숫자만 더하라고 하면 날짜를 더하고 뺍니다.

 

days() 함수를 써도 같은 결과를 얻으실 수 있습니다. (s가 붙었습니다.)

very_first_game_of_kbo + days(10000)
## [1] "2009-08-12"

 

그렇다면 달을 더하고 싶을 때는 months()를 쓰면 되겠죠?

 

프로야구 원년 개막일로부터 1000개월 뒤가 언제인지 알아보려면 이렇게 쓰면 됩니다.

very_first_game_of_kbo + months(1000)
## [1] "2065-07-27"

 

연도는 물론 years()입니다. 프로야구 원년 개막일로부터 38년 뒤는?

very_first_game_of_kbo + years(38)
## [1] "2020-03-27"

 

오늘은 이 프로야구 개막일로부터 얼마나 흘렀을까요?

 

lubridate에서는 today() 함수로 오늘 날짜를 표시할 수 있습니다.

 

그렇다면 오늘 날짜에서 개막일을 빼면 결과를 확인할 수 있습니다.

today() - very_first_game_of_kbo
## Time difference of 13803 days

 

오늘은 프로야구 개막일로부터 1만3803일이 지났다고 합니다.

 

 

기간형 vs 지속형

그럼 1만3803일은 몇 년, 몇 개월, 며칠일까요?

 

이런 계산을 할 때는 interval() 함수가 필요합니다.

 

interval() 함수는 이름 그대로 시작과 끝이 있는 날짜 계산에 도움을 줍니다.

 

먼저 그냥 이 함수 안에 두 날짜를 넣어보겠습니다.

interval(very_first_game_of_kbo, today())
## [1] 1982-03-27 UTC--2020-01-10 UTC

 

그냥 일단 시작과 끝만 표시합니다. 

 

인터벌을 나타내고 싶을 때는 함수 대신 '%--%' 기호를 쓰셔도 됩니다.

'%--%'(very_first_game_of_kbo, today())
## [1] 1982-03-27 UTC--2020-01-10 UTC

 

이렇게 쓸 수도 있습니다.

very_first_game_of_kbo %--% today()
## [1] 1982-03-27 UTC--2020-01-10 UTC

 

이렇게 인터별형 자료에 as.period() 함수를 적용하면 우리가 원하는 결과가 나옵니다.

very_first_game_of_kbo %--% today() %>% as.period()
## [1] "37y 9m 14d 0H 0M 0S"

 

참고로 MS 엑셀에서는 datedif() 함수로 같은 계산이 가능합니다.

 

lubridate에서 기간을 계산할 때 쓰는 함수 중에는 as.duration()이라는 녀석도 있습니다.

 

as.duration()은 기본적으로 초 단위로 계산 결과를 알려줍니다.

very_first_game_of_kbo %--% today() %>% as.duration()
## [1] "1192579200s (~37.79 years)"

 

물론 이런 기능만 있는 건 아닙니다.

 

lubridate는 시작과 끝이 있는 날짜를 크게 기간형(period)과 지속형(durtaion) 형태로 구분합니다.

 

지금까지 우리가 알아본 게 기본적으로 기간형입니다.

 

지속형 계산은 지금까지 우리가 쓴 함수 앞에 'd'를 붙입니다.

 

어떤 차이가 있는지 한번 올해를 예로 들어 알아보겠습니다.

 

올해 1월 1일을 start_2020이라는 변수에 넣어보겠습니다.

start_2020 <- ymd(200101)

 

여기서 1년을 더하면 내년 1월 1일이 나올 겁니다.

start_2020 + years(1)
## [1] "2021-01-01"

 

dyears() 함수를 써도 같은 결과가 나올까요?

start_2020 + dyears(1)
## [1] "2020-12-31"

 

아닙니다. 올해 12월 31일이 나옵니다.

 

그런데 이게 항상 그런 건 아닙니다.

 

지난해 1월 1일을 start_2019에 넣고

start_2019 <- ymd(190101)

 

기간형으로 1년을 더하면 올해 1월 1일이 나옵니다.

start_2019 + years(1)
## [1] "2020-01-01"

 

지속형도 마찬가지로 올해 1월 1일이 나옵니다.

start_2019 + dyears(1)
## [1] "2020-01-01"

 

왜 이런 차이가 생겼을까요?

 

그건 올해가 2월이 29일까지 있는 윤년(閏年·leap year)이기 때문입니다.

 

lubridate에서는 leap_year() 함수로 어떤 해가 윤년인지 아닌지 알아볼 수 있습니다.

 

지난해는 윤년이 아니었지만

leap_year(2019)
## [1] FALSE

 

올해는 윤년입니다.

leap_year(2020)
## [1] TRUE

 

우리는 흔히 1년은 365일이라고 말하지만 다들 잘 아시는 것처럼 실제로는 그렇지 않습니다.

 

(일반적으로) 4년마다 한번씩 윤년이 돌아오기 때문입니다.

 

실제로 lubridate에서 기간형으로 날짜 계산을 맡겨도 1년은 365.25일이라는 대답이 돌아옵니다.

years(1)/days(1)
## estimate only: convert to intervals for accuracy
## [1] 365.25

 

윤년인 올해는 1년이 366일이 되어야 합니다.

 

그럴 때는 days()가 아니라 ddays() 함수로 이를 확인할 수 있습니다.

(ymd(200101) %--% ymd(210101)) / ddays(1)
## [1] 366

 

이렇게 지속형이 필요할 때가 있기 때문에 기간형을 기본으로 두되 따로 이런 함수를 마련해 두고 있는 겁니다.

 

 

지금 몇 시지?

날짜 데이터에 감을 잡으셨을 줄 알고 이제 시간 데이터까지 더해보겠습니다.

 

시간은 hour, 분은 minute, 초는 second라는 것 알고 계시죠?

 

날짜를 표시할 때 ymd() 형태로 썼던 것처럼 시간까지 같이 표시할 때는 ymd_hms() 형태가 기본입니다.

 

프로야구 원년 개막전은 오후 2시(14시) 30분에 시작했습니다.

 

초는 없으니까 ymd_hm() 함수로 이 시각을 표시하면 될 겁니다.

ymd_hm(82-03-27 14:30)
## Error: <text>:1:17: unexpected numeric constant
## 1: ymd_hm(82-03-27 14
##                     ^

 

안 됩니다. 그렇다고 별 문제도 아닙니다.

 

시간 데이터까지를 표시할 때는 따옴표가 필요하기 때문에 에러 메시지가 나온 겁니다.

ymd_hm('82-03-27 14:30')
## [1] "1982-03-27 14:30:00 UTC"

 

이번에도 이 시간 데이터를 very_first_game_of_kbo_hm이라는 변수에 넣겠습니다.

very_first_game_of_kbo_hm <- ymd_hm('82-03-27 14:30')

 

이 데이터가 몇 시였는지 알아볼 때는 hour()

hour(very_first_game_of_kbo_hm)
## [1] 14

 

분을 알아보고 싶을 때는 minute()

minute(very_first_game_of_kbo_hm)
## [1] 30

 

초가 필요할 때는 second() 함수를 쓰시면 됩니다.

second(very_first_game_of_kbo_hm)
## [1] 0

 

이 시간이 오전(AM)인지 오후(PM)인지 알려주는 함수도 있습니다.

 

오전인지 아닌지 알아볼 때는 am()

am(very_first_game_of_kbo_hm)
## [1] FALSE

 

오후가 맞는지 아닌지 알아볼 때는 pm()을 쓰시면 그만입니다.

pm(very_first_game_of_kbo_hm)
## [1] TRUE

 

 

날짜 시간 반올림

모든 계산이 그런 것처럼 날짜·시간 계산에도 반올림이 필요할 때가 있습니다.

 

반올림은 영어로 round. 날짜·시간 계산 반올림에 쓰는 함수는 round_date()입니다.

 

일단 very_first_game_of_kbo_hm을 반올림해도 아무 반응이 없습니다.

round_date(very_first_game_of_kbo_hm)
## [1] "1982-03-27 14:30:00 UTC"

 

단, unit 속성을 주면 결과가 바뀝니다.

 

먼저 연도(year)를 기준으로 반올림하면 이런 결과가 나옵니다.

round_date(very_first_game_of_kbo_hm, unit='year')
## [1] "1982-01-01 UTC"

 

unit='year'일 때는 그해 또는 다음해 첫날로 반올림을 하게 됩니다.

 

3월은 그해와 더 가까워서 이번에는 1982년 1월 1일로 반올림을 하는 겁니다.

 

달(month)을 기준으로 하면 1982년 4월 1일이 됩니다. 

round_date(very_first_game_of_kbo_hm, unit='month')
## [1] "1982-04-01 UTC"

 

3월 27일은 3월 1일보다 4월 1일이 더 가까우니 이런 결과가 나온 겁니다.

 

날짜(day)를 기준으로 하면 다음날이 나옵니다. 

round_date(very_first_game_of_kbo_hm, unit='day')
## [1] "1982-03-28 UTC"

 

오후 2시 30분은 그날 0시보다 다음날 0시하고 더 가까우니까요.

 

floor_date() 함수를 써서 날짜를 '내림'하면 앞선 시간으로 돌아가고

floor_date(very_first_game_of_kbo_hm, unit='year')
## [1] "1982-01-01 UTC"

 

ceiling_date() 함수로 날짜를 '올림'하면 늦은 시간으로 바뀝니다.

ceiling_date(very_first_game_of_kbo_hm, unit='year')
## [1] "1983-01-01 UTC"

 

 

시간대 오가기

시간을 계산할 때는 현재와 과거 또는 미래만 비교하는 게 아니라 지역별 차이=시간대를 감안해야 할 때가 있습니다.

 

현재 R가 어떤 시간대를 사용하고 있는지 알아보려면 Sys.timezone() 함수를 쓰면 됩니다.

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

 

눈치가 빠르신 분들은 지금까지 우리는 이 서울 시간대가 아니라 국제 표준인 협정 세계시(UTC)를 기준으로 시간을 표시했다는 걸 알아채셨을 겁니다.

 

이 시간을 서울 시간대로 바꾸려면 tz 속성을 지정하면 됩니다. 우리는 당연히 'Asis/Seoul'을 지정하면 되겠죠?

very_first_game_of_kbo_hm <- ymd_hm('82-03-27 14:30', tz='Asia/Seoul')

 

그러고 나서 시간을 확인해 보면 UTC가 있던 자리가 KST로 바뀌었다는 사실을 알 수 있습니다.

very_first_game_of_kbo_hm
## [1] "1982-03-27 14:30:00 KST"

 

서울 시간은 UTC보다 9시간 빠릅니다. 거꾸로 UTC는 서울 시간보다 9시간 느리겠죠?

 

그래서 서울 시간으로 1982년 3년 27일 14시 30분은 UTC로는 그날 5시 30분이 됩니다.

 

very_first_game_of_kbo_hm_utc에 이 시간을 넣어 보겠습니다. UTC는 기본값이기 때문에 따로 tz 속성을 지정할 필요가 없습니다.

very_first_game_of_kbo_hm_utc <- ymd_hm('82-03-27 05:30')

 

변수를 확인해 보면 당연히 뒤에 UTC가 붙어 있습니다.

very_first_game_of_kbo_hm_utc
## [1] "1982-03-27 05:30:00 UTC"

 

이때 서울 시간에서 UTC 시간을 빼면 어떻게 될까요?

very_first_game_of_kbo_hm - very_first_game_of_kbo_hm_utc
## Time difference of 0 secs

 

네, 0초 차이가 납니다. 두 시간이 완전히 똑같다는 걸 알 수 있습니다.

 

with_tz() 함수를 쓰면 시간대별로 시간을 바꾸는 것도 가능합니다.

 

미국 뉴욕에서 2020년 새해를 맞이할 때 서울은 몇 시였을까요?

ymd_hms('2020-01-01 00:00:00', tz='America/New_York') %>%
  with_tz('Asia/Seoul')
## [1] "2020-01-01 14:00:00 KST"

 

네, 2020년 1월 1일 오후 2시였습니다.

 

서울이 뉴욕보다 14시간 빠르다는 걸 알 수 있습니다.

 

 

인천공항 출발 데이터 가지고 놀기

마지막으로 실제 날짜·시간 자료를 가지고 이 데이터에서 원하는 정보를 찾아내고 시각화하는 것까지 연습해 보도록 하겠습니다.

 

'R for Data Science'에서는 뉴욕에서 뜨고 내린 비행 데이터로 연습을 합니다. 우리는 인천공항 비행 데이터를 쓰겠습니다. 

 

아래는 항공정보보탈시스템에서 2019년 1월 1일부터 12월 31일까지 인천공항 실시간 운항 정보를 내려받아 정리한 자료입니다.

 

icn.csv

 

여기서 '정리'했다는 건 일부 데이터를 덜어냈다는 뜻입니다.

 

원본 데이터에는 총 20만3479편 운항 정보가 들어 있었는데 그 가운데 약 74.3%인 15만1233편만 추렸습니다.

 

다른 이유는 없습니다. 티스토리는 파일 크기가 10MB 이하인 파일만 한 번에 올릴 수 있도록 하고 있어서 그냥 크기를 줄인 것뿐입니다.

 

이 파일은 CSV(Comma Separated Value)라는 형식. CSV는 이름 그대로 각 열을 쉼표로 구분한 텍스트 파일입니다.

 

tidyverse에는 원래 CSV 파일을 읽을 때 쓰라고 read_csv() 함수가 들어 있는데 이 함수를 써서 파일을 불러 들이면 한글 인코딩 문제가 생길 때가 많습니다.

 

그래서 저는 보통 다음 같은 방식으로 파일을 읽습니다.

icn <- read.csv('icn.csv') %>% as_tibble()

 

파일을 읽어 왔으면 어떻게 생겼는지도 한 번 알아봐야겠죠?

icn
## # A tibble: 151,233 x 9
##       연    월    일 항공사        목적지           계획  출발  구분  현황 
##    <int> <int> <int> <fct>         <fct>            <fct> <fct> <fct> <fct>
##  1  2019     1     1 아시아나항공  SEA(시애틀)      0:05  0:25  화물  출발 
##  2  2019     1     1 에티하드      AUH(아부다비)    0:15  0:09  여객  출발 
##  3  2019     1     1 타이에어아시아엑스~ DMK(방콕)        0:20  0:27  여객  출발 
##  4  2019     1     1 아틀라스화물항공~ PVG(푸동)        0:20  0:39  화물  출발 
##  5  2019     1     1 에티오피안항공~ ADD(아디스아바바)~ 0:45  0:53  여객  출발 
##  6  2019     1     1 네덜란드항공  AMS(암스테르담)  0:55  1:02  여객  출발 
##  7  2019     1     1 에어아시아 엑스~ KUL(쿠알라룸푸르)~ 1:00  1:13  여객  출발 
##  8  2019     1     1 아시아나항공  PVG(푸동)        1:05  1:15  화물  출발 
##  9  2019     1     1 에어브리지카고~ SVO(셰레메티예보(모스크바~ 1:05  1:17  화물  출발 
## 10  2019     1     1 비엣제트 항공 DAD(다낭)        1:15  1:26  여객  출발 
## # ... with 151,223 more rows

 

이렇게 연, 월, 일 그리고 시간 데이터가 따로 따로 있을 때는 make_datetime() 함수로 날짜·시간 데이터로 바꿀 수 있습니다.

icn %>%
  mutate(날짜=make_datetime(연, 월, 일, 계획)) %>%
  select(연, 월, 일, 계획, 날짜)
## # A tibble: 151,233 x 5
##       연    월    일 계획  날짜               
##    <int> <int> <int> <fct> <dttm>             
##  1  2019     1     1 0:05  2019-01-01 04:00:00
##  2  2019     1     1 0:15  2019-01-01 08:00:00
##  3  2019     1     1 0:20  2019-01-01 09:00:00
##  4  2019     1     1 0:20  2019-01-01 09:00:00
##  5  2019     1     1 0:45  2019-01-01 16:00:00
##  6  2019     1     1 0:55  2019-01-01 18:00:00
##  7  2019     1     1 1:00  2019-01-01 19:00:00
##  8  2019     1     1 1:05  2019-01-01 20:00:00
##  9  2019     1     1 1:05  2019-01-01 20:00:00
## 10  2019     1     1 1:15  2019-01-01 23:00:00
## # ... with 151,223 more rows

 

아, 혹시 mutate()select() 함수를 쓰는 방법을 모르시는 분은 '최대한 친절하게 쓴 R로 데이터 뽑아내기(feat. dplyr)' 포스트가 도움이 될 수 있습니다.

 

다시 원래 코드 이야기로 돌아가면 날짜·시간 형태로 바뀌기는 했는데 결과는 원하는 대로가 아닙니다.

 

첫 번째 행은 2019년 1월 1일 0시 5분 출발 계획인 비행편인데 자료는 오전 4시로 바뀌었습니다. 

 

이럴 때는 위에서 살펴본 것처럼 일단 텍스트 형태로 바꾸면 그다음을 노려볼 수 있습니다.

 

연, 월, 일, 계획 열을 전부 합쳐야 하니까 paste() 함수를 쓰면 됩니다.

icn %>%
  mutate(날짜=paste(연, 월, 일, 계획)) %>%
  select(연, 월, 일, 계획, 날짜)
## # A tibble: 151,233 x 5
##       연    월    일 계획  날짜         
##    <int> <int> <int> <fct> <chr>        
##  1  2019     1     1 0:05  2019 1 1 0:05
##  2  2019     1     1 0:15  2019 1 1 0:15
##  3  2019     1     1 0:20  2019 1 1 0:20
##  4  2019     1     1 0:20  2019 1 1 0:20
##  5  2019     1     1 0:45  2019 1 1 0:45
##  6  2019     1     1 0:55  2019 1 1 0:55
##  7  2019     1     1 1:00  2019 1 1 1:00
##  8  2019     1     1 1:05  2019 1 1 1:05
##  9  2019     1     1 1:05  2019 1 1 1:05
## 10  2019     1     1 1:15  2019 1 1 1:15
## # ... with 151,223 more rows

 

잘 나왔습니다. 이제 그냥 ymd_hm() 함수를 쓰면 그만입니다.

icn %>%
  mutate(날짜=paste(연, 월, 일, 계획) %>% ymd_hm()) %>%
  select(연, 월, 일, 계획, 날짜)
## # A tibble: 151,233 x 5
##       연    월    일 계획  날짜               
##    <int> <int> <int> <fct> <dttm>             
##  1  2019     1     1 0:05  2019-01-01 00:05:00
##  2  2019     1     1 0:15  2019-01-01 00:15:00
##  3  2019     1     1 0:20  2019-01-01 00:20:00
##  4  2019     1     1 0:20  2019-01-01 00:20:00
##  5  2019     1     1 0:45  2019-01-01 00:45:00
##  6  2019     1     1 0:55  2019-01-01 00:55:00
##  7  2019     1     1 1:00  2019-01-01 01:00:00
##  8  2019     1     1 1:05  2019-01-01 01:05:00
##  9  2019     1     1 1:05  2019-01-01 01:05:00
## 10  2019     1     1 1:15  2019-01-01 01:15:00
## # ... with 151,223 more rows

 

원하는 대로 잘 나왔습니다. 이제 계획 시간과 출발 시간 열을 만들고 두 시간 차이도 계산해 넣겠습니다.

 

시간 차이를 계산할 때는 interval() 함수와 minute() 함수를 같이 써서 분 단위로 구분하겠습니다. 

 

계획 시간과 실제 출발 시간을 맨 앞으로 빼고 시간 차이, 항공편 구분, 출발 현황까지 뽑아내는 코드는 이렇게 쓸 수 있습니다.

icn %>% 
  mutate(계획시간=paste(연, 월, 일, 계획) %>% ymd_hm,
             출발시간=paste(연, 월, 일, 출발) %>% ymd_hm,
             차이=interval(계획시간, 출발시간)/minutes(1)) %>%
  select(계획시간, 출발시간, 차이, 구분, 현황)
## # A tibble: 151,233 x 5
##    계획시간            출발시간             차이 구분  현황 
##    <dttm>              <dttm>              <dbl> <fct> <fct>
##  1 2019-01-01 00:05:00 2019-01-01 00:25:00    20 화물  출발 
##  2 2019-01-01 00:15:00 2019-01-01 00:09:00    -6 여객  출발 
##  3 2019-01-01 00:20:00 2019-01-01 00:27:00     7 여객  출발 
##  4 2019-01-01 00:20:00 2019-01-01 00:39:00    19 화물  출발 
##  5 2019-01-01 00:45:00 2019-01-01 00:53:00     8 여객  출발 
##  6 2019-01-01 00:55:00 2019-01-01 01:02:00     7 여객  출발 
##  7 2019-01-01 01:00:00 2019-01-01 01:13:00    13 여객  출발 
##  8 2019-01-01 01:05:00 2019-01-01 01:15:00    10 화물  출발 
##  9 2019-01-01 01:05:00 2019-01-01 01:17:00    12 화물  출발 
## 10 2019-01-01 01:15:00 2019-01-01 01:26:00    11 여객  출발 
## # ... with 151,223 more rows

 

여기서 화물편을 빼고 여객편만 골라내려면 코드 맨 처음에 filter() 함수를 추가하면 됩니다.

icn %>% 
  filter(구분=='여객') %>%
  mutate(계획시간=paste(연, 월, 일, 계획) %>% ymd_hm,
             출발시간=paste(연, 월, 일, 출발) %>% ymd_hm,
             차이=interval(계획시간, 출발시간)/minutes(1)) %>%
  select(계획시간, 출발시간, 차이, 현황)
## # A tibble: 137,978 x 4
##    계획시간            출발시간             차이 현황 
##    <dttm>              <dttm>              <dbl> <fct>
##  1 2019-01-01 00:15:00 2019-01-01 00:09:00    -6 출발 
##  2 2019-01-01 00:20:00 2019-01-01 00:27:00     7 출발 
##  3 2019-01-01 00:45:00 2019-01-01 00:53:00     8 출발 
##  4 2019-01-01 00:55:00 2019-01-01 01:02:00     7 출발 
##  5 2019-01-01 01:00:00 2019-01-01 01:13:00    13 출발 
##  6 2019-01-01 01:15:00 2019-01-01 01:26:00    11 출발 
##  7 2019-01-01 01:45:00 2019-01-01 03:35:00   110 지연 
##  8 2019-01-01 01:50:00 2019-01-01 02:01:00    11 출발 
##  9 2019-01-01 02:30:00 2019-01-01 02:32:00     2 출발 
## 10 2019-01-01 02:35:00 2019-01-01 03:06:00    31 출발 
## # ... with 137,968 more rows

 

현황에서 출발은 정상 출발이고 비행기가 계획 시간보다 늦게 떴을 때는 지연이라고 표시합니다.

 

비행기가 예정시간보다 얼마나 늦게 떠야 지연이라고 구분할까요?

 

가장 시간 차이가 적은 순서로 뽑아 보면 해답에 다가갈 수 있을 겁니다.

icn %>% 
  filter(구분=='여객') %>%
  mutate(계획시간=paste(연, 월, 일, 계획) %>% ymd_hm,
             출발시간=paste(연, 월, 일, 출발) %>% ymd_hm,
             차이=interval(계획시간, 출발시간)/minutes(1)) %>%
  select(계획시간, 출발시간, 차이, 현황) %>%
  filter(현황=='지연') %>%
  arrange(차이)
## # A tibble: 8,129 x 4
##    계획시간            출발시간             차이 현황 
##    <dttm>              <dttm>              <dbl> <fct>
##  1 2019-09-21 01:10:00 2019-09-21 01:09:00    -1 지연 
##  2 2019-06-02 23:55:00 2019-06-02 23:55:00     0 지연 
##  3 2019-09-10 23:55:00 2019-09-10 23:58:00     3 지연 
##  4 2019-01-08 17:15:00 2019-01-08 17:45:00    30 지연 
##  5 2019-01-13 17:15:00 2019-01-13 17:45:00    30 지연 
##  6 2019-01-21 19:20:00 2019-01-21 19:50:00    30 지연 
##  7 2019-01-28 19:35:00 2019-01-28 20:05:00    30 지연 
##  8 2019-02-11 19:35:00 2019-02-11 20:05:00    30 지연 
##  9 2019-02-18 07:35:00 2019-02-18 08:05:00    30 지연 
## 10 2019-03-06 19:20:00 2019-03-06 19:50:00    30 지연 
## # ... with 8,119 more rows

 

맨 처음 세 행이 오류 때문이라고 가정하면 30분 이상 차이가 날 때부터 지연이라고 구분하는 걸 알 수 있습니다.

 

어떤 달에 비행기가 늦게 뜨는 일이 많았을까요?

 

따로 말씀드리지 않아도 month() 함수를 써야겠다는 생각이 드시죠?

 

먼저 mutate() 함수로 월을 담아두는 열을 따로 만들겠습니다.

 

그리고 다음 월과 현황을 기준으로 그룹을 짓고  n() 함수로 (정상) 출발과 지연이 몇 건인지 각각 세어보겠습니다.

 

계속해서 전체 비행편 가운데 몇 편이 지연 출발이었는지 계산을 해 '비율'이라는 열을 만들겠습니다.

 

마지막으로 이 비율에 따라 막대 그래프를 그리겠습니다.

 

월별 결과는 막대 그래프 위에 표시. 

 

지금까지 말씀드린 걸 코드로 쓰면 이렇습니다.

icn %>% 
  filter(구분=='여객') %>%
  mutate(출발시간=paste(연, 월, 일, 출발) %>% ymd_hm,
         월=month(출발시간, label=T)) %>%
  group_by(월, 현황) %>%
  summarise(count=n()) %>%
  mutate(비율=count/sum(count)) %>%
  filter(현황=='지연') %>%
  ggplot(aes(x=월, y=비율)) +
  geom_bar(stat='identity') +
  geom_text(aes(label=비율))

그래프 높이를 보면 2월에 지연율이 높다는 건 알겠지만 레이블이 너무 어지러우니까 이 부분만 정리해 보겠습니다.

 

100을 곱해서 비율(%)로 만들고 format() 함수를 써서 자릿수도 정리하는 코드는 이렇게 쓸 수 있습니다. 아, 위치도 살짝 아래로 내리고 글씨색도 하얗게 바꿉니다.

icn %>% 
  filter(구분=='여객') %>%
  mutate(출발시간=paste(연, 월, 일, 출발) %>% ymd_hm,
         월=month(출발시간, label=T)) %>%
  group_by(월, 현황) %>%
  summarise(count=n()) %>%
  mutate(비율=count/sum(count)) %>%
  filter(현황=='지연') %>%
  ggplot(aes(x=월, y=비율)) +
  geom_bar(stat='identity') +
  geom_text(aes(y=비율-.003, label=format(비율*100, digits=2)), color='#ffffff')

이어서 요일별 지연율을 알아봅니다.

 

wday() 함수에 label=T 속성을 줘서 요일별로 정리하는 걸 제외하면 나머지는 월별 코드와 같습니다.

icn %>% 
  filter(구분=='여객') %>%
  mutate(출발시간=paste(연, 월, 일, 출발) %>% ymd_hm,
         요일=wday(출발시간, label=T)) %>%
  group_by(요일, 현황) %>%
  summarise(count=n()) %>%
  mutate(비율=count/sum(count)) %>%
  filter(현황=='지연') %>%
  ggplot(aes(x=요일, y=비율)) +
  geom_bar(stat='identity') +
  geom_text(aes(y=비율-.003, label=format(비율*100, digits=2)), color='#ffffff')

금요일에 유독 지연율이 높은 이유가 뭔지 궁금하기는 합니다. 이유를 알고 계시는 분은 댓글 등으로 알려주셔도 좋겠습니다.

 

물론 hour() 함수를 쓰면 시간대별 결과도 알 수 있습니다.

icn %>% 
  filter(구분=='여객') %>%
  mutate(계획시간=paste(연, 월, 일, 계획) %>% ymd_hm,
         시간대=hour(계획시간)) %>%
  group_by(시간대, 현황) %>%
  summarise(count=n()) %>%
  mutate(비율=count/sum(count)) %>%
  filter(현황=='지연') %>%
  ggplot(aes(x=시간대, y=비율)) +
  geom_bar(stat='identity')

오전 4시에 유독 지연율이 높습니다.

 

추측컨대 이건 이 시각에 출발하는 편수가 적기 때문일 수 있습니다. 지연율을 계산할 때 분모가 줄어들면 비율 자체가 올라갈 수 있는 것.

 

정말 그런지 각 시간대별 출발 계획이던 비행기가 몇 대인지 확인해 보겠습니다. 

icn %>% 
  filter(구분=='여객') %>%
  mutate(계획시간=paste(연, 월, 일, 계획) %>% ymd_hm,
         시간대=hour(계획시간)) %>%
  group_by(시간대) %>%
  summarise(count=n())
## # A tibble: 24 x 2
##    시간대 count
##     <int> <int>
##  1      0  3337
##  2      1  1359
##  3      2   295
##  4      3    14
##  5      4    13
##  6      5   467
##  7      6  4282
##  8      7  7180
##  9      8  9037
## 10      9 11173
## # ... with 14 more rows

예, 정말 적습니다.

 

오전 3시 출발 계획이던 비행기도 역시 정말 적은데 다른 시간대와 엇비슷한 지연율을 유지하는 데도 이유가 있을 것 같습니다.

 

기왕 자료를 뽑았으니 그래프도 그리겠습니다.

icn %>% 
  filter(구분=='여객') %>%
  mutate(계획시간=paste(연, 월, 일, 계획) %>% ymd_hm,
         시간대=hour(계획시간)) %>%
  group_by(시간대) %>%
  summarise(count=n()) %>%
  ggplot(aes(x=시간대, y=count)) +
  geom_bar(stat='identity')

시간대별로 그릴 수 있으면 분 단위로도 그릴 수 있겠죠?

 

간단합니다. hour() 함수만 minute() 함수로 바꾸면 됩니다.

icn %>% 
  filter(구분=='여객') %>%
  mutate(계획시간=paste(연, 월, 일, 계획) %>% ymd_hm,
         시간대=minute(계획시간)) %>%
  group_by(시간대, 현황) %>%
  summarise(count=n()) %>%
  mutate(비율=count/sum(count)) %>%
  filter(현황=='지연') %>%
  ggplot(aes(x=시간대, y=비율)) +
  geom_bar(stat='identity')

일단 결과는 잘 나왔습니다. 다만 딱 한 가지 아쉬운 점이 있습니다.

 

원래 자료에서 5분 단위로 계획 시간을 구분하다 보니 이 결과도 5분 단위로 나왔습니다.

 

이걸 10분 단위로 바꾸려면 어떻게 해야 할까요?

 

제가 생각한 방식은 내림을 써서 시간을 앞당겨주는 것. 매시 정각(0분)과 5분에 출발을 계획한 비행기는 0~10분 사이에 10분과 15분 비행기는 10~20분 사이에 배치하는 방식입니다.

 

그러면 시간을 내리는 기준은 10분이 되어야겠죠? 이 기준은 그냥 '10 mins'처럼 쓰면 그만입니다.

 

그래서 최종 코드는 이렇습니다. 

icn %>% 
  filter(구분=='여객') %>%
  mutate(계획시간=paste(연, 월, 일, 계획) %>% ymd_hm,
             시간대=floor_date(계획시간, '10 mins') %>% minute()) %>%
  group_by(시간대, 현황) %>%
  summarise(count=n()) %>%
  mutate(비율=count/sum(count)) %>%
  filter(현황=='지연') %>%
  ggplot(aes(x=시간대, y=비율)) +
  geom_bar(stat='identity')

이 정도면 R로 날짜·시간 데이터를 다루는 감을 잡으셨을 줄로 믿습니다.

 

혹시 잘못된 부분이나 이해가 잘 가지 않는다는 부분이 있으다면 알려주시기를… 

 

그럼 모두들 Happy Tidyversing!

 

데이터 과학 입문서 '친절한 R with 스포츠 데이터'를 썼습니다

어쩌다 보니 '한국어 사용자에게 도움이 될지도 모르는 R 언어 기초 회화 교재'를 세상에 내놓게 됐습니다. 책 앞 부분은 '한국어 tidyverse 사투리' 번역, 뒷부분은 'tidymodels 억양 따라하기'에 초점

kuduz.tistory.com

 

댓글, 4

  • 댓글 수정/삭제 질문
    2020.12.18 14:00

    항상 좋은 글 잘보고 있습니다. 감사합니다!
    혹시 항공 데이터 수집하는 방법도 시간 남을 때 올려주시면 감사하겠습니다. 크롤링에 관한 포스트를 읽어보면서 어느정도 파악했는데 쉽지가 않아서요...ㅠㅠ

  • 댓글 수정/삭제 잿빛매
    2021.03.17 19:05

    좋은 글 감사합니다. 해보니까 오류가 나더라구요. 살펴보니 '계획' 과 '출발' 변수가 factor 가 아닌 character 로 되어 있어서 as.factor 로 속성을 변경하니 잘 되었습니다.

account_circle
vpn_key
web

security

mode_edit
KindeR | 카테고리 다른 글 더 보기