iBetter Books
수정

파이프 연산자

데이터 분석 코드를 처음 쓰면 이런 모습이 됩니다.

result <- round(mean(sqrt(abs(c(-4, 9, 16, -1, 25)))), 2)

왼쪽에서 오른쪽으로 읽히지 않습니다. 가장 안쪽의 c()부터 바깥으로 읽어야 합니다. 데이터가 어떤 변환을 거쳤는지 한눈에 보이지 않습니다.

파이프 연산자는 이 문제를 해결합니다. 이전 결과를 다음 함수의 첫 번째 인자로 넘기면서, 코드를 위에서 아래로, 왼쪽에서 오른쪽으로 읽을 수 있게 만들어줍니다.

magrittr의 %>%

tidyverse 패키지와 함께 널리 퍼진 파이프입니다. magrittr 패키지에서 왔고, dplyr을 로드하면 자동으로 사용 가능합니다.

# 파일: 05_pipe.R
library(magrittr)   # 또는 library(dplyr)

c(-4, 9, 16, -1, 25) %>%
  abs() %>%
  sqrt() %>%
  mean() %>%
  round(2)
# 3.42

읽는 방향이 자연스럽습니다. "벡터를 만들고, 절댓값을 구하고, 제곱근을 구하고, 평균을 내고, 반올림한다." 생각하는 순서 그대로 코드가 됩니다.

R 4.1+의 네이티브 파이프 |>

R 4.1부터 추가 패키지 없이 쓸 수 있는 파이프입니다.

c(-4, 9, 16, -1, 25) |>
  abs() |>
  sqrt() |>
  mean() |>
  round(2)
# 3.42

%>%|> 모두 같은 일을 합니다. 어느 쪽을 써도 됩니다. 팀이나 프로젝트의 규칙을 따르면 됩니다.

%>%와 |>의 차이

두 파이프는 대부분의 상황에서 동일하게 동작합니다. 차이가 나는 경우를 알아두면 실수를 줄일 수 있습니다.

플레이스홀더

%>%.으로 이전 결과를 원하는 위치에 놓을 수 있습니다.

# 두 번째 인자로 넘기고 싶을 때
c(1, 2, 3) %>% paste("번", ., sep = "")
# "번 1" "번 2" "번 3"

# 여러 번 쓸 수도 있음
"hello" %>% gsub("l", "r", .)   # "herro"

|>는 R 4.2부터 _를 플레이스홀더로 지원합니다. 단, 이름 있는 인자에만 씁니다.

# |>의 _는 이름 있는 인자에만 사용 가능
mtcars |> lm(mpg ~ cyl, data = _)  # R 4.2+

성능

|>는 함수 호출로 변환되는 방식이 더 단순해서 미세하게 빠릅니다. 대용량 데이터를 다룰 때는 체감 차이가 있을 수 있습니다.

람다 함수

# %>% — 중괄호로 감싼 표현식
c(1, 2, 3) %>% { . * 2 + 1 }

# |> — 익명 함수를 명시해야 함
c(1, 2, 3) |> (\(x) x * 2 + 1)()

파이프라인 사고방식

파이프는 단순한 문법 설탕이 아닙니다. 데이터를 변환하는 파이프라인으로 분석을 설계하는 사고방식입니다.

중간 변수를 만들지 않고 처리 흐름을 표현합니다.

# 중간 변수를 만드는 방식
data1 <- c(88, 72, 95, 61, 83, 77, 90, 55, 68, 94)
data2 <- data1[data1 >= 70]      # 70점 이상만
data3 <- sort(data2, decreasing = TRUE)  # 내림차순 정렬
data4 <- head(data3, 3)          # 상위 3개
result <- mean(data4)            # 평균

# 파이프를 쓰는 방식
result <- c(88, 72, 95, 61, 83, 77, 90, 55, 68, 94) |>
  (\(x) x[x >= 70])() |>
  sort(decreasing = TRUE) |>
  head(3) |>
  mean()

중간 변수가 없으면 불필요한 이름을 고민할 필요가 없고, 처리 순서가 코드에 그대로 드러납니다.

체이닝 패턴

데이터 분석에서 파이프가 빛나는 순간은 dplyr과 함께 쓸 때입니다. PART 03에서 자세히 다루지만, 맛보기로 확인해봅니다.

library(dplyr)

# 학생 성적 데이터
students <- data.frame(
  name   = c("Alice", "Bob", "Charlie", "Dana", "Eve"),
  score  = c(85, 62, 91, 78, 55),
  grade  = c("2학년", "1학년", "3학년", "2학년", "1학년")
)

# 2학년 중 80점 이상인 학생을 점수 내림차순으로 정렬
result <- students %>%
  filter(grade == "2학년") %>%
  filter(score >= 80) %>%
  arrange(desc(score)) %>%
  select(name, score)

result
#    name score
# 1 Alice    85

각 단계가 무엇을 하는지 한눈에 읽힙니다. "학생 데이터에서 2학년을 걸러내고, 80점 이상만 남기고, 점수로 내림차순 정렬하고, 이름과 점수만 선택한다."

파이프를 쓸 때와 쓰지 않을 때

파이프가 항상 좋은 것은 아닙니다.

# 단계가 하나라면 굳이 파이프를 쓸 필요 없음
mean(scores)             # 충분히 간결
scores |> mean()         # 오히려 장황
# 단계가 여러 개라면 파이프가 훨씬 읽기 좋음
scores |>
  na.omit() |>
  log() |>
  mean() |>
  exp()

파이프를 쓰면 좋은 상황.

  • 3단계 이상의 변환이 연속될 때.
  • 중간 결과를 따로 저장할 필요가 없을 때.
  • 데이터의 흐름을 설명하고 싶을 때.

파이프를 쓰지 않는 것이 나은 상황.

  • 단계가 하나나 둘일 때.
  • 중간 결과를 디버깅하거나 재사용할 때.
  • 파이프가 코드를 오히려 읽기 어렵게 만들 때.

RStudio 단축키

파이프를 매번 타이핑하는 것은 번거롭습니다. RStudio에서 단축키를 쓰면 편리합니다.

파이프 Windows / Linux macOS
%>% Ctrl + Shift + M Cmd + Shift + M
` >` 설정에서 네이티브 파이프 선택 후 동일 단축키

|>를 기본 단축키로 쓰려면 RStudio에서 Tools > Global Options > Code > Use native pipe operator를 체크합니다.

왜 파이프가 중요한가

PART 03에서 배울 tidyverse 전체가 파이프를 기반으로 설계되었습니다. dplyr, tidyr, stringr, forcats 모두 첫 번째 인자로 데이터를 받고 변환된 데이터를 반환합니다. 파이프와 완벽하게 어울리는 구조입니다.

파이프를 자연스럽게 쓸 수 있다면 tidyverse 코드를 읽고 쓰는 것이 한결 쉬워집니다. 지금 익혀두면 이후 파트에서 분석 코드가 훨씬 편하게 느껴질 것입니다.