'R 데이터 조작 및 전처리 패키지'에 해당되는 글 1건

  1. 2016.09.24 [R] 데이터 전처리, 조작을 위한 dplyr 패키지 : chain operations, pipe operator (Operator %>%, shift+ctrl+M) (2)

지난번 포스팅에서는 dplyr 패키지의

 

(1) filter(), slice(), arrange(), select(), rename() 함수 ( ☞ 바로 가기)

(2) distinct(), sample_n(), sample_frac(), mutate(), transmute(), summarise(), summarise_each(), group_by() 함수 ( ☞ 바로 가기) 에 대해서 알아보았습니다.

 

이번 포스팅에서는 dplyr 패키지의 chain operations (Operator %>%, shift+ctrl+M)에 대해서 알아보겠습니다. 

 

저는 R의 다른 함수를 사용하다가 처음으로 dplyr 패키지의 chain operation (%>%), pipe operator (%>%)을 접하면 '어, 이거 뭐지? 오타인가?' 하는 당혹감이 들었었습니다.  그런데 이게 쓰다보니 참 편하고 프로그램 가독성이 좋더군요.  R로 프로토타입핑 해놓고 시스템화를 위해 개발자에게 설명해줄 때도 개발자가 쉽게 이해를 하고요.

 

 

마치 ggplot2가 시각화를 문법으로 체계화했듯이 dplyr도 그동안 dplyr (1), (2)번 포스팅에서 다루었던 dplyr 함수들을 이번 시간에 소개할 chain operation으로 굴비 꿰듯이 엮어서 물 흐르듯이 데이터 조작, 전처리를 할 수 있는 매끈한 문법을 생성하게 됩니다.

 

 

 

먼저 R dplyr 패키지를 로딩하고, 실습 예제로 사용할 MASS 패키지의 Cars93 데이터셋에 대해 살펴보면 아래와 같습니다.  (Cars93은 자동차 93개의 관측치, 차량 정보 27개의 변수를 가진 데이터 프레임).

 

> library(dplyr)
> library(MASS) # dataframe Cars93
> str(Cars93)
'data.frame':	93 obs. of  27 variables:
 $ Manufacturer      : Factor w/ 32 levels "Acura","Audi",..: 1 1 2 2 3 4 4 4 4 5 ...
 $ Model             : Factor w/ 93 levels "100","190E","240",..: 49 56 9 1 6 24 54 74 73 35 ...
 $ Type              : Factor w/ 6 levels "Compact","Large",..: 4 3 1 3 3 3 2 2 3 2 ...
 $ Min.Price         : num  12.9 29.2 25.9 30.8 23.7 14.2 19.9 22.6 26.3 33 ...
 $ Price             : num  15.9 33.9 29.1 37.7 30 15.7 20.8 23.7 26.3 34.7 ...
 $ Max.Price         : num  18.8 38.7 32.3 44.6 36.2 17.3 21.7 24.9 26.3 36.3 ...
 $ MPG.city          : int  25 18 20 19 22 22 19 16 19 16 ...
 $ MPG.highway       : int  31 25 26 26 30 31 28 25 27 25 ...
 $ AirBags           : Factor w/ 3 levels "Driver & Passenger",..: 3 1 2 1 2 2 2 2 2 2 ...
 $ DriveTrain        : Factor w/ 3 levels "4WD","Front",..: 2 2 2 2 3 2 2 3 2 2 ...
 $ Cylinders         : Factor w/ 6 levels "3","4","5","6",..: 2 4 4 4 2 2 4 4 4 5 ...
 $ EngineSize        : num  1.8 3.2 2.8 2.8 3.5 2.2 3.8 5.7 3.8 4.9 ...
 $ Horsepower        : int  140 200 172 172 208 110 170 180 170 200 ...
 $ RPM               : int  6300 5500 5500 5500 5700 5200 4800 4000 4800 4100 ...
 $ Rev.per.mile      : int  2890 2335 2280 2535 2545 2565 1570 1320 1690 1510 ...
 $ Man.trans.avail   : Factor w/ 2 levels "No","Yes": 2 2 2 2 2 1 1 1 1 1 ...
 $ Fuel.tank.capacity: num  13.2 18 16.9 21.1 21.1 16.4 18 23 18.8 18 ...
 $ Passengers        : int  5 5 5 6 4 6 6 6 5 6 ...
 $ Length            : int  177 195 180 193 186 189 200 216 198 206 ...
 $ Wheelbase         : int  102 115 102 106 109 105 111 116 108 114 ...
 $ Width             : int  68 71 67 70 69 69 74 78 73 73 ...
 $ Turn.circle       : int  37 38 37 37 39 41 42 45 41 43 ...
 $ Rear.seat.room    : num  26.5 30 28 31 27 28 30.5 30.5 26.5 35 ...
 $ Luggage.room      : int  11 15 14 17 13 16 17 21 14 18 ...
 $ Weight            : int  2705 3560 3375 3405 3640 2880 3470 4105 3495 3620 ...
 $ Origin            : Factor w/ 2 levels "USA","non-USA": 2 2 2 2 2 1 1 1 1 1 ...
 $ Make              : Factor w/ 93 levels "Acura Integra",..: 1 2 4 3 5 6 7 9 8 10 ...

 

 

 

R dplyr Chain operations (Chaining) : Operator %>% (단축키 shift+ctrl+M)

 

 

(1) 어떨 때 chain operations 이 유용한가?

 

(1-1) 여러 단계의 절차를 필요로 해서 중간 결과를 저정 후 그 객체를 후속 절차에서 받아서 사용해야 하는 경우의 프로그래밍의 경우 chain operation %>%이 유용합니다.

 

예를 들어, "Cars93 데이터프레임에서 차생산국가(Origin), 차종(Type), 실린더개수(Cylinders)별로 차가격(Price)과 고속도로연비(MPG.highway)의 평균을 구하되, 차가격 평균 10 초과 & 고속도로연비 25 초과하는 경우만 선별해서 제시하시오"라는 문제가 있다고 합시다.

 

이 예제 문제를 단계별로 중간 산출물을 저장하고, 이를 후속 단계에서 받아서 프로그래밍해보면 아래와 같습니다.  프로그래밍이 길기도 하고, 단계가 하나씩 늘어날수록 헷갈리고 복잡해지지요?

 

 

# why chaining?  when to use chain operation?

# : (1) step-by-step operation, saving the intermediate results
 

select <- dplyr::select # assigning select() functon of dplyr package explicitly

 

a1 <- group_by(Cars93, Origin, Type, Cylinders)
a2 <- select(a1, Price, MPG.highway)
a3 <- summarise(a2,
                Price_m = mean(Price, na.rm = TRUE),
                MPG.highway_m = mean(MPG.highway, na.rm = TRUE))
a4 <- filter(a3, Price_m > 10 | MPG.highway_m > 25)

 

 

> a4
Source: local data frame [25 x 5]
Groups: Origin, Type [11]

   Origin    Type Cylinders  Price_m MPG.highway_m
   <fctr>  <fctr>    <fctr>    <dbl>         <dbl>
1     USA Compact         4 12.82857      30.57143
2     USA   Large         6 22.40000      27.28571
3     USA   Large         8 27.62500      25.75000
4     USA Midsize         4 15.87500      29.50000
5     USA Midsize         6 22.84000      27.20000
6     USA Midsize         8 40.10000      25.00000
7     USA   Small         4 10.04286      33.85714
8     USA  Sporty         4 14.60000      28.75000
9     USA  Sporty         6 19.53333      26.66667
10    USA  Sporty         8 38.00000      25.00000
# ... with 15 more rows

 

 

 

 

(1-2) 여러 단계의 각 단계를 괄호로 싸서 프로그래밍하다보니 깊이가 깊어져서 해석하고 이해하기 어려운 경우, dplyr의 chain operation %>% (pipe operator) 이 유용합니다.

 

아래의 R script는 (1-1)과 동일한 결과를 내는 프로그래밍인데요, 4 depth 까지 들어가다보니 안쪽에서 부터 바깥쪽으로 순서대로 해독하기가 많이 어렵습니다. @@~

 

 

# why chaining?  when to use chain operation?
# : (2) Wrapping the function calls inside each other is hard to interpret

filter(
  summarise(
    select(
      group_by(Cars93, Origin, Type, Cylinders),
      Price, MPG.highway
    ),
    Price_m = mean(Price, na.rm = TRUE),
    MPG.highway_m = mean(MPG.highway, na.rm = TRUE)
  ),
  Price_m > 10 | MPG.highway_m > 25

 

 

Adding missing grouping variables: `Origin`, `Type`, `Cylinders`
Source: local data frame [25 x 5]
Groups: Origin, Type [11]

   Origin    Type Cylinders  Price_m MPG.highway_m
   <fctr>  <fctr>    <fctr>    <dbl>         <dbl>
1     USA Compact         4 12.82857      30.57143
2     USA   Large         6 22.40000      27.28571
3     USA   Large         8 27.62500      25.75000
4     USA Midsize         4 15.87500      29.50000
5     USA Midsize         6 22.84000      27.20000
6     USA Midsize         8 40.10000      25.00000
7     USA   Small         4 10.04286      33.85714
8     USA  Sporty         4 14.60000      28.75000
9     USA  Sporty         6 19.53333      26.66667
10    USA  Sporty         8 38.00000      25.00000
# ... with 15 more rows

 

 

 

 

(2) dplyr 패키지의 chain operations은 어떻게 사용하는가? 

 

chain(pipe) operator 는 %>% 이며, 단축키는 shift+ctrl+M 입니다.

 

dataframe %>% group_by() %>% select() %>% summarise() %>% filter() 의 순서로 사용하시면 됩니다.  의도하는 분석 결과를 논리적인 순서대로 찬찬히 생각해보면서 프로그래밍하시면 됩니다. 

 

예를 들면 아래처럼요,

 

"(a) Cars93 데이터프레임에서  %>%  (b) 제조생산국(Origin), 차종(Type), 실린더개수(Cylinders)별로   %>%   (c) 차 가격(Price)과 고속도로 연비(MPG.highway) 변수에 대해   %>%   (d)  (결측값은 제외하고) 평균을 구하는데,   %>%   (e) 단, 가격 평균은 10을 넘고 & 고속도로 연비는 25를 넘는 것만 알고 싶다"

 

 

# How to use dplyr's chain operations %>%
# : dataframe %>% group_by() %>% select() %>% summarise() %>% filter()

Cars93 %>%  # dataframe name
  group_by(Origin, Type, Cylinders) %>%  # group_by()
  select(Price, MPG.highway) %>%  # select() columns
  summarise(
    Price_m = mean(Price, na.rm = TRUE),
    MPG.highway_m = mean(MPG.highway, na.rm = TRUE)  # summarise()
  ) %>%
  filter(Price_m > 10 | MPG.highway_m > 25)  # filter() condition

 

 

Adding missing grouping variables: `Origin`, `Type`, `Cylinders`
Source: local data frame [25 x 5]
Groups: Origin, Type [11]

   Origin    Type Cylinders  Price_m MPG.highway_m
   <fctr>  <fctr>    <fctr>    <dbl>         <dbl>
1     USA Compact         4 12.82857      30.57143
2     USA   Large         6 22.40000      27.28571
3     USA   Large         8 27.62500      25.75000
4     USA Midsize         4 15.87500      29.50000
5     USA Midsize         6 22.84000      27.20000
6     USA Midsize         8 40.10000      25.00000
7     USA   Small         4 10.04286      33.85714
8     USA  Sporty         4 14.60000      28.75000
9     USA  Sporty         6 19.53333      26.66667
10    USA  Sporty         8 38.00000      25.00000
# ... with 15 more rows 

 

 

 

 

(3) dplyr chain operations 는 지켜야 할 순서가 있는가?

 

네, 지켜야 할 순서가 있습니다. 

 

아래에 group_by()를 제일 마지막, 혹은 summarise() 함수 뒤에 위치시켰더니 "unknown variable to group by : Origin'이라고 에러가 났습니다.

 

filter() 조건에 group_by()한 summarise() 중간 결과의 변수가 들어있는 경우에, filter()를 summarise() 함수 앞에 위치시켰더니 'Price_m'이라는 새로운 변수를 찾을 수 없다고 역시 에러가 났습니다.

 

# keep in mind the sequence order of chain operator in dplyr

> Cars93 %>%
+   select(Price, MPG.highway) %>%
+   summarise(
+     Price_m = mean(Price, na.rm = TRUE),
+     MPG.highway_m = mean(MPG.highway, na.rm = TRUE)
+   ) %>%
+   filter(Price_m > 10 | MPG.highway_m > 25) %>% 
+   group_by(Origin, Type, Cylinders)
Error in eval(substitute(expr), envir, enclos) : unknown variable to group by : Origin
> Cars93 %>%
+   select(Price, MPG.highway) %>%
+   summarise(
+     Price_m = mean(Price, na.rm = TRUE),
+     MPG.highway_m = mean(MPG.highway, na.rm = TRUE)
+   ) %>%
+   group_by(Origin, Type, Cylinders) %>% 
+   filter(Price_m > 10 | MPG.highway_m > 25)
Error in eval(substitute(expr), envir, enclos) : unknown variable to group by : Origin
> Cars93 %>%
+   group_by(Origin, Type, Cylinders) %>% 
+   select(Price, MPG.highway) %>%
+   filter(Price_m > 10 | MPG.highway_m > 25) %>% 
+   summarise(
+     Price_m = mean(Price, na.rm = TRUE),
+     MPG.highway_m = mean(MPG.highway, na.rm = TRUE)
+   )
Adding missing grouping variables: `Origin`, `Type`, `Cylinders` Error in eval(substitute(expr), envir, enclos) : object 'Price_m' not found

 

 

 

많은 도움이 되었기를 바랍니다.

 

 

참고로, Python이나 Scala로 Spark 사용할 때 보면 '.' (dot) 을 이용해서 chain operation 하는게 있는데요, R의 '%>%'과 사용방법이 유사합니다. 

 

 [Spark에서 Python으로 map과 reduceByKey를 chaining 해서 단어 세기] 예시

 

rdd = sc.textFile("file")

words = rdd.flatMap(lambda x: x.split(" "))

result = words.map(lambda x: (x, 1)).reduceByKey(lambda x, y: x + y)

 

 

 

이번 포스팅이 도움이 되었다면 아래의 '공감 ~ ♡'를 꾸욱 눌러주세요.

 

[Reference]

1. Introduction to dplyr : http://127.0.0.1:26182/library/dplyr/doc/introduction.html

 

Posted by R Friend R_Friend