Post List

2019년 11월 15일 금요일

dtplyr: dplyr의 편리함과 data.table의 속도를 그대로!



R 내에서 데이터 처리시 가장 많이 사용되는 패키지는 dplyr 입니다. 개인적으로 dplyr을 쓰지 않는 다는 것은 R의 1%도 활용하지 못한다는 생각이 들 정도입니다. 그러나 데이터 양이 늘어날수록 속도가 느려지는 것은 당연합니다.

물론 data.table 패키지라는 해결책도 있습니다. R 뿐만 아니라 다른 언어들과 비교해도 속도에서 만큼은 깡패급으로 빠릅니다. 그러나 사용하기에 문법이 너무 복잡해 접근이 쉽지 않습니다.

(아무리 외워도 외워지지 않는 그놈의 DT[i, j, by].....)


이런 문제를 해결하기 위해 Rstudio에서 새로운 해법을 내놓았습니다. dplyr 문법을 입력하면 data.table 명령어로 래핑하여 데이터를 처리하는, 즉 dplyr의 편리함과 data.table의 속도를 둘 다 잡은 dtplyr 패키지를 선보였습니다.


두말할 필요없이 속도 비교를 해보도록 합시다.

library(nycflights13)
library(data.table)
library(dtplyr)
library(dplyr, warn.conflicts = FALSE)
library(microbenchmark)
library(ggplot2)

먼저 필요한 패키지들을 불러옵니다. dplyr의 경우 warn.conflicts() 인자를 FALSE로 두어 충돌 메세지를 끄도록 합니다.
CRAN 버젼 dtplyr 설치시 에러가 날 수 있습니다. 이는 연결 패키지인 data.table이 최근에 업데이트 되어서이며, data.table을 제거 후 인스톨 한후 dtplyr을 설치하면 잘 설치 됩니다.
# load data
df = flights
dim(df)
## [1] 336776     19
head(df)
## # A tibble: 6 x 19
##    year month   day dep_time sched_dep_time dep_delay arr_time
##                            
## 1  2013     1     1      517            515         2      830
## 2  2013     1     1      533            529         4      850
## 3  2013     1     1      542            540         2      923
## 4  2013     1     1      544            545        -1     1004
## 5  2013     1     1      554            600        -6      812
## 6  2013     1     1      554            558        -4      740
## # ... with 12 more variables: sched_arr_time , arr_delay ,
## #   carrier , flight , tailnum , origin , dest ,
## #   air_time , distance , hour , minute ,
## #   time_hour 
필요한 flight 데이터를 불러오도록 합니다. 총 336776개의 행으로 구성되어 있습니다.

df_dt = data.table(df)
df_tb = as_tibble(df)
df_lz = lazy_dt(df)
데이터 테이블을 각 형식에 맞게 바꿔주도록 합니다. data.table() 함수를 이용해 데이터테이블 형태로, as_tibble() 함수를 이용해 티블 형태로, 마지막으로 dtplyr 패키지의 lazy_dt() 함수를 이용해 dtplyr_step_first 형태로 바꾸어 줍니다. 해당 형식을 통해 dplyr 명령어를 data.table 명령어로 래핑하여 데이터를 처리하게 됩니다.

summary(df_lz)
##               Length Class      Mode       
## parent        19     data.table list       
## vars          19     -none-     character  
## groups         0     -none-     character  
## implicit_copy  1     -none-     logical    
## needs_copy     1     -none-     logical    
## env            5     -none-     environment
## name           1     -none-     name
summary()를 통해 확인해보면 data.table 클래스에 굉장히 독특한 형태로 생겼습니다.
이제 본격적으로 속도를 비교해보도록 하겠습니다. 먼저 필터에 대한 속도 비교입니다.

dtply을 이용할 경우 lazy_dt()를 통해 변경된 데이터에 기존 dplyr 명령어를 그대로 사용해서 데이터처리를 하며, as_tibble() 을 통해 티블 형태로 변경해주길 권장합니다. (그렇지 않으면 용량이 매우 큰 dtplyr_step_first 형태로 남아있게 됩니다.)
results = microbenchmark(
  `data.table` = df_dt[origin == 'JFK' & carrier == 'AA'] ,
  `dplyr` = df_tb %>% filter(origin == 'JFK' & carrier == 'AA'),
  `dtplyr` = df_lz %>% filter(origin == 'JFK' & carrier == 'AA') %>% as_tibble(),
  times = 100
)

results
## Unit: milliseconds
##        expr    min      lq      mean   median       uq     max neval cld
##  data.table 5.2220 6.92075  7.842533  7.49525  8.31910 19.4979   100 a  
##       dplyr 8.1038 9.33090 11.130208 10.14395 11.50605 27.7445   100   c
##      dtplyr 6.5998 8.35225  8.902763  8.83630  9.42860 11.7533   100  b
autoplot(results) +
  aes(fill = expr) +
  theme_bw() +
  labs(title = "Filter")

data.table이 압도적으로 빠르며, 그뒤가 dtplyr, dplyr 순입니다. 물론 data.table과 dtplyr은 간혹 속도가 매우 오래 걸리는 경우가 있어 mean 값이 증가하는 경우도 있습니다.
그러나 data.table와 dtplyr 진정한 위력은 매우 복잡한 데이터를 처리하는데 있습니다. 이번에는 그룹화, 통계값 계산, 필터링 이라는 복잡한 데이터처리를 해보도록 하겠습니다.

results2 = microbenchmark(

  `data.table` = df_dt[, .(mean_delay = mean(dep_delay, na.rm = TRUE)),
        by = c('year', 'month', 'day', 'carrier', 'origin')][mean_delay >= 10],

  `dplyr` = df_tb %>%
    group_by(year, month, day, carrier, origin) %>%
    summarize(mean_delay = mean(dep_delay, na.rm = TRUE)) %>%
    ungroup() %>%
    filter(mean_delay >= 10),

  `dtplyr` = df_lz %>%
    group_by(year, month, day, carrier, origin) %>%
    summarize(mean_delay = mean(dep_delay, na.rm = TRUE)) %>%
    ungroup() %>%
    filter(mean_delay >= 10) %>%
    as_tibble(),

  times = 100
)

results2
## Unit: milliseconds
##        expr     min       lq     mean   median       uq      max neval cld
##  data.table 14.1369 16.05185 17.85918 16.82690 18.81835  25.7218   100 a  
##       dplyr 71.1181 81.33950 91.00665 90.43740 99.06195 130.1543   100   c
##      dtplyr 14.9752 19.12145 22.18545 20.24325 21.60995 148.0419   100  b
autoplot(results2) +
  aes(fill = expr) +
  theme_bw() +
  labs(title = "group_by, mean, filter")


data.table과 dtply이 dplyr에 비해 압도적인 속도 차이를 보입니다. 이러한 차이는 데이터 크기가 늘어날수록, 그리고 데이터 처리 구조가 복잡해 질수록 늘어나게 됩니다.
이처럼 dtplyr은 기존 dplyr 문법을 활용하여 data.table과 거의 비슷한 성능을 내는 보물같은 패키지입니다.

2019년 9월 17일 화요일

RSelenium (셀레니움)을 이용한 동적 웹페이지 크롤링하기





'R을 이용한 퀀트 투자 포트폴리오 만들기'가 출간 되었습니다.
많은 관심 부탁드리겠습니다.

↓↓ 구매링크 ↓↓



일반적인 (정적) 웹페이지는 서버에 미리 저장된 파일을 그대로 웹페이지에 전달하므로, 손쉽게 크롤링 할 수 있습니다.


반면 동적 웹페이지는 서버에 있는 데이터들이 스크립트에 의해 가공처리한 후 생성되어 웹 페이지에 전달되므로, 웹페이지가 계속해서 바뀌게 됩니다. 따라서 일반적을 방법으로는 크롤링할 수 없습니다.

dynamic-web.png


네이버의 요약 재무제표 항목 역시 이러한 동적 웹페이지로 이루어져 있어 기존 방식으로 크롤링 하려면 매우 번거롭지만, 셀레니움을 이용할 경우 손쉽게 크롤링할 수 있습니다.

Selenium은 주로 웹앱을 테스트하는데 이용하는 프레임워크로써, webdriver라는 API를 통해 운영체제에 설치된 Chrome등의 브라우저를 제어합니다.

브라우저를 직접 동작시킨다는 것은 JavaScript를 이용해 비동기적으로 혹은 뒤늦게 불러와지는 컨텐츠들을 가져올 수 있다는 것입니다. 즉, ‘눈에 보이는’ 컨텐츠라면 모두 가져올 수 있으며, JavaScript로 렌더링이 완료된 후의 DOM결과물에 접근이 가능합니다.

(사실 몰라도 됩니다...)

먼저 아래 3개 파일을 본인의 OS에 맞게 다운로드 받은 후, 하나의 폴더에 저장해줍니다. geckodriver chromeDriver는 압축을 해제하며, selenium은 그대로 둡니다.

chromeDriver의 경우 본인이 사용하고 있는 크롬 버젼과 같은 것을 다운로드해야 하며, 해당 버젼의 확인은 크롬창에서 [도움말] -> [Chrome 정보]를 통해 확인할 수 있습니다.







윈도우에서 cmd를 통해 명령 프롬프트를 연 후, 아래 명령어를 입력합니다.

C:\Rselenium에는 파일은 다운로드 받은 폴더의 위치를 입력하며, standalone 뒤 3.141.59는 본인이 다운로드 받은 selenium-server-standalone 파일의 버젼과 같은 숫자를 입력합니다.

해당 cmd 창은 계속 열어두어야 합니다.

cd C:\Rselenium
java -Dwebdriver.gecko.driver="geckodriver.exe" -jar selenium-server-standalone-3.141.59.jar -port 4445



install.packages('RSelenium')
install.packages('seleniumPipes')
library(RSelenium)
library(seleniumPipes)
library(rvest)
library(httr)

셀레니움 관련 패키지인 RSelenium와 seleniumPipes를 인스톨 한 후, 관련 패키지들을 열어줍니다.



remDr = remoteDriver(
  remoteServerAddr="localhost",
  port=4445L,
  browserName="chrome")

remDr$open()
## [1] "Connecting to remote server"
## $acceptInsecureCerts
## [1] FALSE
## 
## $browserName
## [1] "chrome"
## 
## $browserVersion
## [1] "77.0.3865.75"
## 
## $chrome
## $chrome$chromedriverVersion
## [1] "76.0.3809.126 (d80a294506b4c9d18015e755cee48f953ddc3f2f-refs/branch-heads/3809@{#1024})"
## 
## $chrome$userDataDir
## [1] "C:\\Users\\Henry\\AppData\\Local\\Temp\\scoped_dir11848_77181124"
## 
## 
## $`goog:chromeOptions`
## $`goog:chromeOptions`$debuggerAddress
## [1] "localhost:54111"
## 
## 
## $networkConnectionEnabled
## [1] FALSE
## 
## $pageLoadStrategy
## [1] "normal"
## 
## $platformName
## [1] "windows nt"
## 
## $proxy
## named list()
## 
## $setWindowRect
## [1] TRUE
## 
## $strictFileInteractability
## [1] FALSE
## 
## $timeouts
## $timeouts$implicit
## [1] 0
## 
## $timeouts$pageLoad
## [1] 300000
## 
## $timeouts$script
## [1] 30000
## 
## 
## $unhandledPromptBehavior
## [1] "dismiss and notify"
## 
## $webdriver.remote.sessionid
## [1] "6de07eeb6dfc9fd8203ce2708342ca05"
## 
## $id
## [1] "6de07eeb6dfc9fd8203ce2708342ca05"
remDr$navigate('https://finance.naver.com/item/coinfo.nhn?code=005930&target=finsum_more')

먼저 remoteDriver() 함수를 통해 4445번 포트와 크롬을 연결시켜 주며, remDr$open() 함수를 입력하면 크롬 웹창이 열리게 됩니다. remDr$navigate() 함수 내부에 크롤링하고자 하는 사이트 주소를 입력하면 해당 주소로 이동하게 됩니다.

현재 페이지가 셀레니움에 의해 동작되므로, 자동화된 테스트 소프트웨어에 의해 제어되고 있습니다. 라는 문구가 뜹니다.





이제 Financial Summary에 해당하는 부분을 찾아나갑니다.





해당 페이지는 iframe 내부에서 javascript를 통해 타 페이지의 데이터가 들어와있는 형태이므로, 해당 iframe 내부로 접근해야 합니다.


# ID 찾아내기
frames = remDr$findElements(using = "id",
                  value = 'coinfo_cp')

print(frames)
## [[1]]
## [1] "remoteDriver fields"
## $remoteServerAddr
## [1] "localhost"
## 
## $port
## [1] 4445
## 
## $browserName
## [1] "chrome"
## 
## $version
## [1] ""
## 
## $platform
## [1] "ANY"
## 
## $javascript
## [1] TRUE
## 
## $nativeEvents
## [1] TRUE
## 
## $extraCapabilities
## list()
## 
## [1] "webElement fields"
## $elementId
## [1] "f0bfdfff-3c16-4331-ad30-6df5436064cd"
# Frame 안으로 접근
remDr$switchToFrame(frames[[1]])


remDr$findElements() 함수는 원하는 요소로 접근이 가능합니다. 위 iframe은 coinfo_cp라는 id를 사용하고 있으므로 using에는 id를, value에는 coinfo_cp를 입력하며, 이 외에도 xpath, css selector 등 다양한 html 태그를 통해 원하는 html 정보를 찾을 수 있습니다.
그 후 remDr$switchToFrame() 함수를 통해 iframe 내부로 접근합니다.
우리가 필요한 데이터는 연간 재무제표 이므로, [연간]에 해당하는 탭의 위치를 찾은 후 Xpath를 복사합니다. 해당 위치는 다음과 같습니다.
//*[@id="cns_Tab21"]




# 연간 클릭
remDr$findElement(using = 'xpath',
                  value ='//*[@id="cns_Tab21"]')$clickElement()
remDr$findElement() 함수 내부에 xpath를 사용하여 해당 위치를 찾은 후, $clickElement()를 입력하면 해당 위치를 클릭하게 됩니다. 이제 연간 재무제표가 표시된 페이지를 읽어오도록 하겠습니다.




page_parse = remDr$getPageSource()[[1]]
page_html = page_parse %>% read_html()
  1. remDr$getPageSource() 함수를 통해 페이지 소스를 읽어옵니다.
  2. read_html() 함수를 통해 HTML 정보만을 읽어옵니다. 이제 재무제표 데이터가 들어있는 테이블만 추출하면 됩니다.

Sys.setlocale('LC_ALL', 'English')
table = page_html %>% html_table(fill = TRUE)
Sys.setlocale('LC_ALL', 'Korean')
df = table[[13]]
head(df)

##         주요재무정보                                  연간
## 1       주요재무정보 2014/12\n\t\t\t\t\t\t\t\t\t(IFRS연결)
## 2             매출액                             2,062,060
## 3           영업이익                               250,251
## 4 영업이익(발표기준)                               250,251
## 5   세전계속사업이익                               278,750
## 6         당기순이익                               233,944
##                                    연간
## 1 2015/12\n\t\t\t\t\t\t\t\t\t(IFRS연결)
## 2                             2,006,535
## 3                               264,134
## 4                               264,134
## 5                               259,610
## 6                               190,601
##                                    연간
## 1 2016/12\n\t\t\t\t\t\t\t\t\t(IFRS연결)
## 2                             2,018,667
## 3                               292,407
## 4                               292,407
## 5                               307,137
## 6                               227,261
##                                    연간
## 1 2017/12\n\t\t\t\t\t\t\t\t\t(IFRS연결)
## 2                             2,395,754
## 3                               536,450
## 4                               536,450
## 5                               561,960
## 6                               421,867
##                                    연간
## 1 2018/12\n\t\t\t\t\t\t\t\t\t(IFRS연결)
## 2                             2,437,714
## 3                               588,867
## 4                               588,867
## 5                               611,600
## 6                               443,449
##                                       연간
## 1 2019/12(E)\n\t\t\t\t\t\t\t\t\t(IFRS연결)
## 2                                2,305,363
## 3                                  268,387
## 4                                         
## 5                                  294,636
## 6                                  215,948
##                                       연간
## 1 2020/12(E)\n\t\t\t\t\t\t\t\t\t(IFRS연결)
## 2                                2,469,186
## 3                                  354,097
## 4                                         
## 5                                  381,833
## 6                                  279,894
##                                       연간
## 1 2021/12(E)\n\t\t\t\t\t\t\t\t\t(IFRS연결)
## 2                                2,648,131
## 3                                  446,023
## 4                                         
## 5                                  480,005
## 6                                  352,816

  1. 로케일 언어를 English로 변경합니다.
  2. html_table() 함수를 통해 편하게 테이블 데이터만 추출할 수 있습니다.
  3. 다시 로케일 언어를 Korean으로 변경합니다.
  4. 총 19개 테이블 중 13번째 테이블이 연간 재무제표에 해당합니다. 만일 다른 테이블을 추출하고자 하면 해당 숫자만 변경해주면 됩니다.

우리가 원하는 연간 재무제표 데이터가 제대로 크롤링 되었으며, 간단한 클렌징 작업만 거치면 모든 작업이 완료됩니다.

library(stringr)
library(magrittr)

rownames(df) = df[, 1]
df = df[, -1]

colnames(df) = df[1, ]
df = df[-1, ]
colnames(df) = str_sub(colnames(df), 1, 7)

df = sapply(df, function(x) {
  str_replace_all(x, ',', '') %>%
    as.numeric()
}) %>%
  data.frame(., row.names = rownames(df))

head(df)
##                    X2014.12 X2015.12 X2016.12 X2017.12 X2018.12 X2019.12
## 매출액              2062060  2006535  2018667  2395754  2437714  2305363
## 영업이익             250251   264134   292407   536450   588867   268387
## 영업이익(발표기준)   250251   264134   292407   536450   588867       NA
## 세전계속사업이익     278750   259610   307137   561960   611600   294636
## 당기순이익           233944   190601   227261   421867   443449   215948
## 당기순이익(지배)     230825   186946   224157   413446   438909   213435
##                    X2020.12 X2021.12
## 매출액              2469186  2648131
## 영업이익             354097   446023
## 영업이익(발표기준)       NA       NA
## 세전계속사업이익     381833   480005
## 당기순이익           279894   352816
## 당기순이익(지배)     275918   347589

  1. 첫번째 열을 행이름으로 설정한 후, 해당 열을 삭제합니다.
  2. 첫번재 행을 열이름으로 설정한 후, 해당 행을 삭제합니다.
  3. 열이름에서 str_sub() 함수를 이용해 1~7번째 글자만 선택합니다. (YYYY/MM)
  4. sapply() 함수 내에서 str_replace_all() 함수를 이용해 모든 콤마(,)를 없앤 후 숫자 형태로 변경합니다.

결과물을 출력해보면 클렌징 작업이 완료되었습니다.