챕터 4 데이터 주무르기

씹고 뜯고 맛보고 즐기고 – 이가탄

4.1 datatoys 패키지 소개

Play gives children a chance to practice what they are learning
– Fred Rogers

데이터를 분석하는데 있어서 가장 큰 장벽은 데이터를 읽어오고, 분석에 알맞는 형태로 만드는 작업이다. datatoys 패키지는 공공데이터포털에서 제공하는 각종 데이터를 R에서 곧바로 분석할 수 있는 상태로 제공한다(python 버전도 있지만 R이 조금 먼저 개발된다). 패키지의 이름처럼 마치 어린 아이가 데이터를 재밌는 장난감처럼 갖고 놀 수 있도록 함이 목적이다. 본 챕터에서는 datatoys 패키지를 활용하여 데이터를 주무르는 방법을 배워보도록 하자.

library(datatoys)

datatoys패키지에서 제공하는 데이터셋에 대한 자세한 정보는 문서를 통해 확인할 수 있다. 또는 ?를 활용해 다음 ?accident과 같이 확인할 수 있다.

4.2 데이터프레임 이해

이미지, 영상, 문서 같이 여러가지 형태의 데이터가 있지만, 우리는 직사각형처럼 생긴 데이터(데이터프레임)에 초점을 맞출 것이다. 데이터프레임은 행과 열로 이루어진 표 형태의 데이터이다. 행은 관측치를, 열은 변수를 의미한다.

library(datatoys)
library(dplyr)

str(restaurant)
## tibble [137 × 9] (S3: tbl_df/tbl/data.frame)
##  $ 시군명          : chr [1:137] "가평군" "고양시" "고양시" "고양시" ...
##  $ 음식점명        : chr [1:137] "가평축협 한우명가" "청정바지락칼국수" "양촌리아구" "정통중화요리 남궁" ...
##  $ 맛집전화번호    : chr [1:137] "031-581-1592" "031-912-7676" "031-911-0430" "031-911-3702" ...
##  $ 대표음식명      : chr [1:137] "푸른연잎한우명품꽃등심" "천년초들깨수제비" "아구탕" "해물고추짬뽕, 양장피잡채" ...
##  $ 소재지우편번호  : chr [1:137] "12422" "10359" "10218" "10367" ...
##  $ 소재지도로명주소: chr [1:137] "경기도 가평군 가평읍 달전로 19" "경기도 고양시 일산동구 일산로463번길 7" "경기도 고양시 일산서구 대화2로 152" "경기도 고양시 일산서구 일산로 682" ...
##  $ 소재지지번주소  : chr [1:137] "경기도 가평군 가평읍 달전리 382-1번지" "경기도 고양시 일산동구 정발산동 1148번지" "경기도 고양시 일산서구 대화동 762-3번지" "경기도 고양시 일산서구 대화동 2101번지" ...
##  $ WGS84위도       : chr [1:137] "37.8158443" "37.6737073" "37.6719314" "37.6820421" ...
##  $ WGS84경도       : chr [1:137] "127.5161283" "126.7753751" "126.7362187" "126.7535498" ...

restaurant 데이터는 137개의 관측치와 9개의 변수로 이루어져 있다. 각 행은 경기도 한 음식점을 의미하고, 열은 음식점의 정보를 의미한다. 시군명, 음식점명, 맛집 전화번호, 대표 음식명, 소재지 우편번호 등의 정보를 제공한다.

4.3 음주운전 데이터 주무르기

datatoys 패키지에는 drunkdrive라는 데이터셋이 있다. 이 데이터는 경찰청에서 음주운전 적발기록을 통한 음주예방을 위해 경찰처에서 공개한 자료로 성별, 적발횟수, 나이, 알콜농도, 측정일시, 관할경찰서 등의 정보를 제공한다. head() 함수를 통해 어떤 모양으로 생긴 데이터인지부터 확인해보자.

head(drunkdrive)
## # A tibble: 6 × 8
##   성별  적발횟수  나이 알콜농도 측정일시            관할경찰서     나이불명 측정 
##   <fct>    <int> <dbl>    <dbl> <dttm>              <chr>          <chr>    <chr>
## 1 남자         1    29    0.153 2022-07-01 00:00:00 아산경찰서     정상     측정 
## 2 남자         1    28    0.046 2022-07-01 00:02:00 전주덕진경찰서 정상     측정 
## 3 남자         1    61    0.047 2022-07-01 00:02:00 서울관악경찰서 정상     측정 
## 4 여자         1    40    0.185 2022-07-01 00:04:00 부산연제경찰서 정상     측정 
## 5 남자         1    66    0.139 2022-07-01 00:05:00 서울마포경찰서 정상     측정 
## 6 남자         1    31    0.214 2022-07-01 00:05:00 대전중부경찰서 정상     측정

데이터를 주무르기 위한 많은 패키지가 있지만, 가장 많은 사람들이 사용하는 dplyr라는 최적의 패키지가 있다. 이번 챕터에서는 dplyr에서 가장 많이 사용되는 몇가지 함수를 배워보도록 하자.

library(dplyr)

4.3.1 select()

drunkdrive는 총 8가지 변수를 갖고 있다. 너무 많은 데이터들에 압도당하지 않으려면 필요한 변수만 선택(select) 할 필요가 있다. select 함수는 데이터셋에서 필요한 변수(열)만을 선택할 때 사용한다. 성별, 적발횟수, 나이, 알콜농도 열만 선택해보자.

drunkdrive %>% select(성별, 적발횟수, 나이, 알콜농도)
## # A tibble: 12,021 × 4
##    성별  적발횟수  나이 알콜농도
##    <fct>    <int> <dbl>    <dbl>
##  1 남자         1    29    0.153
##  2 남자         1    28    0.046
##  3 남자         1    61    0.047
##  4 여자         1    40    0.185
##  5 남자         1    66    0.139
##  6 남자         1    31    0.214
##  7 남자         1    26    0.136
##  8 남자         1    44    0.084
##  9 여자         1    59    0.184
## 10 남자         1    23    0.164
## # … with 12,011 more rows

데이터가 훨씬 간결해졌다. :를 사용하면 범위를 지정할 수도 있다. 동일한 결과를 보여준다.

drunkdrive %>% select(성별:알콜농도)
## # A tibble: 12,021 × 4
##    성별  적발횟수  나이 알콜농도
##    <fct>    <int> <dbl>    <dbl>
##  1 남자         1    29    0.153
##  2 남자         1    28    0.046
##  3 남자         1    61    0.047
##  4 여자         1    40    0.185
##  5 남자         1    66    0.139
##  6 남자         1    31    0.214
##  7 남자         1    26    0.136
##  8 남자         1    44    0.084
##  9 여자         1    59    0.184
## 10 남자         1    23    0.164
## # … with 12,011 more rows

때로는 필요한 변수를 제외하고 싶을 때도 있다. 이때는 -를 사용하면 된다. 성별, 적발횟수, 나이, 알콜농도 열을 제외하고 보고 싶다면 다음과 같이 사용하면 된다.

drunkdrive %>% select(-성별, -적발횟수, -나이, -알콜농도)
## # A tibble: 12,021 × 4
##    측정일시            관할경찰서     나이불명 측정 
##    <dttm>              <chr>          <chr>    <chr>
##  1 2022-07-01 00:00:00 아산경찰서     정상     측정 
##  2 2022-07-01 00:02:00 전주덕진경찰서 정상     측정 
##  3 2022-07-01 00:02:00 서울관악경찰서 정상     측정 
##  4 2022-07-01 00:04:00 부산연제경찰서 정상     측정 
##  5 2022-07-01 00:05:00 서울마포경찰서 정상     측정 
##  6 2022-07-01 00:05:00 대전중부경찰서 정상     측정 
##  7 2022-07-01 00:10:00 안산상록경찰서 정상     측정 
##  8 2022-07-01 00:13:00 부산사상경찰서 정상     측정 
##  9 2022-07-01 00:13:00 군산경찰서     정상     측정 
## 10 2022-07-01 00:13:00 대구북부경찰서 정상     측정 
## # … with 12,011 more rows

때론 데이터 열의 순서를 바꾸고 싶을 때가 있다. everything() 함수를 함께 활용하면 데이터셋의 남은 모든 열을 선택할 수 있다.

drunkdrive %>% select(알콜농도, 적발횟수, 나이, everything())
## # A tibble: 12,021 × 8
##    알콜농도 적발횟수  나이 성별  측정일시            관할경찰서     나이불명 측정 
##       <dbl>    <int> <dbl> <fct> <dttm>              <chr>          <chr>    <chr>
##  1    0.153        1    29 남자  2022-07-01 00:00:00 아산경찰서     정상     측정 
##  2    0.046        1    28 남자  2022-07-01 00:02:00 전주덕진경찰서 정상     측정 
##  3    0.047        1    61 남자  2022-07-01 00:02:00 서울관악경찰서 정상     측정 
##  4    0.185        1    40 여자  2022-07-01 00:04:00 부산연제경찰서 정상     측정 
##  5    0.139        1    66 남자  2022-07-01 00:05:00 서울마포경찰서 정상     측정 
##  6    0.214        1    31 남자  2022-07-01 00:05:00 대전중부경찰서 정상     측정 
##  7    0.136        1    26 남자  2022-07-01 00:10:00 안산상록경찰서 정상     측정 
##  8    0.084        1    44 남자  2022-07-01 00:13:00 부산사상경찰서 정상     측정 
##  9    0.184        1    59 여자  2022-07-01 00:13:00 군산경찰서     정상     측정 
## 10    0.164        1    23 남자  2022-07-01 00:13:00 대구북부경찰서 정상     측정 
## # … with 12,011 more rows

datatoys 패키지에는 장소별 원인별 화재통계를 제공하는 fire이라는 데이터셋이 있다. 데이터는 아래와 같은 열로 구성되어 있다.

str(fire)
## tibble [44,435 × 18] (S3: tbl_df/tbl/data.frame)
##  $ 연번            : int [1:44435] 1 2 3 4 5 6 7 8 9 10 ...
##  $ 사망            : int [1:44435] 0 0 0 0 0 0 0 0 0 0 ...
##  $ 부상            : int [1:44435] 0 0 0 0 0 0 0 0 0 0 ...
##  $ 인명피해.명.소계: int [1:44435] 0 0 0 0 0 0 0 0 0 0 ...
##  $ 재산피해소계    : int [1:44435] 2920 0 137 326 0 19800 33 30800 0 575 ...
##  $ 시도            : chr [1:44435] "경상남도" "서울특별시" "서울특별시" "서울특별시" ...
##  $ 시군구          : chr [1:44435] "합천군" "영등포구" "강남구" "도봉구" ...
##  $ 읍면동          : chr [1:44435] "청덕면" "여의도동" "논현동" "쌍문동" ...
##  $ 발화열원        : chr [1:44435] "작동기기" "담뱃불, 라이터불" "담뱃불, 라이터불" "담뱃불, 라이터불" ...
##  $ 발화열원소분류  : chr [1:44435] "전기적 아크(단락)" "담뱃불" "담뱃불" "담뱃불" ...
##  $ 발화요인대분류  : chr [1:44435] "전기적 요인" "부주의" "부주의" "부주의" ...
##  $ 발화요인소분류  : chr [1:44435] "접촉불량에 의한 단락" "담배꽁초" "담배꽁초" "담배꽁초" ...
##  $ 최초착화물대분류: chr [1:44435] "전기,전자" "기타" "침구,직물류" "종이,목재,건초등" ...
##  $ 최초착화물소분류: chr [1:44435] "전선피복" "기타" "기타(침구,직물류)" "기타(종이,목재,건초등)" ...
##  $ 장소대분류      : chr [1:44435] "주거" "임야" "자동차,철도차량" "판매,업무시설" ...
##  $ 장소중분류      : chr [1:44435] "단독주택" "들불" "자동차" "일반업무" ...
##  $ 장소소분류      : chr [1:44435] "단독주택" "기타 들불" "오토바이" "일반빌딩" ...
##  $ 지번동          : chr [1:44435] "청덕면" "여의도동" "논현1동" "쌍문2동" ...

starts_with()ends_with() 함수를 함께 활용하면, 특정 문자열로 시작하는 열이나 특정 문자열로 끝나는 열을 선택할 수 있다. 장소로 시작하는 열을 선택해보자.

fire %>% select(starts_with("장소"))
## # A tibble: 44,435 × 3
##    장소대분류      장소중분류 장소소분류            
##    <chr>           <chr>      <chr>                 
##  1 주거            단독주택   단독주택              
##  2 임야            들불       기타 들불             
##  3 자동차,철도차량 자동차     오토바이              
##  4 판매,업무시설   일반업무   일반빌딩              
##  5 주거            공동주택   다세대주택            
##  6 자동차,철도차량 자동차     승용자동차            
##  7 생활서비스      오락시설   PC방(인터넷게임제공업)
##  8 자동차,철도차량 자동차     승용자동차            
##  9 기타            야외       기타야외              
## 10 생활서비스      일상서비스 기타 일상서비스       
## # … with 44,425 more rows

분류로 끝나는 열을 선택해보자.

fire %>% select(ends_with("분류"))
## # A tibble: 44,435 × 8
##    발화열원소분류    발화요인대분류 발화요인소분류       최초착화물대분류            최초착화물소분류       장소대분류      장소중분류 장소소분류            
##    <chr>             <chr>          <chr>                <chr>                       <chr>                  <chr>           <chr>      <chr>                 
##  1 전기적 아크(단락) 전기적 요인    접촉불량에 의한 단락 전기,전자                   전선피복               주거            단독주택   단독주택              
##  2 담뱃불            부주의         담배꽁초             기타                        기타                   임야            들불       기타 들불             
##  3 담뱃불            부주의         담배꽁초             침구,직물류                 기타(침구,직물류)      자동차,철도차량 자동차     오토바이              
##  4 담뱃불            부주의         담배꽁초             종이,목재,건초등            기타(종이,목재,건초등) 판매,업무시설   일반업무   일반빌딩              
##  5 기기 전도,복사열  부주의         음식물 조리중        식품                        음식물                 주거            공동주택   다세대주택            
##  6 기기 전도,복사열  기계적 요인    과열, 과부하         자동차,철도차량,선박,항공기 벨트                   자동차,철도차량 자동차     승용자동차            
##  7 담뱃불            부주의         담배꽁초             종이,목재,건초등            종이                   생활서비스      오락시설   PC방(인터넷게임제공업)
##  8 미상              미상           미상                 미상                        미상                   자동차,철도차량 자동차     승용자동차            
##  9 모닥불, 연탄, 숯  부주의         불씨,불꽃,화원방치   종이,목재,건초등            풀, 나뭇잎             기타            야외       기타야외              
## 10 전기적 아크(단락) 전기적 요인    절연열화에 의한 단락 전기,전자                   전선피복               생활서비스      일상서비스 기타 일상서비스       
## # … with 44,425 more rows

contains() 함수를 사용하면 특정 문자열을 포함하는 열을 선택할 수 있다. 요인이라는 문자열을 포함하는 열을 선택해보자.

fire %>% select(contains("요인"))
## # A tibble: 44,435 × 2
##    발화요인대분류 발화요인소분류      
##    <chr>          <chr>               
##  1 전기적 요인    접촉불량에 의한 단락
##  2 부주의         담배꽁초            
##  3 부주의         담배꽁초            
##  4 부주의         담배꽁초            
##  5 부주의         음식물 조리중       
##  6 기계적 요인    과열, 과부하        
##  7 부주의         담배꽁초            
##  8 미상           미상                
##  9 부주의         불씨,불꽃,화원방치  
## 10 전기적 요인    절연열화에 의한 단락
## # … with 44,425 more rows

select()함수는 ’열’을 선택할 때 사용한다. 만약 ’행’을 선택하고 싶다면 slice() 함수를 사용할 수 있다. slice() 함수는 데이터셋에서 특정 행을 선택할 때 사용한다. 1, 3, 5, 7, 9번 행을 선택해보자.

drunkdrive %>% slice(1, 3, 5, 7, 9)
## # A tibble: 5 × 8
##   성별  적발횟수  나이 알콜농도 측정일시            관할경찰서     나이불명 측정 
##   <fct>    <int> <dbl>    <dbl> <dttm>              <chr>          <chr>    <chr>
## 1 남자         1    29    0.153 2022-07-01 00:00:00 아산경찰서     정상     측정 
## 2 남자         1    61    0.047 2022-07-01 00:02:00 서울관악경찰서 정상     측정 
## 3 남자         1    66    0.139 2022-07-01 00:05:00 서울마포경찰서 정상     측정 
## 4 남자         1    26    0.136 2022-07-01 00:10:00 안산상록경찰서 정상     측정 
## 5 여자         1    59    0.184 2022-07-01 00:13:00 군산경찰서     정상     측정

연결된 행을 선택하기 위해서는 :를 활용할 수 있다.

drunkdrive %>% slice(1:5)
## # A tibble: 5 × 8
##   성별  적발횟수  나이 알콜농도 측정일시            관할경찰서     나이불명 측정 
##   <fct>    <int> <dbl>    <dbl> <dttm>              <chr>          <chr>    <chr>
## 1 남자         1    29    0.153 2022-07-01 00:00:00 아산경찰서     정상     측정 
## 2 남자         1    28    0.046 2022-07-01 00:02:00 전주덕진경찰서 정상     측정 
## 3 남자         1    61    0.047 2022-07-01 00:02:00 서울관악경찰서 정상     측정 
## 4 여자         1    40    0.185 2022-07-01 00:04:00 부산연제경찰서 정상     측정 
## 5 남자         1    66    0.139 2022-07-01 00:05:00 서울마포경찰서 정상     측정

4.3.2 arrange()

뭐든 비교하고 줄세우는건 재밌다. 우리가 그 대상이 아니라면 말이다. arrange() 함수를 사용하면 데이터를 쉽게 정렬할 수 있다. arrange() 함수는 데이터셋에서 특정 열을 기준으로 정렬할 때 사용한다. drunkdrive데이터셋은 음주운전 적발자의 정보를 담고 있다. 나이를 기준으로 정렬하면 어떤 결과가 나올까?

drunkdrive %>% arrange(나이)
## # A tibble: 12,021 × 8
##    성별  적발횟수  나이 알콜농도 측정일시            관할경찰서     나이불명 측정 
##    <fct>    <int> <dbl>    <dbl> <dttm>              <chr>          <chr>    <chr>
##  1 남자         1   -77    0.168 2022-07-16 05:31:00 동두천경찰서   정상     측정 
##  2 남자         1    14    0.112 2022-07-17 03:38:00 원주경찰서     정상     측정 
##  3 남자         1    15    0.114 2022-07-02 03:51:00 공주경찰서     정상     측정 
##  4 남자         1    15    0.058 2022-07-10 03:28:00 나주경찰서     정상     측정 
##  5 여자         1    15    0.066 2022-07-25 07:12:00 안산상록경찰서 정상     측정 
##  6 남자         1    16    0.083 2022-07-03 02:19:00 서울강북경찰서 정상     측정 
##  7 남자         1    16    0.124 2022-07-03 02:40:00 천안동남경찰서 정상     측정 
##  8 남자         1    16    0.13  2022-07-04 01:21:00 서울중랑경찰서 정상     측정 
##  9 남자         1    16    0.091 2022-07-05 02:58:00 창원중부경찰서 정상     측정 
## 10 남자         1    16    0.075 2022-07-05 19:35:00 전주덕진경찰서 정상     측정 
## # … with 12,011 more rows

Outlier다! Outlier를 발견했다. -77살은 존재할 수 없는 나이다. -77살을 제외하고 보면 2022년 7월 원주에서 적발된 14살이 가장 어린 나이다. arrange()함수는 기본적으로 작은수부터 정렬한다. desc()를 함께 사용하면 큰 수부터 정렬할 수 있다. 알콜농도가 높은 순서대로 정렬해보자.

drunkdrive %>% arrange(desc(알콜농도))
## # A tibble: 12,021 × 8
##    성별  적발횟수  나이 알콜농도 측정일시            관할경찰서     나이불명 측정 
##    <fct>    <int> <dbl>    <dbl> <dttm>              <chr>          <chr>    <chr>
##  1 남자         1    67    0.93  2022-07-18 22:27:00 서울서초경찰서 정상     측정 
##  2 남자         1    45    0.77  2022-07-06 20:56:00 수원서부경찰서 정상     측정 
##  3 남자         1    50    0.66  2022-07-03 18:46:00 안산상록경찰서 정상     측정 
##  4 남자         1    57    0.423 2022-07-20 14:30:00 의령경찰서     정상     측정 
##  5 남자         1    53    0.404 2022-07-04 16:50:00 일산동부경찰서 정상     측정 
##  6 남자         1    38    0.403 2022-07-02 14:14:00 구미경찰서     정상     측정 
##  7 여자         1    30    0.396 2022-07-20 02:24:00 양산경찰서     정상     측정 
##  8 남자         1    52    0.385 2022-07-23 23:17:00 오산경찰서     정상     측정 
##  9 남자         1    43    0.379 2022-07-30 11:24:00 서울송파경찰서 정상     측정 
## 10 남자         1    33    0.378 2022-07-31 23:37:00 인천논현경찰서 정상     측정 
## # … with 12,011 more rows

2022년 7월 서울시 서초구에서 적발된 67세 남성의 알콜농도는 0.93으로 가장 높았다. 나이가 어린 순으로 정렬하면서 만약 나이가 같은 경우 알콜농도가 높은 순서대로 정렬하고 싶으면 다음과 같이 활용할 수 있다.

drunkdrive %>% arrange(나이, desc(알콜농도))
## # A tibble: 12,021 × 8
##    성별  적발횟수  나이 알콜농도 측정일시            관할경찰서     나이불명 측정 
##    <fct>    <int> <dbl>    <dbl> <dttm>              <chr>          <chr>    <chr>
##  1 남자         1   -77    0.168 2022-07-16 05:31:00 동두천경찰서   정상     측정 
##  2 남자         1    14    0.112 2022-07-17 03:38:00 원주경찰서     정상     측정 
##  3 남자         1    15    0.114 2022-07-02 03:51:00 공주경찰서     정상     측정 
##  4 여자         1    15    0.066 2022-07-25 07:12:00 안산상록경찰서 정상     측정 
##  5 남자         1    15    0.058 2022-07-10 03:28:00 나주경찰서     정상     측정 
##  6 남자         1    16    0.14  2022-07-17 03:26:00 용인서부경찰서 정상     측정 
##  7 남자         1    16    0.137 2022-07-17 02:32:00 홍성경찰서     정상     측정 
##  8 남자         1    16    0.13  2022-07-04 01:21:00 서울중랑경찰서 정상     측정 
##  9 남자         1    16    0.124 2022-07-03 02:40:00 천안동남경찰서 정상     측정 
## 10 남자         1    16    0.103 2022-07-30 05:24:00 고양경찰서     정상     측정 
## # … with 12,011 more rows

4.3.3 filter()

그렇다면 음주측정을 거부한 사람들이 있을까? 조건에 맞는 행만 뽑아내고 싶을 때는 filter() 함수를 사용해보자. filter() 함수는 조건이 TRUE인 데이터만을 선택할 때 사용한다. 측정이라는 열에서 거부인 데이터만 뽑아보자.

drunkdrive %>% filter(측정 == "거부")
## # A tibble: 457 × 8
##    성별  적발횟수  나이 알콜농도 측정일시            관할경찰서       나이불명 측정 
##    <fct>    <int> <dbl>    <dbl> <dttm>              <chr>            <chr>    <chr>
##  1 남자         1    49       NA 2022-07-01 00:23:00 영암경찰서       정상     거부 
##  2 남자         1    33       NA 2022-07-01 01:05:00 안산상록경찰서   정상     거부 
##  3 남자         1    37       NA 2022-07-01 01:10:00 서울서대문경찰서 정상     거부 
##  4 여자         1    25       NA 2022-07-01 03:04:00 광주서부경찰서   정상     거부 
##  5 남자         1    42       NA 2022-07-01 04:16:00 진해경찰서       정상     거부 
##  6 남자         1    43       NA 2022-07-01 06:05:00 청주청원경찰서   정상     거부 
##  7 남자         1    64       NA 2022-07-01 16:47:00 성주경찰서       정상     거부 
##  8 남자         1    66       NA 2022-07-01 20:14:00 괴산경찰서       정상     거부 
##  9 남자         1    32       NA 2022-07-01 21:07:00 안양동안경찰서   정상     거부 
## 10 남자         1    57       NA 2022-07-01 21:57:00 서울강서경찰서   정상     거부 
## # … with 447 more rows

남자(또는 여자)만 뽑아낼 수도,

drunkdrive %>% filter(성별 == "남자")
## # A tibble: 10,629 × 8
##    성별  적발횟수  나이 알콜농도 측정일시            관할경찰서     나이불명 측정 
##    <fct>    <int> <dbl>    <dbl> <dttm>              <chr>          <chr>    <chr>
##  1 남자         1    29    0.153 2022-07-01 00:00:00 아산경찰서     정상     측정 
##  2 남자         1    28    0.046 2022-07-01 00:02:00 전주덕진경찰서 정상     측정 
##  3 남자         1    61    0.047 2022-07-01 00:02:00 서울관악경찰서 정상     측정 
##  4 남자         1    66    0.139 2022-07-01 00:05:00 서울마포경찰서 정상     측정 
##  5 남자         1    31    0.214 2022-07-01 00:05:00 대전중부경찰서 정상     측정 
##  6 남자         1    26    0.136 2022-07-01 00:10:00 안산상록경찰서 정상     측정 
##  7 남자         1    44    0.084 2022-07-01 00:13:00 부산사상경찰서 정상     측정 
##  8 남자         1    23    0.164 2022-07-01 00:13:00 대구북부경찰서 정상     측정 
##  9 남자         1    33    0.17  2022-07-01 00:15:00 의왕경찰서     정상     측정 
## 10 남자         1    37    0.144 2022-07-01 00:16:00 광주북부경찰서 정상     측정 
## # … with 10,619 more rows

미성년자만 뽑아낼 수도 있다. 어른들이 보다 관심을 가지고 돌봐줘야 할 아이들이 무려 148명이나 존재한다.

drunkdrive %>% filter(나이 < 20)
## # A tibble: 148 × 8
##    성별  적발횟수  나이 알콜농도 측정일시            관할경찰서     나이불명 측정 
##    <fct>    <int> <dbl>    <dbl> <dttm>              <chr>          <chr>    <chr>
##  1 남자         1    18    0.158 2022-07-02 02:56:00 천안서북경찰서 정상     측정 
##  2 남자         1    15    0.114 2022-07-02 03:51:00 공주경찰서     정상     측정 
##  3 남자         1    18    0.237 2022-07-02 23:13:00 용인서부경찰서 정상     측정 
##  4 남자         1    19    0.03  2022-07-03 02:18:00 화순경찰서     정상     측정 
##  5 남자         1    16    0.083 2022-07-03 02:19:00 서울강북경찰서 정상     측정 
##  6 남자         1    16    0.124 2022-07-03 02:40:00 천안동남경찰서 정상     측정 
##  7 남자         1    19    0.149 2022-07-03 05:18:00 밀양경찰서     정상     측정 
##  8 남자         1    19    0.049 2022-07-03 08:20:00 광주서부경찰서 정상     측정 
##  9 남자         1    17    0.045 2022-07-03 23:03:00 김해서부경찰서 정상     측정 
## 10 남자         1    19    0.07  2022-07-04 01:01:00 부천원미경찰서 정상     측정 
## # … with 138 more rows

is.na() 함수를 사용해 결측치(NA)를 뽑아낼 수 있다. NA는 Not Available의 약자로 결측치를 의미한다.

drunkdrive %>% filter(is.na(측정일시))
## # A tibble: 0 × 8
## # … with 8 variables: 성별 <fct>, 적발횟수 <int>, 나이 <dbl>, 알콜농도 <dbl>, 측정일시 <dttm>, 관할경찰서 <chr>, 나이불명 <chr>, 측정 <chr>

하지만 대부분의 상황에서는 결측치를 찾아내기 보다. 결측치를 제거하는데 더 자주 사용된다. 결측치를 제거하고 싶다면 !is.na()를 사용하면 된다.

drunkdrive %>% filter(!is.na(측정일시))
## # A tibble: 12,021 × 8
##    성별  적발횟수  나이 알콜농도 측정일시            관할경찰서     나이불명 측정 
##    <fct>    <int> <dbl>    <dbl> <dttm>              <chr>          <chr>    <chr>
##  1 남자         1    29    0.153 2022-07-01 00:00:00 아산경찰서     정상     측정 
##  2 남자         1    28    0.046 2022-07-01 00:02:00 전주덕진경찰서 정상     측정 
##  3 남자         1    61    0.047 2022-07-01 00:02:00 서울관악경찰서 정상     측정 
##  4 여자         1    40    0.185 2022-07-01 00:04:00 부산연제경찰서 정상     측정 
##  5 남자         1    66    0.139 2022-07-01 00:05:00 서울마포경찰서 정상     측정 
##  6 남자         1    31    0.214 2022-07-01 00:05:00 대전중부경찰서 정상     측정 
##  7 남자         1    26    0.136 2022-07-01 00:10:00 안산상록경찰서 정상     측정 
##  8 남자         1    44    0.084 2022-07-01 00:13:00 부산사상경찰서 정상     측정 
##  9 여자         1    59    0.184 2022-07-01 00:13:00 군산경찰서     정상     측정 
## 10 남자         1    23    0.164 2022-07-01 00:13:00 대구북부경찰서 정상     측정 
## # … with 12,011 more rows

기본적으로 filter()의 인자들은 ‘and’ 조건으로 연결된다. 즉, 남자이면서 미성년자인 데이터를 뽑아내고 싶다면 다음과 같이 활용하면 된다.

drunkdrive %>% filter(성별 == "남자", 나이 < 20)
## # A tibble: 133 × 8
##    성별  적발횟수  나이 알콜농도 측정일시            관할경찰서     나이불명 측정 
##    <fct>    <int> <dbl>    <dbl> <dttm>              <chr>          <chr>    <chr>
##  1 남자         1    18    0.158 2022-07-02 02:56:00 천안서북경찰서 정상     측정 
##  2 남자         1    15    0.114 2022-07-02 03:51:00 공주경찰서     정상     측정 
##  3 남자         1    18    0.237 2022-07-02 23:13:00 용인서부경찰서 정상     측정 
##  4 남자         1    19    0.03  2022-07-03 02:18:00 화순경찰서     정상     측정 
##  5 남자         1    16    0.083 2022-07-03 02:19:00 서울강북경찰서 정상     측정 
##  6 남자         1    16    0.124 2022-07-03 02:40:00 천안동남경찰서 정상     측정 
##  7 남자         1    19    0.149 2022-07-03 05:18:00 밀양경찰서     정상     측정 
##  8 남자         1    19    0.049 2022-07-03 08:20:00 광주서부경찰서 정상     측정 
##  9 남자         1    17    0.045 2022-07-03 23:03:00 김해서부경찰서 정상     측정 
## 10 남자         1    19    0.07  2022-07-04 01:01:00 부천원미경찰서 정상     측정 
## # … with 123 more rows

만약 ‘or’ 조건으로 연결하고 싶다면 |를 사용하면 된다. |는 or을 의미한다. |를 사용하면 남자이거나 미성년자인 데이터를 뽑아낼 수 있다.

drunkdrive %>% filter(성별 == "남자" | 나이 < 20)
## # A tibble: 10,644 × 8
##    성별  적발횟수  나이 알콜농도 측정일시            관할경찰서     나이불명 측정 
##    <fct>    <int> <dbl>    <dbl> <dttm>              <chr>          <chr>    <chr>
##  1 남자         1    29    0.153 2022-07-01 00:00:00 아산경찰서     정상     측정 
##  2 남자         1    28    0.046 2022-07-01 00:02:00 전주덕진경찰서 정상     측정 
##  3 남자         1    61    0.047 2022-07-01 00:02:00 서울관악경찰서 정상     측정 
##  4 남자         1    66    0.139 2022-07-01 00:05:00 서울마포경찰서 정상     측정 
##  5 남자         1    31    0.214 2022-07-01 00:05:00 대전중부경찰서 정상     측정 
##  6 남자         1    26    0.136 2022-07-01 00:10:00 안산상록경찰서 정상     측정 
##  7 남자         1    44    0.084 2022-07-01 00:13:00 부산사상경찰서 정상     측정 
##  8 남자         1    23    0.164 2022-07-01 00:13:00 대구북부경찰서 정상     측정 
##  9 남자         1    33    0.17  2022-07-01 00:15:00 의왕경찰서     정상     측정 
## 10 남자         1    37    0.144 2022-07-01 00:16:00 광주북부경찰서 정상     측정 
## # … with 10,634 more rows

4.3.4 mutate()

mutate() 함수는 데이터셋에 새로운 열을 추가할 때 사용한다. mutate() 함수는 기존의 열을 사용해 새로운 열을 만들 때 사용한다. mutate() 함수는 항상 데이터프레임의 마지막 열에 새로운 열을 추가한다. datatoys 패키지의 tuition 데이터셋은 한국장학재단에서 제공하는 매년 4월 대학정보공시 기준의 대학별 입학정원, 평균입학금, 평균등록금 정보 등을 제공한다. 먼저 select() 함수를 통해 필요한 데이터를 뽑고, mutate() 함수를 사용해 한 학기당 등록금을 계산해보자. 한 학기당 등록금은 평균 등록금을 2로 나눈 값이다.

tuition %>% 
  select(대학명, 평균등록금.원.) %>% 
  mutate(한학기당등록금 = 평균등록금.원. / 2)
## # A tibble: 388 × 3
##    대학명             평균등록금.원. 한학기당등록금
##    <chr>                       <int>          <dbl>
##  1 강릉원주대학교            4261939       2130970.
##  2 강원대학교                4126501       2063250.
##  3 경남과학기술대학교        3800174       1900087 
##  4 경북대학교                4499592       2249796 
##  5 경상국립대학교            4083198       2041599 
##  6 경인교육대학교            3189000       1594500 
##  7 공주교육대학교            3424000       1712000 
##  8 공주대학교                3826015       1913008.
##  9 광주과학기술원            2060000       1030000 
## 10 광주교육대학교            3476500       1738250 
## # … with 378 more rows

또는 ‘원’ 단위의 등록금을 ‘만원’ 단위로 바꿔 볼 수도 있다.

tuition %>% 
  select(대학명, 평균등록금.원.) %>% 
  mutate(평균등록금_만원 = 평균등록금.원. / 10000) %>% 
  arrange(desc(평균등록금_만원))
## # A tibble: 388 × 3
##    대학명               평균등록금.원. 평균등록금_만원
##    <chr>                         <int>           <dbl>
##  1 한국공학대학교              9034616            903.
##  2 한국에너지공과대학교        9000000            900 
##  3 연세대학교                  8949735            895.
##  4 추계예술대학교              8778884            878.
##  5 신한대학교                  8714105            871.
##  6 이화여자대학교              8689951            869.
##  7 을지대학교                  8492904            849.
##  8 한양대학교                  8486720            849.
##  9 한국항공대학교              8465834            847.
## 10 성균관대학교                8381038            838.
## # … with 378 more rows

한국공학대학교의 평균 등록금이 연간 903만원으로 가장 높다. 순위를 표시하고 싶으면 row_number() 함수를 사용하면 된다. row_number() 함수는 데이터프레임의 행 번호를 표시한다. 높은 순위부터 순서대로 표시하고 싶다면 desc() 함수를 사용하면 된다.

tuition %>% 
  select(대학명, 평균등록금.원.) %>% 
  mutate(평균등록금_만원 = 평균등록금.원. / 10000) %>% 
  arrange(desc(평균등록금_만원)) %>% 
  mutate(순위 = row_number(desc(평균등록금_만원)))
## # A tibble: 388 × 4
##    대학명               평균등록금.원. 평균등록금_만원  순위
##    <chr>                         <int>           <dbl> <int>
##  1 한국공학대학교              9034616            903.     1
##  2 한국에너지공과대학교        9000000            900      2
##  3 연세대학교                  8949735            895.     3
##  4 추계예술대학교              8778884            878.     4
##  5 신한대학교                  8714105            871.     5
##  6 이화여자대학교              8689951            869.     6
##  7 을지대학교                  8492904            849.     7
##  8 한양대학교                  8486720            849.     8
##  9 한국항공대학교              8465834            847.     9
## 10 성균관대학교                8381038            838.    10
## # … with 378 more rows

4.3.4.1 ifelse()와 함께

ifelse() 함수는 조건에 따라 다른 값을 반환한다. ifelse() 함수는 mutate() 함수와 함께 사용하면 특정 조건에 따라 다른 값을 가지는 새로운 열을 만들 수 있다. ifelse() 함수는 다음과 같이 사용한다.

tuition %>% 
  select(대학명, 평균등록금.원.) %>% 
  mutate(백만원 = ifelse(평균등록금.원. >= 4000000, "400만원 이상", "400만원 미만"))
## # A tibble: 388 × 3
##    대학명             평균등록금.원. 백만원      
##    <chr>                       <int> <chr>       
##  1 강릉원주대학교            4261939 400만원 이상
##  2 강원대학교                4126501 400만원 이상
##  3 경남과학기술대학교        3800174 400만원 미만
##  4 경북대학교                4499592 400만원 이상
##  5 경상국립대학교            4083198 400만원 이상
##  6 경인교육대학교            3189000 400만원 미만
##  7 공주교육대학교            3424000 400만원 미만
##  8 공주대학교                3826015 400만원 미만
##  9 광주과학기술원            2060000 400만원 미만
## 10 광주교육대학교            3476500 400만원 미만
## # … with 378 more rows

ifelse() 함수의 첫 번째 인자는 조건이다. 조건은 평균등록금.원. >= 4000000으로, 평균 등록금이 400만원 이상인지 아닌지를 판단한다. 두 번째 인자는 조건이 참일 때 반환할 값이다. 세 번째 인자는 조건이 거짓일 때 반환할 값이다. ifelse() 함수는 조건이 참일 때와 거짓일 때 반환할 값의 자료형이 같아야 한다.

4.3.5 rename()

rename() 함수는 열 이름을 바꿀 때 사용한다. 평균등록금.원.이란 뭔가 마음 한구석 불편한 이름을 평균등록금_원으로 바꿔보자.

tuition %>% 
  select(대학명, 평균등록금.원.) %>% 
  rename(
    대학이름 = 대학명, 
    평균등록금_원 = 평균등록금.원.
  )
## # A tibble: 388 × 2
##    대학이름           평균등록금_원
##    <chr>                      <int>
##  1 강릉원주대학교           4261939
##  2 강원대학교               4126501
##  3 경남과학기술대학교       3800174
##  4 경북대학교               4499592
##  5 경상국립대학교           4083198
##  6 경인교육대학교           3189000
##  7 공주교육대학교           3424000
##  8 공주대학교               3826015
##  9 광주과학기술원           2060000
## 10 광주교육대학교           3476500
## # … with 378 more rows

4.3.6 group_by()

group_by() 함수는 데이터를 그룹으로 나눌 때 사용한다. group_by() 함수는 많은 경우 summarise() 함수와 함께 사용한다. summarise() 함수는 그룹별로 요약 통계량을 계산한다. 각 지역별로 평균 등록금을 계산해보자.

tuition %>% 
  group_by(지역별) %>% 
  summarise(평균등록금_원 = mean(평균등록금.원.)) %>% 
  arrange(desc(평균등록금_원))
## # A tibble: 17 × 2
##    지역별 평균등록금_원
##    <chr>          <dbl>
##  1 경기        6398933.
##  2 서울        5965247.
##  3 세종        5917194 
##  4 대전        5898748.
##  5 충남        5625469.
##  6 울산        5617955.
##  7 경북        5579286.
##  8 충북        5509948.
##  9 광주        5342153.
## 10 전북        5331158.
## 11 부산        5276806.
## 12 인천        5264909.
## 13 강원        5204863.
## 14 대구        5045801.
## 15 경남        4944646.
## 16 전남        4504543.
## 17 제주        4357420.

n() 함수를 활용하면 그룹별로 데이터의 개수를 계산할 수 있다. 각 지역별로 몇 개의 대학이 있는지 알아보자.

tuition %>% 
  group_by(지역별) %>% 
  summarise(대학수 = n()) %>% 
  arrange(desc(대학수))
## # A tibble: 17 × 2
##    지역별 대학수
##    <chr>   <int>
##  1 경기       65
##  2 서울       63
##  3 경북       37
##  4 부산       25
##  5 충남       25
##  6 경남       24
##  7 전남       21
##  8 전북       21
##  9 강원       19
## 10 광주       18
## 11 충북       18
## 12 대전       17
## 13 대구       14
## 14 인천        9
## 15 울산        5
## 16 제주        5
## 17 세종        2

4.4 데이터라는 것이 폭발한다. 합쳤을 때.

실제 데이터 분석을 할 때 한가지 데이터만으로 분석이 진행되는 경우는 거의 없다. 일반적으로 우리가 데이터에서 답을 찾아내기 위해서는 여러 데이터들을 합쳐야 하는 경우가 많다. 이렇게 관계가 있는 데이터들을 관계형 데이터라고 하는데 각 개별 데이터셋 끼리의 관계가 중요하기 때문이다. 이 관계는 주로 키(key) 값으로 연결된다. 키는 각 데이터프레임을 연결하는데 사용되는 변수(열)을 뜻한다. 키는 주민등록번호, 학번, 이름 등이 될 수 있다. 키를 사용하여 데이터프레임을 연결하는 것을 결합(join)이라고 한다. 결합은 dplyr 패키지의 inner_join(), left_join(), right_join(), full_join() 함수를 사용한다.

4.4.1 left_join()

left_join() 함수는 두 데이터프레임을 합칠 때 사용한다. left_join() 함수는 왼쪽 데이터프레임의 모든 행을 포함하는 데이터프레임을 만든다. 위에서 설명한 다른 join방법들이 있지만 별다른 이유가 없다면 left_join() 함수를 사용하자. 이는 왼쪽의 모든 관측값을 보존한다.

성적 <- tibble(
  이름 = c("철수", "영희", "영수"),
  토익 = c(900, 800, 700)
)

개인정보 <- tibble(
  이름 = c("영수", "영희"),
  고향 = c("부산", "정읍"),
  나이 = c(20, 21)
)

먼저 tibble() 함수를 사용해 새로운 데이터프레임을 2가지 만들었다. 성적 데이터프레임은 이름과 토익 점수를 담고 있다. 개인정보 데이터프레임은 이름, 고향, 나이를 담고 있다. left_join() 함수를 사용해 두 데이터프레임을 합쳐보자.

성적 %>% 
  left_join(개인정보, by = "이름")
## # A tibble: 3 × 4
##   이름   토익 고향   나이
##   <chr> <dbl> <chr> <dbl>
## 1 철수    900 <NA>     NA
## 2 영희    800 정읍     21
## 3 영수    700 부산     20

그 결과 성적 데이터프레임의 모든 행이 포함된 데이터프레임이 만들어졌다. left_join() 함수는 왼쪽 데이터셋의 값을 보존하기 때문에 왼쪽 데이터셋에 없는 키 값은 결측값으로 처리된다. 성적 데이터프레임에 없는 이름인 “철수”의 고향과 나이는 결측값으로 처리된다. 만약 개인정보를 먼저 입력했다면 모든 관측값이 보존되었을 것이다.

개인정보 %>% 
  left_join(성적, by = "이름")
## # A tibble: 2 × 4
##   이름  고향   나이  토익
##   <chr> <chr> <dbl> <dbl>
## 1 영수  부산     20   700
## 2 영희  정읍     21   800

by = 인자는 결합할 때 사용할 키를 지정한다. 만약 지정되지 않았다면 두 데이터프레임에 공통된 열 이름을 키로 사용한다. by = 인자에 키가 여러 개인 경우에는 c() 함수를 사용해 여러 개의 키를 지정한다. 만약 철수라는 동명이인이 여러명 존재한다고 해보자.

성적 <- tibble(
  이름 = c("철수", "영희", "영수", "철수", "철수"),
  학번 = c(1, 2, 3, 4, 5),
  토익 = c(900, 800, 700, 450, 100)
)

개인정보 <- tibble(
  이름 = c("영수", "영희", "철수", "철수", "철수"),
  학번 = c(3, 2, 1, 4, 5),
  고향 = c("부산", "정읍", "서울", "평양", "뉴욕"),
  나이 = c(20, 21, 22, 23, 24)
)

성적 %>% 
  left_join(개인정보, by = c("이름", "학번"))
## # A tibble: 5 × 5
##   이름   학번  토익 고향   나이
##   <chr> <dbl> <dbl> <chr> <dbl>
## 1 철수      1   900 서울     22
## 2 영희      2   800 정읍     21
## 3 영수      3   700 부산     20
## 4 철수      4   450 평양     23
## 5 철수      5   100 뉴욕     24

결과적으로 이름과 학번이 모두 일치한 데이터를 join한다.

만약 키가 되는 열 이름이 서로 다른 경우에는 어떻게 할까? 그러 때는 by = 인자에 왼쪽 데이터프레임의 키와 오른쪽 데이터프레임의 키를 지정할 수 있다.

성적 <- tibble(
  이름 = c("철수", "영희", "영수"),
  토익 = c(900, 800, 700)
)

개인정보 <- tibble(
  name = c("영수", "영희", "철수"),
  고향 = c("부산", "정읍", "서울"),
  나이 = c(20, 21, 22)
)

성적 %>% 
  left_join(개인정보, by = c("이름" = "name"))
## # A tibble: 3 × 4
##   이름   토익 고향   나이
##   <chr> <dbl> <chr> <dbl>
## 1 철수    900 서울     22
## 2 영희    800 정읍     21
## 3 영수    700 부산     20