데이터 정제

데이터 정제(data cleaning) 과정은 원데이터를 모형개발과 시각화를 위해 적절하게 처리하는 것으로 볼 수 있다. 이를 위해서 자료구조를 맞추는 것, 중복이 없도록 하는 것, 결측값을 해결하는 것 등 다양한 사항이 포함된다.

1 자료형 1

외부에서 다양한 데이터를 R로 가져오게 되면 통상 데이터프레임에 담기게 되고 이를 각 자료형에 맞도록 자료형을 맞추는 작업이 가장 먼저 수행되는 데이터 정제 작업 중 하나다.

데이터프레임과 벡터 자료형

예제로 제19대 대통령 선거 득표결과를 담고 있는 엑셀파일을 다운로드받아 이를 정제한다. 먼저 시도명과 나머지 데이터를 보게 되면 모두 문자열로 되어 있는 것을 확인할 수 있다.

library(tidyverse)
library(readxl)

dirty_dat <- read_excel("data/president.xlsx", sheet = "Sheet1", skip=4)

dirty_df <- dirty_dat %>% 
  select(1:8) %>% 
  set_names(c("시도명", "선거인수", "투표수", "문재인", "홍준표", "안철수", "유승민", "심상정")) %>% 
  slice(2:n())

dirty_df %>% 
  DT::datatable()

이를 시도명은 총 17개가 있어 범주를 한정하고 이를 요인형(factor) 자료형으로 맞춰야 하고, 선거인수, 투표수 등은 정수형 혹은 숫자형으로 변환을 시켜둬야 의미가 있는 데이터로 탈바꿈된다.

( dirty_df <- dirty_df %>% 
  mutate(시도명 = factor(시도명)) %>% 
  mutate(선거인수 = parse_number(선거인수),
         투표수 = str_remove_all(투표수, ",") %>% as.integer) )
# A tibble: 17 x 8
   시도명   선거인수  투표수 문재인     홍준표     안철수    유승민    심상정   
   <fct>       <dbl>   <int> <chr>      <chr>      <chr>     <chr>     <chr>    
 1 서울특별시…  8382999 6590646 "2,781,34… "1,365,28… "1,492,7… "476,973… "425,459…
 2 부산광역시…  2950224 2261633 "872,127\… "720,484\… "378,907… "162,480… "109,329…
 3 대구광역시…  2043276 1581347 "342,620\… "714,205\… "235,757… "198,459… "74,440\…
 4 인천광역시…  2409031 1820091 "747,090\… "379,191\… "428,888… "118,691… "129,925…
 5 광주광역시…  1166901  957321 "583,847\… "14,882\r… "287,222… "20,862\… "43,719\…
 6 대전광역시…  1220602  945897 "404,545\… "191,376\… "218,769… "59,820\… "63,669\…
 7 울산광역시…   941093  744960 "282,794\… "203,602\… "128,520… "60,289\… "62,187\…
 8 세종특별자치시…   189421  152801 "77,767\r… "23,211\r… "32,010\… "9,192\r… "9,353\r…
 9 경기도   10262309 7916009 "3,319,81… "1,637,34… "1,807,3… "540,023… "546,373…
10 강원도    1287173  955885 "324,768\… "284,909\… "206,840… "65,278\… "62,389\…
11 충청북도  1303688  975441 "374,806\… "255,502\… "211,454… "57,282\… "65,095\…
12 충청남도  1711912 1240204 "476,661\… "306,614\… "290,216… "68,521\… "83,868\…
13 전라북도  1525626 1205794 "778,747\… "40,231\r… "285,467… "30,802\… "59,296\…
14 전라남도  1572838 1238738 "737,921\… "30,221\r… "378,179… "25,819\… "49,509\…
15 경상북도  2249984 1711627 "369,726\… "827,237\… "253,905… "149,017… "88,080\…
16 경상남도  2744633 2135055 "779,731\… "790,491\… "284,272… "142,479… "113,051…
17 제주특별자치도…   518000  374459 "169,493\… "68,063\r… "77,861\… "22,784\… "31,716\…

앞선 방식은 데이터에 대해 친숙하고, 다년간 경험이 쌓은 경우 적절한데, 사실 이전에 is.*() 함수를 사용해서 자료형을 확인하는 것도 가능하다. 2016년 작성된 assertive 팩키지는 이를 확장하여 다양한 경우에 직관적인 구문을 제공하여 생산성을 높였다. 자료형을 변환시킬 경우 as.*() 방식으로 기존 자료형을 적절한 자료형으로 변환시킬 수 있다.

is.*() 판별식

  • is.logical()

  • is.factor()

  • is.numeric()

  • is.character()

  • is.Date()

  • is.data.frame()

  • is.list()

assertive 팩키지 판별식

  • assert_is_logical()

  • assert_is_factor()

  • assert_is_numeric()

  • assert_is_character()

  • assert_is_date()

  • assert_is_data.frame()

  • assert_is_list()

  • 자료형 변환

  • as.logical()

  • as.factor()

  • as.numeric()

  • as.character()

  • as.date()

  • as.data.frame()

  • as.list()

  • 2 연속형 데이터

    2.1 범위값 확인

    데이터 자료형이 맞는지 확인이 되었다면 다음으로 살펴볼 수 있는 중요한 주제는 범위가 맞는지 확인하는 것이다. 데이터의 자료형의 정합성은 맞으나 잘못된 값이 들어갈 수가 있다. 예를 들어,

    • 맛집 별점: 1-5
    • 몸무게와 신장: 양수
    • 날짜: 현재보다 미래는 될 수 없음
    • 점수: 0 - 100점
    • 자동차 연비: 리터당 0 - 1000 km

    기술통계량과 시각화를 통해서 와인 등급(평가)을 확인할 수 있다. 원점수는 0 - 10점 사이며 이를 벗어나는 점수는 잘못된 데이터로 파악할 수 있다.

    기술통계량

    wine_df <- read_csv("data/wineQualityReds.csv")
    
    wine_df %>% 
      summarise(min_quality = min(quality),
                max_quality = max(quality))
    # A tibble: 1 x 2
      min_quality max_quality
            <dbl>       <dbl>
    1           3           8

    시각화

    wine_df %>% 
      ggplot(aes(x=quality)) +
        geom_histogram(breaks = 1:10, fill = "skyblue") +
        scale_x_continuous(breaks = 1:10)

    2.1.1 assertive 함수

    assertive 팩키지 assert_all_are_in_closed_range() 함수를 사용해서 하한과 상한을 특정하여 범위를 넘어서는 데이터를 잡아내서 적절한 조치를 취한다.

    library(assertive)
    
    assert_all_are_in_closed_range(wine_df$quality, lower = 0, upper = 10)

    2.1.2 오류 수정

    assertive 팩키지 assert_all_are_in_closed_range() 함수를 통해 오류사항을 탐지한 후에 적절한 조치를 취해서 이를 고쳐야만 된다.

    • 제거: 관측점이 많지 않는 경우
    • 결측값(NA) 처리: 결측값으로 보정
    • 범위 상한 혹은 하한으로 처리
    • 범위를 벗어난 데이터를 적절한 결측값 처리 방법이나 로직으로 채워 놓음.

    quality 와인품질 점수가 3-8점이나 quality_range 변수를 균등분포 [0,13] 사이에서 난수를 뽑아 와인품질로 넣어 고의적으로 범위를 벗어난 데이터로 만든다.

     set.seed(777)
    
    wine_df <- wine_df %>% 
      mutate(quality_range = runif(n(),0,13) %>% as.integer)

    범위를 넘어간 값을 NA를 적절한 함수나 방법(평균 등) 결측값으로 치환하거나, 제거하거나, 최대 혹은 최소값으로 치환시키는 것도 가능하다.

    결측값 치환

    wine_df %>% 
      mutate(quality_range = ifelse(quality_range > 10, NA, quality_range)) %>% 
      mutate(quality_range = ifelse(is.na(quality_range), mean(quality_range, na.rm = TRUE), quality_range)) %>%
      ggplot(aes(x=quality_range)) +
        geom_histogram(bins = 10) +
        scale_x_continuous(breaks = 0:10)



    제거

    wine_df %>% 
      filter(quality_range <= 10) %>% 
      ggplot(aes(x=quality_range)) +
        geom_histogram(bins = 10) +
        scale_x_continuous(breaks = 0:10)



    치환

    wine_df %>% 
      mutate(quality_range = ifelse(quality_range >= 10, 10, quality_range)) %>% 
      ggplot(aes(x=quality_range)) +
        geom_histogram(bins = 10) +
        scale_x_continuous(breaks = 0:10)

    3 중복

    3.1 개인정보 데이터

    다양한 이유로 인해 중복된 데이터가 생성될 수 있다. 이를 확인하기 위해서 개인정보 생성기를 만들어보자. generator: Personally Identifiable Information (PII)를 사용해서 중복 데이터를 생성한다. 5명을 생성하고 의도적으로 3명만 중복을 시킨다.

    library(generator)
    
    n <- 5
    set.seed(1)
    pii_df <- data.frame(name = r_full_names(n), 
                 dob = r_date_of_births(n), 
                 email = r_email_addresses(n), 
                 phone = r_phone_numbers(n), 
                 stringsAsFactors = FALSE)
    
    pii_df <- bind_rows(pii_df, pii_df[1:3,]) %>% 
      arrange(name)
    
    pii_df
                     name        dob              email      phone
    1       Ambrose Doyle 1959-11-12 qdgiawrxuk@met.rtc 6933852946
    2       Ambrose Doyle 1959-11-12 qdgiawrxuk@met.rtc 6933852946
    3     Augustine Runte 1993-07-02         bq@wty.lkt 5693965261
    4     Eldridge Daniel 1986-04-24 vqsmlyaj@trlzj.gbc 6894921693
    5     Eldridge Daniel 1986-04-24 vqsmlyaj@trlzj.gbc 6894921693
    6 Gudrun Pfannerstill 2019-04-19    rkvg@lipfkq.cvi 6845265927
    7 Gudrun Pfannerstill 2019-04-19    rkvg@lipfkq.cvi 6845265927
    8     Jennie Homenick 1945-09-16 ilvtzqwxm@kise.sdf 6511676549

    3.2 중복 탐지: duplicated

    duplicated() 함수를 사용해서 중복여부를 판정할 수 있다.

    pii_df %>% 
      sample_frac(size = 1, replace=FALSE) %>% 
      mutate(check_dup = duplicated(pii_df)) %>% 
      count(check_dup)
    # A tibble: 2 x 2
      check_dup     n
      <lgl>     <int>
    1 FALSE         5
    2 TRUE          3

    filter() 함수와 결합시켜 중복된 관측점을 잡아낼 수 있다.

    pii_df %>% 
      filter(duplicated(pii_df))
                     name        dob              email      phone
    1       Ambrose Doyle 1959-11-12 qdgiawrxuk@met.rtc 6933852946
    2     Eldridge Daniel 1986-04-24 vqsmlyaj@trlzj.gbc 6894921693
    3 Gudrun Pfannerstill 2019-04-19    rkvg@lipfkq.cvi 6845265927

    3.3 중복 제거: distinct

    distinct() 함수를 사용해서 중복된 것을 제거하고 유일무이한 관측점만 뽑아 후속작업을 추진할 수 있다. 혹은 filter() 함수에 !duplicated()함수를 결합시켜 동일한 효과를 낼 수 있다.

    # pii_df %>% 
    #   filter(!duplicated(pii_df))
    
    pii_df %>% 
      distinct()
                     name        dob              email      phone
    1       Ambrose Doyle 1959-11-12 qdgiawrxuk@met.rtc 6933852946
    2     Augustine Runte 1993-07-02         bq@wty.lkt 5693965261
    3     Eldridge Daniel 1986-04-24 vqsmlyaj@trlzj.gbc 6894921693
    4 Gudrun Pfannerstill 2019-04-19    rkvg@lipfkq.cvi 6845265927
    5     Jennie Homenick 1945-09-16 ilvtzqwxm@kise.sdf 6511676549

    3.4 부분 중복 데이터

    이름(name)과 생년월일(dob)는 동일하나 주소로 위경도가 다른 데이터가 있는 경우 부분 중복이 존재하는 상황이다.

    partial_pii_df <- data.frame(name = r_full_names(n), 
                 dob = r_date_of_births(n), 
                 lat = r_latitudes(n), 
                 lon = r_longitudes(n), 
                 stringsAsFactors = FALSE)
    
    partial_pii_df <- bind_rows(partial_pii_df, partial_pii_df[1:3, ]) %>% 
      mutate(lat = r_latitudes(8),
             lon = r_longitudes(8)) %>% 
      arrange(name)
    
    partial_pii_df
                   name        dob        lat         lon
    1      Bud Reichert 1947-01-27  79.870467 -140.270182
    2      Bud Reichert 1947-01-27  76.914376 -101.287697
    3    Elton Hartmann 2002-12-22  40.409801   32.606337
    4    Elton Hartmann 2002-12-22 -71.842837  -83.697045
    5    Haywood Harvey 1945-10-06   8.576386  122.582532
    6    Haywood Harvey 1945-10-06 -39.018150    6.046861
    7       Otto Murphy 2007-09-11  38.113896  -65.533074
    8 Priscilla Spencer 1977-06-28 -19.997082  101.826481

    3.5 부분 중복 탐지

    부분중복을 탐지하는 방법으로 count() 함수를 사용한다. 즉, name, dob 기준으로 빈도수가 2이상인 경우 주소지(위경도 기준)가 두곳이상으로 판단하여 부분 중복 관측점을 뽑아낸다.

    partial_dup_df <- partial_pii_df %>% 
      count(name, dob) %>% 
      filter(n >= 2)
    
    partial_dup_df
    # A tibble: 3 x 3
      name           dob            n
      <chr>          <date>     <int>
    1 Bud Reichert   1947-01-27     2
    2 Elton Hartmann 2002-12-22     2
    3 Haywood Harvey 1945-10-06     2

    3.6 부분 중복 제거

    distinct() 함수를 사용해서 중복되는 관측점을 제거한다. 즉, 가장 첫번째 주소지만 가져오는 방식으로 name, dob 중복을 해소한다.

    partial_pii_df %>% 
      distinct(name, dob, .keep_all = TRUE)
                   name        dob        lat        lon
    1      Bud Reichert 1947-01-27  79.870467 -140.27018
    2    Elton Hartmann 2002-12-22  40.409801   32.60634
    3    Haywood Harvey 1945-10-06   8.576386  122.58253
    4       Otto Murphy 2007-09-11  38.113896  -65.53307
    5 Priscilla Spencer 1977-06-28 -19.997082  101.82648

    3.7 부분 중복 축약

    두번째 방식으로 group_by() + summarize() 패턴을 사용해서 다양한 요약통계량(min, max, mean 등)을 사용해서 중복을 해소한다.

    partial_pii_df %>% 
      group_by(name, dob) %>% 
      summarise(lat = mean(lat), lon=mean(lon))
    # A tibble: 5 x 4
    # Groups:   name [5]
      name              dob          lat    lon
      <chr>             <date>     <dbl>  <dbl>
    1 Bud Reichert      1947-01-27  78.4 -121. 
    2 Elton Hartmann    2002-12-22 -15.7  -25.5
    3 Haywood Harvey    1945-10-06 -15.2   64.3
    4 Otto Murphy       2007-09-11  38.1  -65.5
    5 Priscilla Spencer 1977-06-28 -20.0  102. 

    조금 고급진 … 확장이 가능한 패턴은 group_by() + summarize() 패턴 대신 group_by() + mutate() 패턴을 사용하고 나서 distinct().keep_name=TRUE와 조합하여 사용하는 것이다.

    partial_pii_df %>% 
      group_by(name, dob) %>% 
      mutate(mean_lat = mean(lat), mean_lon=mean(lon)) %>% 
      distinct(name, dob, .keep_all = TRUE) %>% 
      select(-lat, -lon)
    # A tibble: 5 x 4
    # Groups:   name, dob [5]
      name              dob        mean_lat mean_lon
      <chr>             <date>        <dbl>    <dbl>
    1 Bud Reichert      1947-01-27     78.4   -121. 
    2 Elton Hartmann    2002-12-22    -15.7    -25.5
    3 Haywood Harvey    1945-10-06    -15.2     64.3
    4 Otto Murphy       2007-09-11     38.1    -65.5
    5 Priscilla Spencer 1977-06-28    -20.0    102. 

    4 텍스트

    범주형 데이터는 정수형 데이터와 텍스트 문자 데이터의 모습을 모두 갖추고 있다. 문자 텍스트 데이터는 정규표현식과 밀접한 관련이 있다.

    5 범주형 데이터

    5.1 데이터 정제

    연속형 변수의 범위 확인과 마찬가지로 범주형 변수의 범위도 확인이 필요하다. 먼저 대통령 선거 득표 데이터를 바탕으로 시도명과 후보가 범주형 자료형임을 파악할 수 있다.

    clean_df <- dirty_df %>% 
      select(-선거인수, -투표수) %>% 
      gather(후보, 득표수, -시도명) %>% 
      separate(득표수, into=c("득표수", "득표율"), sep="\r\n") %>% 
      mutate(득표수 = parse_number(득표수),
            득표율 = parse_number(득표율))
    
    clean_df 
    # A tibble: 85 x 4
       시도명         후보    득표수 득표율
       <fct>          <chr>    <dbl>  <dbl>
     1 서울특별시     문재인 2781345   42.3
     2 부산광역시     문재인  872127   38.7
     3 대구광역시     문재인  342620   21.8
     4 인천광역시     문재인  747090   41.2
     5 광주광역시     문재인  583847   61.1
     6 대전광역시     문재인  404545   42.9
     7 울산광역시     문재인  282794   38.1
     8 세종특별자치시 문재인   77767   51.1
     9 경기도         문재인 3319812   42.1
    10 강원도         문재인  324768   34.2
    # … with 75 more rows

    범주형 변수에 대한 자세한 정보는 데이터 과학: 요인(Factor) - 범주형 자료형을 참고한다. 중요한 것은 범주로 지정되지 않는 경우 제대로 처리가 되지 않는다는 것이다.

    levels() 함수를 범주형 변수에 적용하여 범주에 속한 회원여부를 확인한다.

    clean_df %>% 
      select(시도명) %>% 
      pull(시도명) %>% 
      levels()
     [1] "강원도"         "경기도"         "경상남도"       "경상북도"      
     [5] "광주광역시"     "대구광역시"     "대전광역시"     "부산광역시"    
     [9] "서울특별시"     "세종특별자치시" "울산광역시"     "인천광역시"    
    [13] "전라남도"       "전라북도"       "제주특별자치도" "충청남도"      
    [17] "충청북도"      

    5.2 범주여부 확인

    수도권 여부를 판정하여 해당 데이터만 추려낸다. 이를 위해서 또 다른 데이터프레임을 만들어서 이를 활용하여 수도권과 비수도권을 구분한다. semi_join() 함수를 사용해서 수도권 변수에 3개시도만 매칭하여 추려낸다.

    수도권 <- tribble(~"시도명",
                   "서울특별시",
                   "경기도",
                   "인천광역시") %>% 
      mutate(시도명 = factor(시도명))
    
    clean_df %>% 
      semi_join(수도권) %>% 
      select(-득표율) %>% 
      spread(후보, 득표수)
    # A tibble: 3 x 6
      시도명      문재인 심상정  안철수 유승민  홍준표
      <fct>        <dbl>  <dbl>   <dbl>  <dbl>   <dbl>
    1 경기도     3319812 546373 1807308 540023 1637345
    2 서울특별시 2781345 425459 1492767 476973 1365285
    3 인천광역시  747090 129925  428888 118691  379191

    비수도권의 경우 anti_join()을 사용하면 semi_join()과 반대효과를 얻을 수 있다.

    clean_df %>% 
      anti_join(수도권) %>% 
      select(-득표율) %>% 
      spread(후보, 득표수)
    # A tibble: 14 x 6
       시도명         문재인 심상정 안철수 유승민 홍준표
       <fct>           <dbl>  <dbl>  <dbl>  <dbl>  <dbl>
     1 강원도         324768  62389 206840  65278 284909
     2 경상남도       779731 113051 284272 142479 790491
     3 경상북도       369726  88080 253905 149017 827237
     4 광주광역시     583847  43719 287222  20862  14882
     5 대구광역시     342620  74440 235757 198459 714205
     6 대전광역시     404545  63669 218769  59820 191376
     7 부산광역시     872127 109329 378907 162480 720484
     8 세종특별자치시  77767   9353  32010   9192  23211
     9 울산광역시     282794  62187 128520  60289 203602
    10 전라남도       737921  49509 378179  25819  30221
    11 전라북도       778747  59296 285467  30802  40231
    12 제주특별자치도 169493  31716  77861  22784  68063
    13 충청남도       476661  83868 290216  68521 306614
    14 충청북도       374806  65095 211454  57282 255502

    5.3 적절한 범주

    지난 대통령선거는 다소 논란은 있지만, 문재인, 홍준표, 안철수 3명의 후보가 자웅을 겨뤘다고 볼 수 있다. 따라서 다른 후보는 기타 후보로 뭉뚱그려 처리하는 것이 간결한 정보를 전달한다는 측면에서 의미가 크다.

    clean_df %>% 
      group_by(후보) %>% 
      summarise(득표수 = sum(득표수)) %>%
      mutate(득표율 = 득표수/sum(득표수)) %>% 
      ungroup() %>%  
      arrange(desc(득표수)) %>% 
      mutate(누적득표수 = cumsum(득표수)) %>% 
      mutate(누적비율 = 누적득표수 / sum(득표수))
    # A tibble: 5 x 5
      후보     득표수 득표율 누적득표수 누적비율
      <chr>     <dbl>  <dbl>      <dbl>    <dbl>
    1 문재인 13423800 0.413    13423800    0.413
    2 홍준표  7852849 0.242    21276649    0.655
    3 안철수  6998342 0.215    28274991    0.870
    4 유승민  2208771 0.0680   30483762    0.938
    5 심상정  2017458 0.0621   32501220    1    

    이를 위해서 fct_collapse() 함수를 사용해서 후보의 범주를 재범주화하는 것이 큰 도움이 된다. 문재인, 홍준표, 안철수 후보를 제외한 나머지 후보를 “기타”로 묶어 범주를 재조정한다.

    clean_df %>% 
      mutate(후보구분 = fct_collapse(후보, 기타 = c("유승민", "심상정")))%>% 
      group_by(후보구분) %>% 
      summarise(득표수 = sum(득표수)) %>%
      mutate(득표율 = 득표수/sum(득표수)) %>% 
      ungroup() %>%  
      arrange(desc(득표수)) %>% 
      mutate(누적득표수 = cumsum(득표수)) %>% 
      mutate(누적비율 = 누적득표수 / sum(득표수))
    # A tibble: 4 x 5
      후보구분   득표수 득표율 누적득표수 누적비율
      <fct>       <dbl>  <dbl>      <dbl>    <dbl>
    1 문재인   13423800  0.413   13423800    0.413
    2 홍준표    7852849  0.242   21276649    0.655
    3 안철수    6998342  0.215   28274991    0.870
    4 기타      4226229  0.130   32501220    1    

    6 균일성

    데이터의 균일성을 맞추는 것은 무척이나 중요하다. 대표적으로 다양한 국가로부터 실업율, 경제성장율 등 자료를 가져올 경우 ISO-8601을 지키지 않는 경우 날짜가 각 국가별로 다른 형태로 준비되어 이를 통일시켜 균일성을 확보해야만 된다. 날짜가 미국, 유럽, 한국과 같이 다양한 형태로 뒤섞여 있는 경우 이를 lubridate 팩키지 parse_date_time() 함수에 orders=를 지정하여 날짜형 자료형의 균일성을 확보할 수 있다.

    library(lubridate)
    
    date_df <- tribble(~"국가", ~"날짜", ~"경제성장율",
            "미국", "March 15, 2019", 3.0,
            "유럽", "15/03/2019", 1.5,
            "한국", "2019년 03월 15일", 3.1)
    
    date_df %>% 
      mutate(날짜 = case_when(str_detect(국가, "미국") ~ parse_date(날짜, format="%M %d, %Y"),
                              TRUE ~ parse_date(날짜)))
    # A tibble: 3 x 3
      국가  날짜       경제성장율
      <chr> <date>          <dbl>
    1 미국  NA                3  
    2 유럽  NA                1.5
    3 한국  NA                3.1
    date_df %>% 
      mutate(날짜 = str_replace(날짜, "년|월|일", "-")) %>% 
      mutate(날짜 = parse_date_time(날짜, orders = c("bdY", "dmY", "ymd")))
    # A tibble: 3 x 3
      국가  날짜                경제성장율
      <chr> <dttm>                   <dbl>
    1 미국  2019-03-15 00:00:00        3  
    2 유럽  2019-03-15 00:00:00        1.5
    3 한국  2019-03-15 00:00:00        3.1

    7 상호교차검증

    상호 교차검증의 필요성을 보여주는 대표적인 사례가 스코틀랜드 독립에 대한 설문조사 내용이다. 찬성과 반대를 합하였는데 이것이 100%를 넘고 있다.

    webshot2::webshot("https://www.dailymail.co.uk/news/article-2761778/Something-doesn-t-add-CNN-Reports-110-turnout-Scottish-independence-vote.html", selector="#i-3e6f6d8c2620a5dc", "fig/scotland.png")

    득표율의 총합은 100이 되어야 한다. clean_df는 대선 후보 5명만 뽑은 것이라 일부 누락이 발생되어 100이 되지 않는 것이 정상이다.

    clean_df %>% 
      group_by(시도명) %>% 
      summarise(득표율 = sum(득표율)) %>% 
      arrange(득표율) %>% 
      mutate(차이 = 100 - 득표율)
    # A tibble: 17 x 3
       시도명         득표율  차이
       <fct>           <dbl> <dbl>
     1 전라남도         99.1 0.9  
     2 경상북도         99.2 0.81 
     3 강원도           99.3 0.7  
     4 제주특별자치도   99.3 0.7  
     5 충청남도         99.3 0.690
     6 충청북도         99.3 0.690
     7 경상남도         99.4 0.610
     8 대구광역시       99.4 0.59 
     9 전라북도         99.4 0.570
    10 울산광역시       99.4 0.56 
    11 인천광역시       99.5 0.540
    12 경기도           99.5 0.5  
    13 세종특별자치시   99.5 0.49 
    14 광주광역시       99.5 0.48 
    15 대전광역시       99.5 0.470
    16 부산광역시       99.6 0.43 
    17 서울특별시       99.6 0.43 

    8 결측 데이터

    데이터에 결측값(missing value)이 발생되면 이에 대한 다각도 점검과 결측값을 해소하는 방안에 대해서 심도있게 검토해야만 된다.