몇 번째 날int days 13

C는 다차원 배열(multi dimensional arrays)을 제공해 주지만 포인터의 배역보다는 많이 사용되지 않는다 이 절에서는 다차원 배열의 몇가지 성질에 대해서 살펴보겠다.

어떤달의 며칠이 1년의 몇번째 날인가를 계산하고 그 역으로도 계산하는 문제를 풀어보자 예를들면 3월1일은 그해의 60번째 날이 되고 윤년에는 61번째 날이 될 것이다 우선 두개의 함수를 정의하자 day_of_year는 월별 날짜를 연별 날짜로 바꾸는 함수이고,month_day는 연별 날짜를 월별 날짜로 바꾸는 함수이다.month_day 함수는 달 날짜 두 값을 리턴하게되므로 달과 날짜를 나타내는 매개변수는 포인터로 하기로 한다.

    month_day(1988,60,&m,&d)

는 m을 2로 d를 29로 해서 1988년의 60번째 날은 2월 29일임을 계산한다.

두함수는 같은정보로 실행되는데 그 정보는 어느 달이 며칠까지 있는지와 어느해가 윤년인지 하는 것이다. 이정볼르 가지고 있는 배열과 두함수는 다음과 같다.

staticchar daytab[2][13] = {

    {0,31,28,31,30,31,30,31,31,30,31,30,31},

    {0,31,29,31,30,31,30,31,31,30,31,30,31}

};

/* day_of_year : set day of year from month & day */

int day_of_year(int year,int month,int day)

{

    int i,leap;

    leap = year%4 == 0 && year%100 != 0 || year%400 == 0;

    for (i=1; i<month; i++)

        day += daytab[leap][i];

    return day;

}

/* month_day: set month, day from day of year */

void month_day(int year, int yearday, int *pmonth, int *pday) {

    int i, leap;

    leap = year%4 == 0 && year%100 != 0 || year%400 == 0;

    for (i = 1; yearday > daytab[leap][i]; i++)

        yearday -= daytab[leap][i];

    *pmonth = i;

    *pday = yearday;

}

윤년인지 아닌지를 나타내는 논리변수 leap의 값은 1(true) 또는 0(false)이므로 daytab이라는 배열의 첨자로 그대로 이용할수있다

배열 daytab은 day_of_year와 month_day에서 공동으로 사용할 수있게 외부형(external)으로 선언했다 여기서는 char 형의 변수에 다른 형의 데이터를 넣는 예를 보여주기 위해 daytab을 char로 했다

daytab은 2차원 배열이다.C에서는 2차원 배열을 각 원소가 1차원 배열인 배올로 취급한다 그러므로 배열 표시도 다른 언어에서 처럼

daytab[i,j] /* wrong */

로 하지 않고

daytab[i][j] /* [row][col] */

와 같이 한다. 표시방법이 다르지만 사용자는 다른언어와 같은 방법으로 배열을 사용하면 된다 2차원의 각 배열의 요소는 행단위로 지정된다 즉 1열 원소 2열 원소..이런식으로 저장된다

배열의 초기값을 정의할 때 초깃값을 중괄호 속에 써 준다. 2차원 배열의 경우 각 행의 초기값을 중괄호로 묶어서 써준다. C에서 배열의 첫번째 요소는 0번째 원소인데 1~12월 숫자 그대로 나타내기 위해 13요소의 배열을 잡고 0번째 요소의 값은 0으로 해주었다

2차원 배열이 함수로 전달되고 그 함수 내에서 배열을 선언할떄 열의 수는 반드시 써야 한다 2차원 배열을 매개변수로 사용할 때 각 행의 포인터가 전달되므로 행의 수는 선언할 필요가 없다 위와 같은 경우 13개의 정수형으로 이루어진 배열의 포인터가 전달되는 것이다 그러므로 daytab이란는 배열이 함수 f로 전달될 경우 f선언은

f(int daytab[2][13]){ ... }

또는

f(int daytab[ ][13]){ ... }

처럼 하면 되는데 행 번호를 나타내지 않으므로

f(int (*daytab)[13]){ ... }

로 해도 좋다. 이 문장은 매개변수가 13개 정수들의 배열을 가리키는 포인터임을 알려주는 것이다 이떄[]가 *보다 우선순위가 높으므로 ()가 필요하다. 만약

int *daytab[13]

과 같이 선언한다면 이는 13개 정수의 포인터로 이루어진 배열을 선언하는 것이 된다. 행의 숫자를 생략할 수있음은 앞에서 설명한 바와 같고 다른 것(열의 숫자)은 생략하면 안된다.

예제 5-8 day_of_year와 month_day 함수에 에러 검사하는 부분을 첨가해 보라

영어 낱말 '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

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

날짜 데이터 입력하기

몇 번째 날int days 13

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일까지 인천공항 실시간 운항 정보를 내려받아 정리한 자료입니다.

몇 번째 날int days 13
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

몇 번째 날int days 13