1. R언어 빅데이터 전략 1

R언어는 데이터 크기가 전체 메모리 공간의 최대 20%를 넘게 되면 급격한 성능저하를 경험할 수 있다. 따라서, 데이터 크기가 커지게 되면 물리적인 메모리를 키우던가, 표본추출 등을 통해서 데이터 크기를 줄이던가, bigmemory 팩키지를 활용하여 하드디스크를 외부 확장 메모리로 활용하여 필요할 때마다 메모리가 수용할 수 있는 덩어리(chunk) 크기로 잘라서 가져와서 처리하는 방법을 고려해야만 한다.

“R is not well-suited for working with data larger than 10-20% of a computer’s RAM.” - The R Installation and Administration Manual

표본추출의 경우 편이가 없고 전체 데이터셋을 충분히 반영한다면 이를 통해 도출된 모형도 받아들일 수 있기 때문에 충분히 권장된다.

R은 메모리에 모든 객체를 저장하기 때문에 고성능 하드웨어를 구입하게 되면 빅데이터 문제를 효율적으로 처리할 수 있다. 즉 32비트 컴퓨터로는 최대 2GB 공간만 활용할 수 있는 반면 64비트 컴퓨터로 교체하게 되면 최대 8TB 공간을 활용할 수 있다.

bigmemory, ff, ffbase 계열 팩키지를 활용하게 되면 하드디스크에 데이터를 저장하고 필요할 때만 덩어리(chunk)로 나눠서 메모리에서 처리하는 것이 가능하다. 덩어리(chunk)로 나누게 되면 자연스럽게 병렬처리도 가능하다. 즉, 덩어리를 쪼개서(split), 처리하고(apply), 결합하는(combine), split-apply-combine 전략을 적용하여 빅데이터를 하드디스크에 넣어 효율적으로 분석하고 모형을 개발할 수 있다.

빅데이터 빅메모리

더 고성능 프로그래밍 언어와 통합은 R에서 작업을 수행하기 보다 범용 프로그래밍 언어 자바(Java), C/C++ 언어를 활용하여 작업을 수행하게 하고 결과값을 반환받는 형태로 작업을 수행한다. rJava, Rcpp 팩키지가 대표적으로 이러한 패러다임을 구현하는 목적으로 개발되어 많이 활용되고 있다.

tidyverse 빅데이터 생태계

tidyverse 생태계에 기반한 빅데이터 처리 전략에 대해서는 빅데이터 - tidyverse 스파크를 참조한다.

2. R 메모리 2 3

2.1. 물리적 컴퓨터 메모리 크기 확인

데이터를 컴퓨터로 분석하는데 가장 먼저 물리적인 컴퓨터 메모리 크기를 확인한다. 이를 위해서 system 쉘명령어를 통해 systeminfo 를 호출하여 “총 실제 메모리:”를 통해 물리 메모리 크기를 확인한다.

library(tidyverse)
library(pryr)
library(stringr)

# 1. 컴퓨터 시스템 정보 ------

Sys.info()["sysname"]
  sysname 
"Windows" 
# 2. 메모리 확인 -----
# system("awk '/MemFree/ {print $2}' /proc/meminfo", intern=TRUE)
memory_cmd <- 'systeminfo'
system_info_v <- system(memory_cmd, intern=TRUE)

system_info_v[str_detect(system_info_v, "총 실제 메모리:")]
[1] "총 실제 메모리:          16,308MB"

2.2. R 환경에서 사용가능한 메모리 크기

R 환경에서 사용가능한 메모리 크기를 memory.limit(), memory.size() 함수를 통해 확인한다. 만약 데이터나 randomForest 같은 모형객체가 지나치게 큰 경우 memory.limit(size = 30000) 명령어를 통해 물리적으로 할당받은 16GB보다 더큰 30GB를 가상메모리로 사용하는 것도 가능하다. 단, 메모리가 아닌 하드디스크의 저장공간을 사용하기 때문에 속도저하에 따른 벌칙은 감수한다.

  • memory.limit: 메가바이트(MB) 단위로 현재 최대 사용 가능한 메모리 크기를 반환한다.
  • memory.size: 메가바이트(MB) 단위로 최대 사용중인 전체 메모리 공간 혹은 할당된 메모리 공간크기를 반환한다.
# 3. R에서 사용가능한 메모리: 단위가 MB-----
# memory.limit  returns an integer value giving the current maximum memory use allowed (in megabytes).
# memory.size   returns the maximum total allocated memory or total memory in use (in megabytes).

## 최대 가능 메모리
memory.limit()
[1] 16307
## OS에서 할당받은 메모리 크기 변경
memory.limit(size = 30000) # 30GB 가상메모리 확장
[1] 30000

2.3. 메모리 공간 전후 비교

데이터를 가져오거나 dplyr를 통해 데이터 작업을 하면서 중간 객체가 만들어지고, 그래프를 생성하는 등 다양한 객체를 생성시키게 되면 메모리 사용공간이 증가하는 것을 파악할 수 있다. 문제는 생성되는 객체크기보다 더 많은 메모리공간이 점유된다는 점이다. 윈도우는 이런 면에서 악명이 높다.

# 4. `memory.size()` 확인 ------
# https://stackoverflow.com/questions/14352565/r-memory-issue-with-memory-limit

before_mem_size <- memory.size()

test_df <- data.frame(ga = character(0), na=numeric(0))

for(i in 1:10000) {
    repeat_num <- rpois(1, 10)
    test_df <- bind_rows(test_df, data.frame(ga = sample(letters, repeat_num, replace=TRUE), 
                                             na = runif(repeat_num)))
}

after_mem_size <- memory.size()


cat("생성이전", "\t", "생성이후", "\t", ":", "차이(Mb)", "\n",
    before_mem_size, "\t\t", after_mem_size, "\t", ":", format(object.size(test_df), units = "auto"), "\n")
생성이전     생성이후    : 차이(Mb) 
 62.11       72.94   : 1.5 Mb 

2.4. 메모리 사용량 4

일반적으로 데이터 분석을 하게 되면 데이터, 모형, 그래프, 변수 등 다양한 객체가 R 메모리 공간을 잡아먹는다.

showMemoryUse() 함수를 만들어서 작업하고 있는 공간에서 메모리를 많이 사용하고 있는 객체를 정의하고 이를 필요한 경우 삭제한다. 불필요하고 쓸모가 없어진 객체를 rm 명령어를 통해 삭제한다.

# 1. 메모리 사용량 ------

showMemoryUse <- function(sort="size", decreasing=FALSE, limit) {
    
    objectList <- ls(parent.frame())
    
    oneKB <- 1024
    oneMB <- 1048576
    oneGB <- 1073741824
    
    memoryUse <- sapply(objectList, function(x) as.numeric(object.size(eval(parse(text=x)))))
    
    memListing <- sapply(memoryUse, function(size) {
        if (size >= oneGB) return(paste(round(size/oneGB,2), "GB"))
        else if (size >= oneMB) return(paste(round(size/oneMB,2), "MB"))
        else if (size >= oneKB) return(paste(round(size/oneKB,2), "kB"))
        else return(paste(size, "bytes"))
    })
    
    memListing <- data.frame(objectName=names(memListing),memorySize=memListing,row.names=NULL)
    
    if (sort=="alphabetical") memListing <- memListing[order(memListing$objectName,decreasing=decreasing),] 
    else memListing <- memListing[order(memoryUse,decreasing=decreasing),] #will run if sort not specified or "size"
    
    if(!missing(limit)) memListing <- memListing[1:limit,]
    
    print(memListing, row.names=FALSE)
    return(invisible(memListing))
}

# 2. 예제 데이터 -----

## 2.1. 데이터프레임
iris_df <- read_csv("https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data", col_names = FALSE)

iris_df <- iris_df %>% rename(sepal_length = X1,
                              sepal_width = X2,
                              petal_length = X3,
                              petal_width = X4,
                              class = X5) %>% 
    mutate(class = str_replace(class, "Iris-", "")) %>% 
    mutate(class = factor(class, levels = c("setosa", "versicolor", "virginica")))

# 2. 탐색적 데이터 분석 -----------------------------
library(lattice)
super.sym <- trellis.par.get("superpose.symbol")

iris_lattice <- splom( ~ iris_df[1:4], groups = class, data = iris_df,
       panel = panel.superpose,
       key = list(title = "붓꽃 3종 산점도",
                  columns = 3, 
                  points = list(pch = super.sym$pch[1:3],
                                col = super.sym$col[1:3]),
                  text = list(c("Setosa", "Versicolor", "Virginica"))))

# 3. 나무모형 -----------------------------
## 3.1. rpart 
iris_rpart <- rpart::rpart(class ~ ., data = iris_df, method="class")

## 3.2. Random Forest 모형 -------------------------

iris_tuned_rf <- randomForest::randomForest(class ~ ., 
                              importance=TRUE,
                              data=iris_df)

# 4. 메모리 사용량 추정 -------------------------

showMemoryUse(decreasing=TRUE, limit=5)
    objectName memorySize
       test_df    1.54 MB
  iris_lattice  590.09 kB
 iris_tuned_rf   490.7 kB
 showMemoryUse  182.95 kB
    iris_rpart   70.02 kB
rm(test_df)

showMemoryUse(decreasing=TRUE, limit=5)
    objectName memorySize
  iris_lattice  590.09 kB
 iris_tuned_rf   490.7 kB
 showMemoryUse  182.95 kB
    iris_rpart   70.02 kB
 system_info_v   27.08 kB

3. 하드디스크 저장공간 활용 빅데이터 처리 툴체인 5 6

R을 기반언어로 빅데이터(수십~수백 GB)를 노트북이나 PC에서 데이터를 처리할 수 있는 손쉬운 방법을 살펴보자.

bigmemory는 컴퓨터 메모리(RAM)보다 훨씬 더 큰 행렬(matrix)에 빅데이터를 저장하고 이를 처리하는 기반이 되는 자료구조를 제공한다. 빅데이터를 처리하는 기본 전략은 디스크에 빅데이터를 저장하고 필요할 때만 메모리로 필요한 부분만큼 옮겨 처리한다. 이를 위해 big.matrix라는 자료구조가 새로이 고안되었다.

big.matrix 자료구조로 빅데이터를 가져오게 되면 다음 두가지 파일에 주목한다.

3.1. bigmemtory 맛보기

big.matrix 함수를 활용하여 빅데이터 자료구조를 생성하고 이를 분석 및 모형 개발에 즉시 활용할 수 있다. R 행렬(matrix)를 통해 데이터 분석을 해본 경험이 있다면 수월할 수 있다.

# 0. 환경설정 -----
library(tidyverse)
library(bigmemory)
library(biganalytics)
library(bigtabulate)
library(gmodels)
library(ggpubr)
library(extrafont)
loadfonts()

# 1. big.matrix 생성 -----

big_matrix_smpl <- big.matrix(nrow = 5, ncol = 3, type = "double", init = 0,
                              backingfile = "hello_world_big_matrix.bin",
                              descriptorfile = "hello_world_big_matrix.desc")

## 1.1. 기본적인 사용법 -----
### 데이터 할당
big_matrix_smpl[3,2] <- 7
### 데이터 살펴보기
head(big_matrix_smpl)
     [,1] [,2] [,3]
[1,]    0    0    0
[2,]    0    0    0
[3,]    0    7    0
[4,]    0    0    0
[5,]    0    0    0
### 행과 열 정보
dim(big_matrix_smpl)
[1] 5 3

3.2. 미국 주택담보 데이터 가져오기

https://www.fhfa.gov/DataTools/Downloads 웹사이트에서 주택담보대출 데이터를 가져온다. 이유는 matrix 행렬 자료 구조가 숫자와 문자(요인)가 섞인 자료를 처리할 수 없기 때문에 동일한 자료형태를 유지하고 있는 공공데이터를 불러 읽어온다.

데이터에 공백이 다수 있기 때문에 공백처리를 위해서 readLines 함수로 중복 공백을 단일 공백으로 변환하는 작업을 수행한 후에 read.big.matrix 함수를 통해 데이터를 분석한다.

# 2. 빅데이터 가져오기 -----
# https://www.fhfa.gov/DataTools/Downloads
# download.file("https://www.fhfa.gov/DataTools/Downloads/Documents/Enterprise-PUDB/National-File-A/2016_SFNationalFileA2016.zip", destfile = "data/2016_SFNationalFileA2016.zip")
# unzip(zipfile = "data/2016_SFNationalFileA2016.zip", exdir = "data", overwrite = TRUE)

# mort_txt <- readLines("data/fhlmc_sf2016a_loans.txt")
# mort_txt <- str_replace_all(mort_txt, "[\\s]+", " ")
# writeLines(mort_txt, "data/fhlmc_sf2016a_loans_clean.txt")

## 2.1. 데이터 읽어들이기 -----
mort_bm <- read.big.matrix("data/fhlmc_sf2016a_loans_clean.txt",
                           sep=" ",
                           header = FALSE,
                           type = "integer",
                           backingfile = "mortgage.bin", 
                           descriptorfile = "mortgage.desc")

head(mort_bm)
     [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10] [,11] [,12] [,13]
[1,]    2    1    1    3    3    3    2    8    4     5     9     1     5
[2,]    2    2    1    2    3    2    1    8    4     5     9     1     5
[3,]    2    3    1    1    2    3    2    1    4     9     9     3     3
[4,]    2    4    1    2    3    3    2    8    4     5     5     1     2
[5,]    2    5    1    1    2    3    4    1    4     5     5     1     2
[6,]    2    6    1    1    3    3    1    8    4     5     5     2     2
     [,14] [,15]
[1,]     1     4
[2,]     1     4
[3,]     1     4
[4,]     1     4
[5,]     1     4
[6,]     1     4

3.3. 빅데이터 탐색적 데이터 분석

빅데이터에서 단변량 범주형 변수를 추출하여 table 함수를 활용하여 빈도를 세고 나서 이를 R 데이터프레임으로 변환한 후에 tidyverse 생태계로 적절한 처리를 한다.

빅데이터 이변량 범주형 변수의 경우 biganalytics 팩키지 bigtable() 함수를 활용하여 두 범주형 변수에 맞춰 빈도수를 계산하고 나서 단변량 범주형 변수와 동일하게 교차분석 혹은 시각화를 한다.

# 3. 빅데이터 탐색적 분석 -----
## 3.1. 단변량 범주형 변수 
table(mort_bm[, 10]) %>% tbl_df %>% 
    rename(인종 = Var1) %>% 
    mutate(인종변수 = case_when(인종 == 1 ~ "American Indian or Alaska Native",
                              인종 == 2 ~ "Asian",
                              인종 == 3 ~ "Black or African American",
                              인종 == 4 ~ "Native Hawaiian or Other Pacific Islander",
                              인종 == 5 ~ "White",
                              인종 == 6 ~ "Two or more races",
                              인종 == 7 ~ "Hispanic or Latino",
                              인종 == 9 ~ "Not available not applicable")) %>% 
    select(인종, 인종변수, 빈도수=n) %>% 
    mutate(비율 = scales::percent(빈도수/sum(빈도수))) %>% 
    arrange(desc(빈도수))
# A tibble: 8 x 4
  인종  인종변수                                   빈도수 비율 
  <chr> <chr>                                       <int> <chr>
1 5     White                                     1089377 71.6%
2 9     Not available not applicable               143302 9.4% 
3 7     Hispanic or Latino                         112057 7.4% 
4 2     Asian                                       99910 6.6% 
5 3     Black or African American                   42255 2.8% 
6 6     Two or more races                           27973 1.8% 
7 4     Native Hawaiian or Other Pacific Islander    3637 0.2% 
8 1     American Indian or Alaska Native             2487 0.2% 
## 3.2. 이변량 범주형 변수 
bigtable(mort_bm, c(8, 12)) %>% as.data.frame() %>% 
    rownames_to_column(var = "대출목적") %>% 
    rename(남성 = `1`, 
           여성 = `2`) %>% 
    select(대출목적, 남성, 여성) %>% 
    filter(row_number() != 3) %>%
    mutate(대출목적 = ifelse(대출목적 == 1, "주택구입", "기타")) %>% 
    gather(성별, 지원자수, -대출목적) %>% 
        ggplot(aes(x=대출목적, y=지원자수, fill=성별)) +
          geom_bar(stat="identity", position = "dodge") +
          scale_y_continuous(labels = scales::comma) +
          theme_pubr(base_family = "NanumGothic") +
          labs(x="")

3.4. 맵리듀스(Map-Reduce)

빅데이터 분석의 전형적인 방법인 Split-Apply-Combine 전략을 Map, Reduce 함수를 활용하여 구현한다. 즉, 빅데이터를 대도시와 그렇지 않는 것으로 나눈 후에 소득비율을 계산하는 함수(income_prop)을 적용하여 처리한 후에 Reduce()통해 결과값을 취합하여 정리한다.

# 4. 빅데이터 분석 - Split-Apply-Combine -----

## 4.1. 쪼개기(Split) ----- 
mort_metro_split_bm <- split(1:nrow(mort_bm), mort_bm[, 3])

str(mort_metro_split_bm)
List of 2
 $ 0: int [1:136467] 7 34 39 40 45 47 82 88 97 110 ...
 $ 1: int [1:1384531] 1 2 3 4 5 6 8 9 10 11 ...
## 4.2. 적용(Apply) ----- 
income_prop <- function(bm, rows) {
    bm_subset <- bm[rows, ]
    
    prop_low_income  <- sum(bm_subset[, 5] == 1) / (sum(bm_subset[, 5] == 1) + sum(bm_subset[, 5] == 2) + sum(bm_subset[, 5] == 3))
    prop_mid_income  <- sum(bm_subset[, 5] == 2) / (sum(bm_subset[, 5] == 1) + sum(bm_subset[, 5] == 2) + sum(bm_subset[, 5] == 3))
    prop_high_income <- sum(bm_subset[, 5] == 3) / (sum(bm_subset[, 5] == 1) + sum(bm_subset[, 5] == 2) + sum(bm_subset[, 5] == 3))

    c(prop_low_income, prop_mid_income, prop_high_income)
}

mort_income_by_metro_lst <- Map(function(rows) income_prop(mort_bm, rows), mort_metro_split_bm)

## 4.3. 결합(Combine) ----- 

Reduce(rbind, mort_income_by_metro_lst) %>% tbl_df
# A tibble: 2 x 3
     V1    V2    V3
  <dbl> <dbl> <dbl>
1 0.102 0.752 0.145
2 0.124 0.392 0.484
## 4.4. 교차검증(Crosscheck) ----- 

income_metro_df <- bigtable(mort_bm, c(3, 5)) %>% as.data.frame() %>% 
    rownames_to_column(var = "도시구분") %>% 
    select(-`9`) %>% 
    gather(소득집단, 소득, - 도시구분) %>% 
    mutate(도시구분 = ifelse(도시구분 == 1, "대도시", "지방"),
               소득집단 = case_when(소득집단 == 1 ~ "<10%",
                                    소득집단 == 2 ~ ">=10%, <30%",
                                    TRUE ~ ">=30%"))

income_metro_df %>% group_by(도시구분) %>% 
    mutate(비율 = 소득/sum(소득)) %>% 
    select(-소득) %>% 
    spread(소득집단, 비율)
# A tibble: 2 x 4
# Groups:   도시구분 [2]
  도시구분 `<10%` `>=10%, <30%` `>=30%`
* <chr>     <dbl>         <dbl>   <dbl>
1 대도시    0.124         0.392   0.484
2 지방      0.102         0.752   0.145

  1. EODA (2013), Five ways to handle Big Data in R

  2. Statckoverflow, How to check the amount of RAM in R

  3. 제타위키, 윈도우 메모리 용량 확인

  4. Stackoverflow, Tricks to manage the available memory in an R session

  5. How To Work With Files Too Large For A Computer’s RAM? Using R To Process Large Data In Chunks Practical walkthroughs on machine learning, data exploration and finding insight.

  6. Sundar Pradeep & Philip Moy(2015), Handling large data sets in R