1 문제 개요

PDF 파일에서 표를 추출하는 문제를 가정해보자. KPMG FY2018 투명성 보고서에서 20 페이지에 위치한 “소속공인회계사의 경력 현황” 표를 추출하는 경우가 이에 해당된다.

PDF 파일의 특정 영역에서 표를 가져오는 사례

2 PDF 파일에서 영역 특정

가장 먼저 해야되는 문제는 PDF 파일에서 표가 들어 있는 영역을 특정하는 것이다.

PDF 파일에서 표추출

3 표 추출 방법

표를 추출하여 데이터프레임으로 만드는 방법은 tabulizer 팩키지에서 표를 추출하는 방식과 pdftools 팩키지에서 텍스트를 추출하여 표를 복원하는 방식이 있다.

3.1 tabulizer 표추출

tabulizer 팩키지를 사용하여 locate_area() 함수로 추출할 표의 위치를 특정한다. 그리고 나서, extract_tables() 함수로 표를 데이터프레임으로 추출하고 후처리 작업을 하여 원하는 결과를 만들어낸다.

library(tidyverse)
library(tabulizer)

# locate_areas(file = "data/transparency-report-2018.pdf", pages = 21)
# [[1]]
#      top     left   bottom    right 
# 630.8387  81.0575 703.5307 539.9231 

kpmg_table <- extract_tables("data/transparency-report-2018.pdf", 
                             pages = 21,
                             encoding = "UTF-8",
                             output = "data.frame",
                             method = "stream",
                             guess = TRUE,
                             area = list(c(630.8387,  81.0575, 703.5307, 539.9231)))

## 표 후처리 작업
var_name <- c("사업부문", "1년미만", "1년이상~3년미만", "3년이상~5년미만", "5년이상~10년미만", "10년이상~15년미만", "15년이상", "합계")

kpmg_table %>% 
  .[[1]] %>% 
  janitor::clean_names()  %>% 
  set_names(var_name) %>% 
  slice(3) %>% 
  pivot_longer(`사업부문`:`합계`, names_to ="구분", values_to="인원") %>% 
  filter(구분 != "사업부문")
# A tibble: 7 x 2
  구분              인원 
  <chr>             <chr>
1 1년미만           290  
2 1년이상~3년미만   428  
3 3년이상~5년미만   248  
4 5년이상~10년미만  326  
5 10년이상~15년미만 173  
6 15년이상          115  
7 합계              1,580

3.2 표위치 특정 - pdf_data()

KPMG FY2018 투명성 보고서에서 20 페이지에 위치한 “소속공인회계사의 경력 현황” 표를 특정하기 위해서 먼저 경로명을 입력한다. pdf_data() 함수를 사용해서 해당 표에 해당되는 모든 텍스트를 가져온다.

library(pdftools)
library(tidyverse)

transprency_pdf <- "data/transparency-report-2018.pdf"

# pdf_info(transprency_pdf)
# pdf_fonts(transprency_pdf)

(transparency_tbl <- pdf_data(transprency_pdf)[[21]] %>% 
  filter(x > 90, x< 502, y>635, y<676 ))
# A tibble: 19 x 6
   width height     x     y space text     
   <int>  <int> <int> <int> <lgl> <chr>    
 1    40     10    92   645 FALSE 사업부문 
 2    35     10   150   645 FALSE 1년미만  
 3    43     10   204   639 FALSE 1년이상~ 
 4    35     10   211   651 FALSE 3년미만  
 5    43     10   263   639 FALSE 3년이상~ 
 6    35     10   269   651 FALSE 5년미만  
 7    43     10   322   639 FALSE 5년이상~ 
 8    41     10   325   651 FALSE 10년미만 
 9    48     10   378   639 FALSE 10년이상~
10    41     10   384   651 FALSE 15년미만 
11    41     10   440   645 FALSE 15년이상 
12    23     11   100   675 FALSE 인원     
13    20     11   157   675 FALSE 290      
14    20     11   216   675 FALSE 428      
15    20     11   275   675 FALSE 248      
16    20     11   333   675 FALSE 326      
17    20     11   392   675 FALSE 173      
18    20     11   451   675 FALSE 115      
19    31     11   496   675 FALSE 1,580    

4 위치 DF → 표 DF

앞서 추출한 위치를 포함한 데이터프레임을 표 데이터프레임으로 변환시킨다.

(transparency_tmp_tbl <- transparency_tbl %>% 
  mutate(clean_text = case_when(str_detect(text, "사업부문") ~ "사업부문",
                                str_detect(text, "1년미만") ~ "1년미만",
                                str_detect(text, "1년이상") ~ "1년이상~3년미만",
                                str_detect(text, "3년이상") ~ "3년이상~5년미만",
                                str_detect(text, "\\b5년이상\\b") ~ "5년이상~10년미만",
                                str_detect(text, "10년이상") ~ "10년이상~15년미만",
                                str_detect(text, "\\b15년이상\\b") ~ "15년이상",
                                str_detect(text, "인원") ~"인원",
                                str_detect(text, "\\d{3}") ~ text,
                                TRUE ~ "기타")) %>% 
  filter(!str_detect(clean_text, "기타|,")) %>% 
  mutate(fieldname_value = case_when(str_detect(clean_text, "\\d{3}|\\b인원\\b") ~ "value",
                                     TRUE ~ "field")) %>% 
  select(fieldname_value, clean_text))
# A tibble: 14 x 2
   fieldname_value clean_text       
   <chr>           <chr>            
 1 field           사업부문         
 2 field           1년미만          
 3 field           1년이상~3년미만  
 4 field           3년이상~5년미만  
 5 field           5년이상~10년미만 
 6 field           10년이상~15년미만
 7 field           15년이상         
 8 value           인원             
 9 value           290              
10 value           428              
11 value           248              
12 value           326              
13 value           173              
14 value           115              
## 필드명과 값 결합
transparency_field_tbl <- transparency_tmp_tbl %>% 
  filter(fieldname_value == "field",
         !str_detect(clean_text, "사업부문")) %>% 
  select(사업구분 = clean_text)

transparency_value_tbl <- transparency_tmp_tbl %>% 
  filter(fieldname_value == "value",
        !str_detect(clean_text, "인원")) %>% 
  select(인원 = clean_text)
  
transparency_df <- 
  bind_cols(transparency_field_tbl, transparency_value_tbl) 

transparency_df %>% 
  DT::datatable()

5 분석가능한 데이터프레임

다음 단계로 시각화 및 각종 통계분석을 위해서 자료형을 적절히 맞춘다. 즉, 사업구분은 범주형으로 범주수준도 유의미하게 맞추고, 인원수도 숫자로 인식시킨다. 이를 바탕으로 ggplot 으로 시각화 한다.

transparency_df %>% 
  mutate(인원 = parse_number(인원)) %>% 
  mutate(사업구분 = factor(사업구분, levels = c("1년미만", "1년이상~3년미만", "3년이상~5년미만", 
                                                 "5년이상~10년미만", "10년이상~15년미만", "15년이상")))  %>% 
  ggplot(aes(x=fct_rev(사업구분), y=인원)) +
    geom_col(fill="skyblue") +
    coord_flip() +
    theme_bw(base_family = "AppleGothic") +
    labs(x="", y="", title="KPMG 공인회계사 경력현황")

6 데이터프레임 정답비교

Paul Murrell의 compare, compareDF, 자바스크립트 기반 daff 팩키지가 데이터프레임 비교 전용으로 알려져 있고, 그외에도 dplyr::setdiff 혹은 sqldf 팩키지 SQL, merge 함수를 사용하는 다양한 사례가 있다.

library(compare)

## PDF 파일에서 추출한 표
transparency_df <- transparency_df %>% 
  mutate(인원 = parse_number(인원))

## 정답표
solution_df <- tribble(~"사업구분", ~"인원",
"1년미만", 290,
"1년이상~3년미만", 428,
"3년이상~5년미만", 248,
"5년이상~10년미만", 326,
"10년이상~15년미만", 173,
"15년이상", 115)

## 정답표와 추출한 PDF 비교

# check_answer_df <- compare_df(transparency_df, solution_df, c("사업구분", "인원"))
compare(transparency_df, solution_df)
TRUE
 

데이터 과학자 이광춘 저작

kwangchun.lee.7@gmail.com