13. Iterating with purrr

Published by onesixx on

Iterating with purrr

list에 iteration을 하는 여러가지방법이 있지만, purrr은 input인수에 기반한 functional programming을 특성을 살려 통일된 방법을 제시한다.

https://jennybc.github.io/purrr-tutorial/bk00_vectors-and-lists.html

input으로 vector를 사용한다고 착각하지만, vector가 들어가면 일단 as.list()로 list변환한 후 사용하기 때문에
list를 data인수로 한다는 것이 정확하다.

  • Atomic vectors 는
    종류는 logical, integer, double, character 이 있고
    homogeneous하고
    length는 element 수이고, nrow는 구할수 없다.
  • List는
    atomic vector들의 데이터구조
    실제 R에서는 List는 여전히 vector이지만, atomic vector는 아니다.
    length는 1보다 크다. (column갯수)



This enables us to iterate over a list and apply a function to each element independently.
This is mainly aimed at operating on lists, though it can also be used to apply non-vectorized functions to vectors.

 purrr 라는 이름은 여러가지 의미가 있는데, 기본적으로
It is primarily meant to convey that this package enforces pure programming.
It is also a pun on a cat purring and has five letters,
so it is the same length as many of Hadley Wickham’s other packages such as dplyrreadr and tidyr.

13.1 MAP vs. lapply

map()함수는 purrr의 기초가 된다.
map()은 lapply()와 같이, (기본적으로 pipes를 염두해두고 구성되어 있긴 하지만)
list의 각 element에 각각 독립적으로, function을 적용하고, 결과로 같은 length의 list를 return한다.
=> 굳이 lapply()대신 map()을 사용할 필요는 없지만, tidyverse 패키지의 함수들은 잘 정돈된 syntax를 가지고 있고 helper들도 유용하게 사용할수 있어 사용해 볼만하다.

> theList
$A
     [,1] [,2] [,3]
[1,]    1    4    7
[2,]    2    5    8
[3,]    3    6    9
$B
[1] 1 2 3 4 5
$C
     [,1] [,2]
[1,]    1    3
[2,]    2    4
$D
[1] 2

map()을 사용해서 같은 결과를 얻을 수 있다.

theList <- list(A=matrix(1:9, 3), B=1:5, C=matrix(1:4, 2), D=2)
lapply(theList, sum)
>
$A
[1] 45
$B
[1] 15
$C
[1] 10
$D
[1] 2
# same result using map()
theList %>% map(sum)                 #identical(lapply(theList, sum), theList %>% map(sum))

만약 theList 가 NA값을 가지고 있다면, sum()에 na.rm=T 인자를 추가해 줘야한다.
args(map)는 function (.x, .f, …) 이고 …에 추가적인 na.rm=T라는 인수를 넣어줄수 있다.

theList2 <- theList
theList2[[1]][2, 1] <- NA
theList2[[2]][4] <- NA

lapply(theList2, sum, na.rm=T)

theList2 %>% map(function(x) sum(x, na.rm=TRUE))
theList2 %>% map(sum, na.rm=T)

13.2 여러가지 Type에 MAP() 적용하기

map()을 사용하면 결과는 항상 list로 return된다.
lapply는 map()과 같이 항상 list로 결과를 return하고, sapply는 가능한 vector로 결과를 돌려주려고 하고, 필요시에만 list로 변환해서 결과를 return한다
하지만, lapply의 경우 다른 type이 필요할 경우 한번의 변환을 더 해줘야하고, sapply의 경우, 결과가 어떻게 나왔는지 확인하는 과정이 필요할 수 도 있다.
그래서 purrr는 결과type을 정의하는 추가적인 map_*()함수를 제공한다.
이런함수들을 사용할때 정의된 return값의 type이 다르다면 error가 나기 때문에, 개발자의 의도대로 코딩을 이어갈수 있다.

Image
purrr functions and their corresponding outputs

It is important to note that
each of these map_* functions expects a vector of length one for each element.
A result greater than length one for even a single element will result in an error.

13.2.1 map_int

map_int() 는 결과가 integer일때 사용한다.

#List의 element가 1차원이면 length를 return하고, 2차원이면 row갯수를 return
> theList %>% map_int(NROW)
A B C D 
3 5 2 1 

결과는 Integer로 numeric이어야하므로, mean()함수를 적용시키면 double이 되어 error가 나게 된다.

> theList %>% map_int(mean)
Error: Can't coerce element 1 from a double to a integer

13.2.2 map_dbl

그래면 numeric type이지만, double형태로 return하는 map_dbl()을 사용해 보자.

> theList %>% map_dbl(mean)
  A   B   C   D 
5.0  NA 2.5 2.0 

13.2.3 map_chr

Applying a function that returns character data necessitates map_chr.

> theList %>% map_chr(class)
        A         B         C         D 
 "matrix" "integer"  "matrix" "numeric"

만약 해당 data (여기서는  theList)의 element 중 class가 여러개 혼재되어 있는 element가 있다면,

theList3 <- theList
theList3[['E']] <- factor(c('A', 'B', 'C'), ordered=TRUE)
class(theList3$E)
[1] "ordered" "factor" 
> theList3 %>% map_chr(class)
Error: Result 5 must be a single string, not a character vector of length 2
Call `rlang::last_error()` to see a backtrace

map_chr()는 error를 return할 것이다.
왜냐면, map_char()의 결과는 input인 list의 각각 element에 대해 length가 1인 vector가 return되야 하기 때문이다.

map()을 사용하면 결과가 list 형태로 나오기 때문에, 결과가 vector일 필요가 없어 error는 나지 않는다. map_*()가 적용가능하다면 map()은 항상 적용가능하다.

> theList3 %>% map(class)
$A
[1] "matrix"

$B
[1] "integer"

$C
[1] "matrix"

$D
[1] "numeric"

$E
[1] "ordered" "factor" 

13.2.4 map_lgl

The results of logical operations can be stored in a logical vector using map_lgl.

> theList %>% map_lgl(function(x) NROW(x) < 3)
    A     B     C     D 
FALSE FALSE  TRUE  TRUE

13.2.5 map_df

iteration을 함수 plyr()를 list형태의 데이터에 주는 ldply() 는 결과가 data.frame으로 나오고 이것은 map_df()과 같다.

buildDT <- function(x){ 
              data.table(A=1:x, B=x:1) 
           }
listOfLengths <- list(3, 4, 1, 5)
listOfLengths %>% map(buildDT)

[[1]]
   A B
1: 1 3
2: 2 2
3: 3 1

[[2]]
   A B
1: 1 4
2: 2 3
3: 3 2
4: 4 1

[[3]]
   A B
1: 1 1

[[4]]
   A B
1: 1 5
2: 2 4
3: 3 3
4: 4 2
5: 5 1

위 결과를 하나의 data.table으로 만들때 map_df사용

> listOfLengths %>% map_df(buildDT)
    A B
 1: 1 3
 2: 2 2
 3: 3 1
 4: 1 4
 5: 2 3
 6: 3 2
 7: 4 1
 8: 1 1
 9: 1 5
10: 2 4
11: 3 3
12: 4 2
13: 5 1

13.2.6 map_if

조건이 True일때만 function이 적용되게 하려면 map_if() 를 사용.
조건이 False이면 function이 적용되지 않은 채로 그대로 return한다.

> theList %>% 
    map_if(is.matrix, function(x) x*2)
$A
     [,1] [,2] [,3]
[1,]    2    8   14
[2,]    4   10   16
[3,]    6   12   18

$B
[1]  1  2  3  4 NA

$C
     [,1] [,2]
[1,]    2    6
[2,]    4    8

$D
[1] 2
> theList %>% map_if(is.matrix, ~ .x*2)
> theList

$A
     [,1] [,2] [,3]
[1,]    1    4    7
[2,]    2    5    8
[3,]    3    6    9

$B
[1]  1  2  3  4 NA

$C
     [,1] [,2]
[1,]    1    3
[2,]    2    4

$D
[1] 2

This was easily accomplished using an anonymous function, though purrr provides yet another way to specify a function inline. We could have supplied a formula rather than a function, and map_if (or any of the map functions) would create an anonymous function for us. Up to two arguments can be supplied and must be of the form .x and .y.

13.3 ITERATING OVER A DATA.FRAME

data.frame도 list중에 하나이기 때문에, numeric 컬럼에대해 평균을 구하는 것은 어렵지 않다.
numeric컬럼이 아닌경우에는 warning과 함께 NA가 return 된다.

> data(diamonds, package='ggplot2')
> diamonds %>% map_dbl(mean)
       carat          cut        color      clarity        depth        table        price            x            y 
   0.7979397           NA           NA           NA   61.7494049   57.4571839 3932.7997219    5.7311572    5.7345260 
           z 
   3.5387338 
Warning messages:
1: In mean.default(.x[[i]], ...) :
  argument is not numeric or logical: returning NA
2: In mean.default(.x[[i]], ...) :
  argument is not numeric or logical: returning NA
3: In mean.default(.x[[i]], ...) :
  argument is not numeric or logical: returning NA

This operation can be similarly calculated using summarize_each in dplyr.
Numerically, they are the same, but map_dbl returns a numeric vector and mutate_each returns a single-row data.frame.

> library(dplyr)
> diamonds %>% summarize_each(funs(mean))

# A tibble: 1 × 10
      carat   cut color clarity   depth    table  price        x
      <dbl> <dbl> <dbl>   <dbl>   <dbl>    <dbl>  <dbl>    <dbl>
1 0.7979397    NA    NA      NA 61.7494 57.45718 3932.8 5.731157
# ... with 2 more variables: y <dbl>, z <dbl>

warning was generated for each non-numeric column informing that mean cannot be used on non-numeric data. Even with the warning the function still completes, returning NA for each non-numeric column.

13.4 MAP WITH MULTIPLE INPUTS

mapply()를 사용하면 여러개 list를 인수를 가진 function을 적용할수 있다.

The purrr analog is pmap, with map2 as a special case when the function takes exactly two arguments.

## build two lists
firstList  <- list(A=matrix(1:16, 4), B=matrix(1:16, 2), C= 1:5)
secondList <- list(A=matrix(1:16, 4), B=matrix(1:16, 8), C=15:1)
## adds the number of rows (or length) of corresponding elements
simpleFunc <- function(x, y) { NROW(x) + NROW(y) }  # cf. nrow() vs. NROW()
> mapply(identical, firstList, secondList)
    A     B     C 
 TRUE FALSE FALSE 
> mapply(simpleFunc, firstList, secondList)
$A
[1] 8

$B
[1] 10

$C
[1] 20
> map2(firstList, secondList, simpleFunc)  # same
> map2_int(firstList, secondList, simpleFunc)
 A  B  C 
 8 10 20

더 일반적인 pmap은 List안에 반복될 list를 담고 있는 data를 사용한다.

> pmap(list(firstList, secondList), simpleFunc)
$A
[1] 8

$B
[1] 10

$C
[1] 20
> pmap_int(list(firstList, secondList), simpleFunc)
 A  B  C 
 8 10 20 

13.5 CONCLUSION

Iterating over lists is easier than ever with purrr.
Most of what can be done in purrr can already be accomplished using base R functions such as lapply, but it is quicker with purrr, both in terms of computation and programming time.
In addition to the speed improvements, purrr ensures the results returned are as the programmer expected and was designed to work with pipes, which further enhances the user experience.


onesixx

Blog Owner

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x