본 도구에서 사용하는 오픈소스 라이브러리

본 도구의 코드에는 1개의 오픈소스 라이브러리가 포함되어 있습니다.

R 치트시트 — 간결 레퍼런스

R 4.4 문법, 자료 구조와 가장 자주 쓰는 base R / tidyverse 치트시트로, 일상 시나리오의 약 80%를 다룹니다.

R

R R 4.4

R (GNU R / 대화형 인터프리터) · 함수형 · 벡터화 · OO (S3 / S4 / R6) · 동적

추천 학습 경로

먼저 Rscript 실행과 할당(<-)을 익히고 → 벡터, data.frame 등 자료 구조를 익히고 → if/for/apply 계열로 흐름을 작성하고 → 함수, 클로저와 파이프 |>를 배우고 → apply 집합 연산을 깊이 파고 → S3/R6 객체 지향과 오류 처리를 이해한 뒤 → 필요에 따라 파일 I/O, 병렬, 네트워크와 정규식을 배우세요. FAQ 절은 나중에 돌아와 함정을 피하는 데 좋습니다. 패키지 관리와 renv는 빌드 장을 참고하세요.

1.Hello World와 실행 환경

Rscript 실행, REPL 상호작용, 출력, 그리고 도움말.

최소 프로그램

R은 표현식 단위로 평가한다. print는 객체를 출력하고, cat은 직접 출력한다. 스크립트는 Rscript로 실행할 수 있다.

1
2
3
4
5
print('Hello, world!') # 콘솔에 출력
cat('Hello, world!\n') # 직접 출력, 따옴표 없음
# hello.R 로 저장 후 실행:
# $ Rscript hello.R
# 또는 R / RStudio 에서 source('hello.R')

대입과 출력

대입은 <- 또는 =를 쓴다. print는 객체 구조를 보여주고, cat은 이어 붙여 출력하기에 적합하다. message는 stderr에 쓴다.

1
2
3
4
5
x <- 42 # 대입(권장 <-)
y = 42 # = 도 유효하나, 스타일상 자제
print(x) # [1] 42 로 표시
cat('x =', x, '\n') # 연결 출력:x = 42
message('这是提示') # stderr 에 기록

Rscript 실행

Rscript는 비대화형 방식으로 스크립트를 실행해 일괄 처리와 명령행에 적합하다. source는 세션 내에서 스크립트 파일을 실행한다.

1
2
3
4
5
6
7
8
9
10
# 스크립트 hello.R:
# print('hello')
# 스크립트 실행:
# $ Rscript hello.R
# 인자와 함께 실행:
# $ Rscript hello.R a b
# 세션 안에서 파일 실행:
source('hello.R')
# 문자열을 직접 실행:
eval(parse(text = '1 + 1'))

명령행 인수

commandArgs(trailingOnly = TRUE)는 스크립트 뒤의 명령행 인수를 가져온다. 인수는 모두 문자열이다.

1
2
3
4
5
6
7
8
9
# 실행:Rscript app.R a b c
args <- commandArgs(trailingOnly = TRUE)
print(args) # [1] "a" "b" "c"
# 스크립트명 포함 여부(FALSE 면 args[1] 이 스크립트 경로):
commandArgs() # Rscript 와 스크립트 경로 포함
# args[1] 을 입력 경로로 사용:
if (length(args) > 0) {
input <- args[1]
}

출력

print는 객체와 구조를 표시하고, cat은 따옴표 없이 이어 붙이며, sprintf는 형식을 지정한다. invisible은 자동 출력을 억제한다.

1
2
3
4
5
6
7
8
x <- c(1, 2, 3)
print(x) # [1] 1 2 3
cat(x, sep = ', ', '\n') # 1, 2, 3
sprintf('Pi = %.2f', pi) # "Pi = 3.14"
# 식은 자동 출력(최상위에서만):
1 + 1 # [1] 2
# 함수 내 마지막 식은 자동 반환 후 출력:
invisible(1) # 출력 안 함

도움말

?는 도움말을 열고, ??는 전체 텍스트 검색이다. args는 함수의 형식 인수를, example은 예제를 실행한다.

1
2
3
4
5
6
7
8
?mean # mean 의 헬프 페이지
??regression # 전체 텍스트 검색
args(mean) # 형식 인자 목록
example(mean) # 내장 예제 실행
apropos('dist') # dist 포함 객체 검색
help.search('聚类') # 주제로 검색
# 소스 확인:
mean # 함수 본문 표시

패키지 로드

library는 패키지를 검색 경로에 로드하고, require는 논리값을 반환한다. ::는 패키지 내부 함수를 명시적으로 가져온다.

1
2
3
4
5
6
7
8
9
10
11
library(dplyr) # 패키지 로드
library('ggplot2') # 문자열 형태도 가능
if (!requireNamespace('pkg', quietly = TRUE)) {
install.packages('pkg')
}
# 명시적으로 네임스페이스 지정 호출:
dplyr::select(df, x)
# 로드된 것 확인:
search()
# 설치된 패키지 목록:
rownames(installed.packages())

스크립트 실행

source는 현재 세션에서 스크립트 파일을 실행하며, 함수와 변수를 정의할 수 있다. echo는 실행 과정을 표시한다.

1
2
3
4
5
6
7
source('utils.R') # utils.R 실행
source('utils.R', encoding = 'UTF-8')
# 각 줄과 결과 표시:
source('demo.R', echo = TRUE)
# 자주 쓰임:자체 함수 집합 로드:
# utils.R 안에서 f <- function(x) x * 2 정의
f(5) # 10

2.변수와 상수

대입, 기본 타입, NA/NULL, 그리고 타입 변환.

대입 연산자

<-가 관용적 대입이다. =도 가능하다. <<-는 함수 안에서 바깥쪽 변수를 쓴다. 전역 대입은 assign을 쓴다.

1
2
3
4
5
6
7
8
9
10
11
x <- 5 # 권장
y = 5 # 이것도 유효
x <- y <- z <- 1 # 연쇄 대입, 모두 1
# 함수 안에서 바깥 변수 변경:
f <- function() {
g <<- 99 # 전역 환경에 기록
}
# 이름으로 대입:
assign('name', 'Rex')
name # "Rex"
# = 와의 차이:= 는 중첩 함수 환경에 들어가지 않음

기본 타입

원자 벡터에는 numeric, integer, character, logical이 있다. 정수는 L 접미사를 쓴다. typeof는 저장 타입을 본다.

1
2
3
4
5
6
7
8
9
10
x <- 3.14 # numeric (double)
i <- 1L # integer(L 접미사)
s <- 'hi' # character
b <- TRUE # logical
typeof(x) # "double"
typeof(i) # "integer"
# 길이:모든 스칼라는 길이 1 의 벡터:
length(s) # 1
# 복합형:
typeof(1:5) # "integer"

NA와 NULL

NA는 결측값을, NULL은 빈 객체를 나타낸다. is.na / is.null로 판정한다. NaN은 숫자 아님, Inf는 무한이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
v <- c(1, NA, 3)
is.na(v) # FALSE TRUE FALSE
any(is.na(v)) # TRUE
# NULL 은 객체가 없음을 뜻함:
x <- NULL
is.null(x) # TRUE
# 수치의 특수값:
0 / 0 # NaN
1 / 0 # Inf
is.nan(NaN) # TRUE
is.finite(Inf) # FALSE
# 통계 시 NA 무시:
mean(c(1, NA), na.rm = TRUE) # 1

타입 변환

as.*로 명시적 변환을 한다. c()로 서로 다른 타입을 이어 붙이면 자동으로 가장 일반적인 타입으로 변환된다. 판정은 is.*를 쓴다.

1
2
3
4
5
6
7
8
9
10
11
12
as.numeric('42') # 42
as.character(42) # "42"
as.logical(1) # TRUE
as.factor(c('a', 'b')) # 팩터
# 혼합 연결은 자동 변환:
c(1, 'a') # "1" "a"(character 로 변환)
c(1, TRUE) # 1 1(numeric 로 변환)
# 변환 실패는 NA:
as.numeric('abc') # NA(경고 있음)
# 안전한 변환 + 확인:
x <- suppressWarnings(as.numeric('abc'))
is.na(x) # TRUE

벡터 생성

c는 이어 붙이기, 콜론(:)은 수열 생성, seq는 단계 크기 지정, rep는 반복이다. names로 원소에 이름을 부여한다.

1
2
3
4
5
6
7
8
9
10
c(1, 2, 3) # 연결
1:10 # 1 2 ... 10
seq(0, 1, by = 0.25) # 0 0.25 ... 1
seq(1, 10, length.out = 5) # 등간격 5 점
rep(1, 3) # 1 1 1
rep(c('a', 'b'), each = 2) # a a b b
# 이름 있는 요소:
x <- c(a = 1, b = 2)
names(x) # "a" "b"
x['a'] # 1

명명 규칙

변수명에는 문자, 숫자, 점, 밑줄을 쓸 수 있으며 숫자로 시작할 수 없다. 점은 R에서 연산자가 아니다.

1
2
3
4
5
6
7
8
9
10
11
12
my_var <- 1 # 언더스코어
my.var <- 2 # 점은 유효
.var <- 3 # 점으로 시작
# 숫자로 시작할 수 없음:
# 1var <- 1 # 구문 오류
# 예약어는 변수명으로 불가:
# if <- 1 # 오류
# 캐멀/스네이크 모두 가능, 일관성 유지:
totalCount <- 10
total_count <- 20
# R 에서 점은 일반 문자:
a.b <- 1 # 유효

변수 관리

ls는 변수를 나열하고, rm은 삭제한다. exists는 존재 여부를 판정한다. rm(list = ls())는 환경을 비운다.

1
2
3
4
5
6
7
8
9
10
11
x <- 1; y <- 2
ls() # "x" "y"
exists('x') # TRUE
rm(x) # x 삭제
rm(list = ls()) # 현재 환경 비우기
# 특정 패턴만 삭제:
rm(list = ls(pattern = '^tmp'))
# 객체 구조 확인:
str(y) # num 2
# 환경 정보:
ls(all.names = TRUE) # 숨은 객체 포함

내장 상수

R의 자주 쓰는 내장 상수: pi, letters, month.name, 그리고 .Machine의 플랫폼 세부 정보.

1
2
3
4
5
6
7
8
9
10
11
pi # 3.141593
letters # a b ... z
LETTERS # A B ... Z
letters[1:3] # "a" "b" "c"
month.name # January ...
month.abb # Jan Feb ...
# 기계 정밀도 등:
.Machine$double.eps # 2.22e-16
.Machine$integer.max # 2147483647
# 기타 내장:
date() # 현재 날짜/시간 문자열

스코프 기초

R의 어휘적 스코프: 함수 내부에서 변수를 단계별로 바깥으로 찾는다. 전역과 로컬의 같은 이름은 가려진다. get은 지정한 환경에서 값을 가져온다.

1
2
3
4
5
6
7
8
9
10
11
12
13
x <- 'global'
f <- function() {
x <- 'local' # 전역 가림
x
}
f() # "local"
# 대입하지 않으면 전역 참조:
g <- function() x
g() # "global"
# 명시적으로 환경에서 조회:
get('x', envir = .GlobalEnv)
# 환경 체인:
parent.env(globalenv())

3.데이터 타입과 구조

벡터, 팩터, 행렬, 리스트, 데이터 프레임, 그리고 tibble, R의 데이터 구조 체계.

벡터

원자 벡터는 동질이며 1차원으로, R의 가장 기본 구조다. c()로 생성하며 인덱스는 1부터 시작한다.

1
2
3
4
5
6
7
8
9
10
11
12
v <- c(1, 2, 3)
v[1] # 1(인덱스는 1 부터)
v[c(1, 3)] # 1 3
v[-1] # 첫 번째 제거
v > 1 # FALSE TRUE TRUE
v[v > 1] # 논리 필터:2 3
length(v) # 3
# 정수열:
1:5
# 이름 있는 접근:
x <- c(a = 1, b = 2)
x['a']

팩터

팩터는 범주형 변수를 저장하며, levels로 범주를 고정한다. 순서형 팩터는 순서를 나타낸다. as.factor / factor로 생성한다.

1
2
3
4
5
6
7
8
9
10
11
f <- factor(c('低', '中', '高'))
levels(f) # "低" "中" "高"
table(f) # 카운트
# 카테고리 순서 고정:
f2 <- factor(c('低', '高'),
levels = c('低', '中', '高'))
# 순서 있는 팩터:
of <- ordered(c('低', '高'),
levels = c('低', '中', '高'))
of[2] > of[1] # TRUE
# 수치를 팩터로 변환 후 다시 값으로 취할 때 주의(FAQ 참조):

행렬

matrix는 2차원 동질이다. byrow로 채우기 방향을 제어한다. 행명/열명과 차원명은 dimnames.

1
2
3
4
5
6
7
8
9
10
11
12
m <- matrix(1:6, nrow = 2, ncol = 3)
m # 2 행 3 열
m[1, 2] # 1 행 2 열
m[1, ] # 1 행
# 행 방향으로 채우기:
matrix(1:6, nrow = 2, byrow = TRUE)
# 행/열 연산:
rowSums(m); colMeans(m)
# 차원:
dim(m) # 2 3
# 이름 붙이기:
dimnames(m) <- list(c('r1', 'r2'), c('a', 'b', 'c'))

배열

array는 임의 차원의 동질 구조다. dim은 각 차원의 길이를 지정한다. 다차원 텐서에 적합하다.

1
2
3
4
5
6
7
8
9
10
11
a <- array(1:24, dim = c(2, 3, 4))
dim(a) # 2 3 4
a[1, 2, 3] # 슬라이스로 취득
# 인덱스 후 차원이 낮아짐:
a[1, , ] # 3x4 행렬
# 차원 방향으로 합산:
apply(a, 3, sum) # 각 3 번째 차원 슬라이스의 합
# 차원 변경:
dim(a) <- c(6, 4) # 6x4 로 재구성
# matrix 와의 관계:
is.matrix(a) # TRUE(dim 이 2 일 때)

리스트

list는 이질적이며 중첩 가능하다. $와 [[는 원소를 가져오고, [는 부분 리스트를 반환한다. length는 원소 개수를 센다.

1
2
3
4
5
6
7
8
9
10
11
l <- list(name = 'Rex', age = 5, tags = c('a', 'b'))
l$name # "Rex"
l[[2]] # 5
l[1] # 하위 리스트(name 포함)
length(l) # 3
names(l) # "name" "age" "tags"
# 중첩 리스트:
l2 <- list(a = list(x = 1), b = 2)
l2$a$x # 1
# 재귀적으로 평탄화:
unlist(l2) # 벡터로 평탄화

데이터 프레임

data.frame은 표 형식 데이터로, 열은 이질적일 수 있다. 기본값은 stringsAsFactors=FALSE이다(R 4.0+). str/head로 본다.

1
2
3
4
5
6
7
8
9
10
11
12
13
df <- data.frame(
name = c('alice', 'bob'),
age = c(30, 25),
admin = c(TRUE, FALSE)
)
df$age # 열 취득(벡터)
df[2, ] # 2 행
df[['name']] # 열 취득(정확한 이름)
nrow(df); ncol(df) # 2 3
str(df) # 구조 개요
head(df, 2) # 앞 2 행
# 열 추가:
df$score <- c(88, 92)

tibble

tibble은 tidyverse의 현대적 데이터 프레임이다: 지연 계산 열, 친화적 출력, 문자열을 팩터로 변환하지 않음.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# library(tibble)
tb <- tibble(
name = c('a', 'b'),
value = c(1, 2)
)
tb # 보기 좋고, 타입 표시
# 열은 앞선 열을 참조 가능:
tb2 <- tibble(x = 1:3, y = x * 2)
# 팩터로 만들지 않음:
class(tb$name) # character
# data.frame 과 상호 변환:
df <- as.data.frame(tb)
tb3 <- as_tibble(df)
# 추출:
tb$value; tb[['value']]

속성

attributes는 메타 정보를 담는다: names, dim, class 등. attr은 개별적으로 읽기/설정한다. structure는 한 번에 구성한다.

1
2
3
4
5
6
7
8
9
10
11
12
x <- 1:3
attr(x, 'unit') <- 'cm' # 사용자 정의 속성
attributes(x) # 전체 나열
attr(x, 'unit') # "cm"
# structure 로 한 번에 구성:
y <- structure(1:3, unit = 'cm')
# 자주 쓰는 내장 속성:
dim(m); names(df); class(x)
# 속성 제거:
attributes(x) <- NULL
# 객체 클래스 확인:
class(c(1, 2)) # "numeric"

타입 판정

is.*로 타입을 판정하고, class/typeof/mode로 타입을 본다. as.*로 변환한다. is.data.frame 등은 분기에 쓰인다.

1
2
3
4
5
6
7
8
9
10
11
12
is.numeric(1) # TRUE
is.integer(1L) # TRUE
is.character('a') # TRUE
is.logical(TRUE) # TRUE
is.list(list()) # TRUE
is.data.frame(df) # TRUE
is.matrix(m) # TRUE
# class vs typeof:
class(data.frame()) # "data.frame"
typeof(list()) # "list"
# null / NA 판정:
is.null(NULL); is.na(NA)

4.참조와 객체 의미

R에 날 포인터는 없다: copy-on-modify 값 의미와 environment 참조 의미.

쓰기 시 복사

R은 복사 시 수정(copy-on-modify)을 채택한다: 대입은 데이터를 공유하며, 수정될 때에만 실제로 복사된다. 큰 객체의 별칭 대입은 거의 비용이 없다.

1
2
3
4
5
6
7
8
9
10
11
x <- 1:1e6
y <- x # 복사 없이, 데이터 공유
# y 를 변경하면 복사 발생:
y[1] <- 99
# 이때 x 는 영향 없음:
x[1] # 1
# tracemem 으로 복사 시점 관찰:
tracemem(x)
y2 <- x # 공유, 복사 없음
y2[1] <- 1 # 여기서 복사 발생
untracemem(x)

tracemem 복사 추적

tracemem은 객체를 표시하며, R이 그 객체를 실제로 복사할 때 주소 변화를 출력한다. 예기치 않은 복사와 메모리 비용을 진단하는 데 쓰인다.

1
2
3
4
5
6
7
8
9
10
x <- 1:1e6
tracemem(x) # 추적 시작
y <- x # 출력 없음:복사 없음
y[1] <- 0 # tracemem: 복사 출력됨
untracemem(x) # 추적 중지
# 큰 데이터프레임의 열을 바꾸면 복사:
df <- data.frame(a = 1:1e5, b = rnorm(1e5))
tracemem(df)
df$a <- df$a + 1 # 프레임 전체가 복사됨
untracemem(df)

환경 참조

environment는 R의 참조 객체다: 함수에 전달해도 복사되지 않으며, 함수 안에서의 수정이 바로 반영된다. R의 '포인터'에 해당한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
e <- new.env() # 환경 새로 생성
e$x <- 1
e[['y']] <- 2
# 함수 안에서 환경 변경(<<- 불필요):
inc <- function(env) {
env$x <- env$x + 1
}
inc(e)
e$x # 2(참조 시맨틱 유효)
# 읽기:
get('x', envir = e)
ls(e) # "x" "y"
# 환경은 해시 테이블이기도 함:
e$name <- 'Rex'

가변 상태 컨테이너

환경을 가변 상태 컨테이너로 사용한다: 카운터, 캐시, 누적기. 전역 변수 오염을 피하고 상태를 명시적으로 전달한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
counter <- function() {
e <- new.env()
e$n <- 0
function() {
e$n <- e$n + 1 # 클로저가 포착한 환경을 변경
e$n
}
}
next_num <- counter()
next_num() # 1
next_num() # 2
# 캐시:key 로 결과 저장
cache <- new.env(hash = TRUE)
cache$key <- list(result = 42)

얕은 복사와 깊은 복사

리스트의 대입은 얕은 복사다: 하위 객체를 공유하며, 중첩 원소를 바꾸면 그 층이 복사된다. 깊은 복사는 명시적으로 구현해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
l1 <- list(x = 1:3, y = list(a = 1))
l2 <- l1 # 얕은 복사, 하위 객체 공유
# l1 최상위를 바꾸면 그 층에서 복사:
l1$x[1] <- 99 # x 는 복사, y 는 공유 유지
# 깊은 복사 예(재귀 복사):
deep_copy <- function(obj) {
if (is.list(obj)) lapply(obj, deep_copy)
else obj
}
l3 <- deep_copy(l1)
# data.table 에는 명시적 copy() 가 있음:
# dt2 <- copy(dt)

참조 클래스 R6

R6는 참조 의미의 클래스다: 객체는 참조로 전달되며 메서드가 복사본이 아닌 원본을 수정한다. 상태 관리에 적합하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# library(R6)
Counter <- R6Class('Counter',
public = list(
n = 0,
increment = function() self$n <- self$n + 1
)
)
c1 <- Counter$new()
c2 <- c1 # 참조, 같은 객체를 가리킴
c2$increment()
c1$n # 1(c1 도 변경됨)
# R5 / setRefClass 도 유사한 참조 시맨틱:
# setRefClass 로 field + methods 정의
# S3/S4 와 달리 R6 는 변경 시 copy-on-modify 없음

data.table 참조

data.table은 :=로 제자리 수정한다(참조 의미). data.frame은 복사 의미다. setDT는 제자리로 data.table로 변환한다.

1
2
3
4
5
6
7
8
9
10
11
12
# library(data.table)
dt <- data.table(a = 1:3, b = 4:6)
dt[, c := a + b] # 그 자리에서 열 c 추가, 복사 없음
dt
# data.frame 의 변경은 복사:
df <- as.data.frame(dt)
df$d <- 1 # 복사본 생성
# 그 자리에서 설정/삭제:
dt[, d := NULL] # 열 삭제
setDT(df) # 그 자리에서 data.table 로 변환
# 명시적 복사:
dt2 <- copy(dt) # data.table::copy

객체 크기

object.size는 단일 객체의 점유를, gc는 전체 메모리를 본다. lobstr::obj_size는 공유량을 계산할 수 있다.

1
2
3
4
5
6
7
8
9
object.size(1:1e6) # ~8 MB
format(object.size(1:1e6), units = 'MB')
# 메모리 전체:
gc() # 사용/피크(MB)
memory.size() # Windows 전용
# 공유 객체의 실제 점유:
# lobstr::obj_size(x, y) # 중복 부분은 한 번만 계산
# 대규모 데이터:
# data.table 로 복사 줄이기 가능

명시적 복사

독립 복사본이 필요할 때 명시적으로 복사한다: 리스트는 lapply로 재귀 복사하고, data.table은 copy를, 벡터는 []를 쓴다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 리스트의 깊은 복사:
l <- list(a = 1:3, b = list(x = 1))
l_copy <- lapply(l, function(x) {
if (is.list(x)) lapply(x, function(y) y)
else x
})
# data.table 복사본:
# dt2 <- copy(dt)
# 벡터 복사:
v <- c(1, 2, 3)
v2 <- v[] # 명시적 복사
# v2 를 바꿔도 v 에 영향 없음:
v2[1] <- 99
v[1] # 1
# 공유 여부 확인:
# lobstr::ref(v, v2)

5.흐름 제어

if / else, 벡터화된 ifelse, for / while, switch, 그리고 논리 연산.

if / else

if 조건은 길이가 1이어야 한다. 1이 아니면 첫 원소를 취하고 경고를 낸다. else는 if와 같은 줄에 중괄호가 일치해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
x <- 5
if (x > 0) {
print('正数')
} else if (x == 0) {
print('零')
} else {
print('负数')
}
# 조건은 길이 1 이어야 안전:
if (c(TRUE, FALSE)) print('x') # 경고 후 첫 요소 사용
# 대입식:
y <- if (x > 0) 'pos' else 'neg'

ifelse 벡터화

ifelse는 벡터의 원소별 조건을 판정해 조건과 같은 길이의 결과를 반환한다. 일괄 치환에 적합하다.

1
2
3
4
5
6
7
8
9
10
11
x <- c(-1, 0, 1, 2)
ifelse(x > 0, 'pos', 'non-pos')
# 중첩:
ifelse(x > 0, 'pos',
ifelse(x == 0, 'zero', 'neg'))
# ifelse 는 반환 타입을 강제함에 주의:
ifelse(x > 0, 1L, 0) # 모두 double 로 변환
# NA 유지:
ifelse(x > 0, 'pos', NA_character_)
# tidyverse 대안:
# dplyr::case_when(x > 0 ~ 'pos', TRUE ~ 'neg')

for 루프

for는 벡터 또는 수열을 순회한다. seq_along은 인덱스로 순회하며, 1:length의 빈 수열 함정을 피한다.

1
2
3
4
5
6
7
8
9
10
11
for (i in 1:5) print(i)
# 벡터 요소 반복:
for (ch in c('a', 'b')) print(ch)
# 인덱스로 반복:
x <- c(10, 20, 30)
for (i in seq_along(x)) {
x[i] <- x[i] * 2
}
x # 20 40 60
# x 가 비어 있을 때 1:length(x) 는 오류가 되므로 회피
# 루프보다 벡터화 우선

while과 repeat

while은 조건을 먼저 판정한 뒤 실행한다. repeat는 무조건 반복하며 break로 빠져나온다. 둘 모두 무한 루프에 주의한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
n <- 0
while (n < 3) {
n <- n + 1
}
n # 3
# repeat 은 최소 1 회 실행:
total <- 0
repeat {
total <- total + 1
if (total >= 5) break
}
total # 5
# 무한 루프 방지:최대 횟수 설정
# 조건이 영원히 참이 아니면 break/return 사용

next와 break

next는 다음 반복으로 넘어가고, break는 루프를 빠져나간다. 건너뛰기와 조기 종료에 적합하다.

1
2
3
4
5
6
7
8
9
10
11
12
for (i in 1:10) {
if (i %% 2 == 0) next # 짝수 건너뜀
if (i > 7) break # 7 보다 크면 종료
print(i) # 1 3 5 7
}
# 중첩 루프에서 break 는 안쪽만 빠져나감:
for (i in 1:3) {
for (j in 1:3) {
if (j == 2) break
cat(i, j, '\n')
}
}

switch

switch는 위치 또는 이름으로 분배한다. 숫자는 n번째를 취하고, 문자는 이름이 일치하는 분기로 가며, 매칭되지 않으면 NULL을 반환한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
switch(2, 'a', 'b', 'c') # "b"(위치 지정)
op <- 'add'
switch(op,
add = 1 + 1, # 2
mul = 2 * 3,
'未知'
)
# 기본값 없이 매치되지 않으면 NULL 반환:
switch('nope', add = 1)
# 숫자 + 누락은 NULL 반환:
switch(5, 'a', 'b')
# match.arg 로 인자 검증:
# match.arg(choice, c('a', 'b'))

벡터화가 루프보다 우월

R은 벡터화로 명시적 루프를 대체합니다: 벡터 전체를 한 번에 연산하여 더 빠르고 간결합니다. 루프는 벡터화할 수 없는 경우에만 사용하세요.

1
2
3
4
5
6
7
8
9
10
11
x <- 1:1e6
y <- x * 2 # 벡터 전체 곱, 빠름
z <- x^2 + sqrt(x)
# 동등한 루프(느림, 비권장):
for (i in seq_along(x)) x[i] * 2
# 자주 쓰는 벡터화 함수:
sum(x); mean(x); cumsum(x)
which(x > 5) # 조건을 만족하는 인덱스
x[x > 5] # 값 필터링
# 비교의 벡터화:
any(x > 5); all(x > 0)

논리 연산

& |는 벡터화된 논리 연산이고, && ||는 첫 번째 요소만 평가합니다(단락 평가). any/all은 결과를 요약합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
x <- c(TRUE, FALSE, TRUE)
y <- c(FALSE, TRUE, TRUE)
x & y # FALSE FALSE TRUE
x | y # TRUE TRUE TRUE
!x # 반전
# 단락 버전(단일 값):
TRUE && FALSE # FALSE
TRUE || stop('不执行') # 단락, 오류 없음
# 집약:
any(x); all(x)
# 비교 연결은 일일이 작성:
# x > 1 && x < 5
# && || 는 첫 요소만 취함에 주의

인덱스 순회

seq_along / seq_len은 인덱스를 생성하고, rev는 역순입니다. which는 위치를 가져옵니다. split은 그룹별 루프입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
x <- c(3, 1, 4, 1, 5)
seq_along(x) # 1 2 3 4 5
seq_len(3) # 1 2 3
# 역순 반복:
for (i in rev(seq_along(x))) print(x[i])
# which:
which(x == 1) # 2 4
which.max(x) # 5
# 그룹별 반복:
grp <- split(x, x > 2)
lapply(grp, sum)
# 요소별 출력:
# sapply(x, function(v) ...)

6.함수와 클로저

함수 정의, 매개변수, 지연 평가, 가변 인자, 클로저와 파이프.

함수 정의

function으로 함수를 정의하고, 함수 본문의 마지막 표현식이 반환값입니다. 익명 함수는 매개변수에 직접 사용됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
square <- function(x) {
x * x
}
square(5) # 25
# 단일 식은 단축 가능:
add1 <- function(x) x + 1
add1(2) # 3
# 익명 함수:
(function(x) x * 2)(10) # 20
# 함수는 객체이므로 대입 가능:
f <- add1
f(1) # 2
# 함수 본문 확인:
body(square)

매개변수와 기본값

함수 매개변수에는 기본값을 설정할 수 있습니다. 호출 시 매개변수 이름을 생략하고 위치로 전달하거나, 이름으로 전달할 수 있습니다. missing은 제공 여부를 판단합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
greet <- function(name, greeting = '你好') {
paste(greeting, name)
}
greet('张三') # 你好 张三
greet('张三', '早上好')
greet(greeting = '嗨', name = '李四')
# 인자 생략 여부 확인:
show <- function(x) {
if (missing(x)) return('未提供')
x
}
show() # 未提供
# ... 투과 인자:
f <- function(x, ...) paste(x, ...)
f('a', 'b', 'c')

지연 평가

R 매개변수는 지연 평가됩니다: 사용될 때만 평가되며, 사용되지 않으면 오류가 발생하지 않습니다. force는 강제로 미리 평가합니다. 이는 클로저에서 매우 중요합니다.

1
2
3
4
5
6
7
8
9
10
11
f <- function(a, b) a
f(1, stop('这行不执行')) # 1、b 는 미사용
# 강제 평가:
g <- function(a, b) {
force(b) # 즉시 b 평가
a
}
g(1, stop('会报错')) # 오류 발생
# 지연 평가 + 기본 인자가 앞 인자 참조:
h <- function(x, y = x * 2) y
h(5) # 10

가변 인자

...는 임의의 여러 매개변수를 캡처합니다. list(...)로 수집하고, do.call로 리스트를 사용해 동적으로 함수를 호출합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
sum_all <- function(...) sum(...)
sum_all(1, 2, 3) # 6
# 리스트로 수집:
capture <- function(...) list(...)
capture(1, 'a', TRUE)
# do.call:동적으로 인자 전달
args <- list(x = 1:3, y = 1:3)
do.call(pmax, args)
# 벡터 요소를 분해해 전달:
vals <- c(1, 2, 3)
do.call(sum, as.list(vals)) # 6
# ... 를 다른 함수에 전달:
wrap <- function(...) plot(...)

반환값

함수는 마지막 표현식을 반환합니다. return은 조기 반환합니다. invisible은 반환값을 자동으로 출력하지 않습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
f <- function(x) x * 2 # 마지막 식이 반환값
f(3) # 6
# return 으로 조기 종료:
g <- function(x) {
if (x < 0) return('负数')
x * 2
}
g(-1) # 负数
# 여러 값은 리스트로 반환:
h <- function(x) {
list(sq = x^2, rt = sqrt(x))
}
h(4) # 리스트에 sq rt 포함
# 출력하지 않는 반환값:
invisible(42)

클로저

함수는 생성된 환경을 캡처하여 비공개 상태를 보유할 수 있습니다. 팩토리 함수는 상태를 가진 새로운 함수를 생성합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
make_adder <- function(n) {
function(x) x + n # 클로저가 n 을 포착
}
add5 <- make_adder(5)
add5(3) # 8
# 카운터 클로저:
counter <- local({
n <- 0
function() {
n <<- n + 1 # 환경 안의 n 변경
n
}
})
counter(); counter() # 1 2
# 환경은 클로저의 매개체:
environment(add5)

익명 함수

익명 함수는 함수 리터럴이며, apply 계열과 purrr에서 자주 사용됩니다. R 4.1+는 \(x) 축약을 지원합니다.

1
2
3
4
5
6
7
8
9
10
11
lapply(1:3, function(x) x^2) # 1 4 9
sapply(1:3, function(x) x * 2)
# \\(x) 축약(R >= 4.1):
lapply(1:3, \(x) x^2)
# 직접 호출:
(function(x) x + 1)(5) # 6
# purrr 파이프 스타일:
# purrr::map(1:3, ~ .x * 2)
# 익명 함수를 정렬에 쓰는 예:
sort(c(3, 1, 2))
sort(c('b', 'a'), decreasing = TRUE)

고차 함수

함수는 매개변수로 다른 함수에 전달하거나 함수를 반환할 수 있습니다. Map / Reduce / Filter 등 내장 고차 함수.

1
2
3
4
5
6
7
8
9
10
11
12
13
Filter(is.numeric, list(1, 'a', 2))
Reduce(`+`, 1:5) # 15
Map(function(x, y) x + y, 1:3, 4:6)
# 함수를 반환하는 함수(커링):
curry_add <- function(a) {
function(b) a + b
}
curry_add(10)(5) # 15
# 흔한 패턴:데이터프레임 각 열에 함수 적용
sapply(df, mean, na.rm = TRUE)
# 직접 만든 고차 함수:
apply_twice <- function(f, x) f(f(x))
apply_twice(function(x) x + 1, 5) # 7

파이프

|>는 네이티브 파이프로 왼쪽 결과를 오른쪽 함수의 첫 번째 매개변수로 전달하여 중첩 호출을 선형으로 만듭니다. %>%는 magrittr 버전입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
x <- 1:10
x |> sum() # 55
# 파이프 연결:
1:10 |> mean() |> round(2)
# 첫 인자가 아닌 곳으로 보낼 때는 플레이스홀더:
1:10 |> paste(collapse = '-') # 오류 예시
# 네이티브 파이프 플레이스홀더:
1:10 |> paste(_, collapse = '-')
# tidyverse 스타일:
df |> dplyr::filter(age > 18) |>
dplyr::select(name) |>
dplyr::arrange(name)
# magrittr 파이프는 . 으로 플레이스홀더:
library(magrittr)
1:10 %>% sum()

7.문자열

문자 벡터, 연결, 추출, 분할, 정규식과 포맷팅.

문자열 기본

문자 벡터는 따옴표를 사용하며, 큰따옴표와 작은따옴표는 동일합니다. nchar는 길이, []는 문자를 가져옵니다. 백슬래시는 이스케이프가 필요합니다.

1
2
3
4
5
6
7
8
9
10
11
12
s <- 'hello'
"hi" # 동등
toupper('abc') # "ABC"
tolower('ABC') # "abc"
nchar('中文') # 2(문자 수)
# 백슬래시 이스케이프:
cat('a\tb') # a\tb
# 원시 문자열:
raw_s <- r"(C:\path\file)"
# 연결:
paste('a', 'b') # "a b"
paste0('a', 'b') # "ab"

연결

paste는 기본적으로 공백으로 구분하고, paste0는 구분자가 없습니다. collapse는 전체 벡터를 하나의 문자열로 합칩니다. 벡터화된 연결.

1
2
3
4
5
6
7
8
9
10
11
12
paste('a', 'b', 'c') # "a b c"
paste0('a', 'b') # "ab"
paste('a', 'b', sep = '-') # "a-b"
# collapse:벡터를 한 문자열로
paste(1:3, collapse = ',') # "1,2,3"
# 벡터화된 조합:
paste(letters[1:3], 1:3) # "a 1" "b 2" "c 3"
# 순환 재활용 조합:
paste(letters[1:2], 1:3) # 길이를 3 으로 보강
# 문자 수와 바이트 수:
nchar('你好') # 2
nchar('你好', type = 'bytes') # 6

추출

substr / substring은 위치로 추출하고, [] 대괄호는 단일 문자를 가져옵니다. strsplit은 분할.

1
2
3
4
5
6
7
8
9
10
s <- 'hello world'
substr(s, 1, 5) # "hello"
substring(s, 7) # "world"(끝까지)
# 한 글자 추출:
strsplit(s, '')[[1]] # 글자 단위
# 부분 문자열 치환:
substr(s, 7, 11) <- 'R!'
s # "hello R!"
# 위치로 여러 구간 추출:
substr('abcdef', c(1, 4), c(2, 6))

분할

strsplit은 구분자로 리스트로 분할합니다. unlist는 평탄화합니다. 정규식도 구분자로 사용 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
s <- 'a,b,c'
strsplit(s, ',') # 리스트 ["a" "b" "c"]
unlist(strsplit(s, ',')) # 벡터
# 벡터화된 분할:
strsplit(c('a-b', 'c-d'), '-')
# 정규식으로 분할:
strsplit('a1b22c', '[0-9]+')
# 고정 문자열 매치(정규식 아님):
strsplit('a.b.c', '.', fixed = TRUE)
# 다시 합치기:
paste(unlist(strsplit(s, ',')), collapse = ';')

정규식 함수

grep / grepl은 매칭, gsub / sub는 치환입니다. 벡터화되어 모든 요소를 처리합니다. 자세한 내용은 정규식 섹션을 참조하세요.

1
2
3
4
5
6
7
8
9
10
x <- c('apple', 'banana', 'cherry')
grepl('^a', x) # TRUE FALSE FALSE
grep('a', x) # 인덱스 1 2
# 치환:
gsub('a', 'o', x) # opple bonono cherory
sub('a', 'o', x) # 첫 번째만 치환
# 매치 부분 추출:
regmatches(x, gregexpr('a', x))
# 대소문자:
grepl('APPLE', x, ignore.case = TRUE)

포맷팅

sprintf는 C의 printf 스타일 포맷팅을 모방합니다. %s는 문자열, %d는 정수, %f는 부동소수점. formatC / round는 숫자를 제어합니다.

1
2
3
4
5
6
7
8
9
10
11
sprintf('%s is %d', 'R', 4) # "R is 4"
sprintf('%.2f', pi) # "3.14"
sprintf('%5.1f', pi) # " 3.1"
sprintf('%05d', 42) # "00042"
# 퍼센트 기호 이스케이프:
sprintf('100%%')
# 벡터화:
sprintf('x%d', 1:3) # "x1" "x2" "x3"
# 숫자 포맷:
format(pi, digits = 3)
formatC(12345, big.mark = ',')

대소문자 변환과 공백 제거

toupper / tolower는 대소문자를 변환하고, trimws는 공백을 제거합니다. chartr은 문자 치환입니다. 도구명 처리에 자주 사용됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
toupper('hello') # "HELLO"
tolower('HELLO') # "hello"
# 첫 글자 대문자:
to_title <- function(s) {
paste0(toupper(substr(s, 1, 1)), substr(s, 2, nchar(s)))
}
to_title('hello') # "Hello"
# 공백 제거:
trimws(' hi ') # "hi"
trimws('\nhi\t', which = 'both')
# 문자 단위 치환:
chartr('a-c', 'A-C', 'abc')
# 패키지명을 스네이크 케이스로:
# gsub('([A-Z])', '_\\L\\1', s, perl = TRUE)

인코딩 처리

R 문자열은 UTF-8이 주를 이룹니다. Encoding은 인코딩을 확인하고, enc2utf8은 변환합니다. iconv는 코드 변환을 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
s <- '你好'
Encoding(s) # "UTF-8"
enc2utf8(s) # UTF-8 로 변환
enc2native(s) # 네이티브 인코딩으로 변환
# 인코딩 변환:
iconv(s, from = 'UTF-8', to = 'GB18030')
# 바이트 표시:
charToRaw('A') # 41
rawToChar(as.raw(0x41)) # "A"
# 16 진수 문자열:
paste(as.hexmode(charToRaw('R')), collapse = ' ')
# 파일 읽기에서 인코딩 지정:
# readLines('f.txt', encoding = 'UTF-8')

stringr

tidyverse의 문자열 처리 패키지. str_ 접두사가 붙은 함수로 명명이 통일되어 있으며, str_detect / str_replace / str_extract.

1
2
3
4
5
6
7
8
9
10
11
12
# library(stringr)
str_detect(c('a1', 'b2'), '[0-9]')
str_replace('ab-cd', '-', '_')
str_replace_all('a-b-c', '-', '')
str_extract('price 99', '[0-9]+')
str_extract_all('a1 b2', '[0-9]')
str_remove('xx-abc', 'xx-')
str_split('a,b', ',')
str_pad('5', 3, pad = '0') # "005"
str_trim(' hi ')
str_to_title('hello world')
str_length('你好') # 2

8.집합과 apply 계열

apply / lapply / sapply로 일괄 순회, 정렬, 중복 제거, 집합 연산과 dplyr 데이터 조작.

lapply와 sapply

lapply는 리스트/벡터의 각 요소에 함수를 적용하고 리스트를 반환합니다. sapply는 벡터나 행렬로 단순화를 시도합니다.

1
2
3
4
5
6
7
8
9
10
l <- list(a = 1:3, b = 4:5)
lapply(l, sum) # 리스트, $a $b 포함
lapply(l, function(x) x * 2)
# sapply 로 단순화:
sapply(l, sum) # a=6 b=9 이름 있는 벡터
sapply(1:4, sqrt)
# vapply 로 반환 타입 지정(더 안정적):
vapply(1:4, sqrt, numeric(1))
# unlist 로 리스트 결과 평탄화:
unlist(lapply(1:3, function(x) x^2))

apply

apply는 행렬/배열의 차원을 따라 일괄 연산합니다. MARGIN=1은 행별, 2는 열별. 반환값은 차원별로 결합됩니다.

1
2
3
4
5
6
7
8
9
10
m <- matrix(1:9, nrow = 3)
apply(m, 1, sum) # 각 행의 합
apply(m, 2, sum) # 각 열의 합
apply(m, 1, mean)
apply(m, 2, function(x) x / max(x))
# 3 차원 배열에서 3 번째 차원 방향:
a <- array(1:24, c(2, 3, 4))
apply(a, 3, sum)
# 인덱스 포함 그룹화:
apply(m, 1, which.max)

mapply 다중 인자

mapply는 여러 매개변수에 함수를 병렬로 적용하며, Map에 대응합니다. 짧은 매개변수는 루프로 보충됩니다. SIMPLIFY는 단순화를 제어합니다.

1
2
3
4
5
6
7
8
9
mapply(paste, c('a', 'b'), c(1, 2))
# pmax 등에 대응:
mapply(max, c(1, 5), c(3, 2)) # 3 5
# Map 은 단순화하지 않고 항상 리스트:
Map(paste, c('a', 'b'), c(1, 2))
# 여러 인자의 벡터화:
mapply(function(x, y) x^y, 1:3, 1:3)
# 행렬로 단순화:
mapply(function(x, y) c(x, y), 1:2, 3:4, SIMPLIFY = TRUE)

split 그룹화

split은 인자별로 벡터를 그룹 리스트로 분할합니다. lapply와 함께 그룹 통계를 수행하며, group-by와 동일합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
x <- c(1, 2, 3, 4, 5)
g <- c('a', 'a', 'b', 'b', 'b')
split(x, g) # 리스트 $a $b
# 그룹별 평균:
lapply(split(x, g), mean)
# 데이터프레임을 열로 그룹화:
df <- data.frame(grp = c('a', 'a', 'b'), val = 1:3)
split(df, df$grp)
# 통계 요약:
sapply(split(df$val, df$grp), summary)
# tapply:한 번에 처리
tapply(df$val, df$grp, mean)
# dplyr 의 group_by + summarise 에 대응

정렬과 순위

sort는 벡터를 정렬하고, order는 정렬 인덱스를 반환하며, rank는 순위를 매�니다. decreasing은 방향을 제어합니다. 데이터 프레임을 열별로 정렬.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
x <- c(3, 1, 2)
sort(x) # 1 2 3
order(x) # 2 3 1(인덱스)
x[order(x)] # 정렬 후
rev(sort(x)) # 내림차순
rank(c(3, 1, 2)) # 3 1 2
# 데이터프레임 정렬:
df <- data.frame(age = c(30, 20), name = c('b', 'a'))
df[order(df$age), ]
# 여러 열 정렬:
df2 <- data.frame(a = c(1, 1, 2), b = c(2, 1, 3))
df2[order(df2$a, df2$b), ]
# dplyr:
# dplyr::arrange(df, desc(age))

중복 제거

unique은 중복을 제거하고, duplicated는 중복을 표시합니다. 중복 행 중 첫 번째만 취합니다. all.equal은 벡터를 비교합니다.

1
2
3
4
5
6
7
8
9
10
11
12
x <- c(1, 2, 1, 3, 2)
unique(x) # 1 2 3
duplicated(x) # FALSE FALSE TRUE FALSE TRUE
!duplicated(x) # 첫 등장 유지
x[!duplicated(x)]
# 데이터프레임 중복 행 제거:
df <- data.frame(a = c(1, 1, 2), b = c(1, 1, 3))
unique(df)
# 중복 카운트:
table(x)
# dplyr:
# dplyr::distinct(df, a, .keep_all = TRUE)

집합 연산

union / intersect / setdiff 집합 연산. %in%은 요소 존재를 판단합니다. unique 후 교집합, 합집합, 차집합을 구합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
a <- c(1, 2, 3); b <- c(2, 3, 4)
union(a, b) # 1 2 3 4
intersect(a, b) # 2 3
setdiff(a, b) # 1
setdiff(b, a) # 4
# 멤버 판정:
2 %in% a # TRUE
# 중복 유지한 합집합:
c(a, b)
# 동등 비교:
identical(a, b)
setequal(a, c(3, 2, 1)) # TRUE(순서 무시)
# %in% 으로 필터:
a[a %in% b] # 2 3

리스트 조작

리스트 결합, 평탄화, 일괄 명명 변경. unlist는 재귀적으로 평탄화하고, do.call은 배열로 결합합니다. purrr의 타입화된 조작.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
l1 <- list(a = 1); l2 <- list(b = 2)
c(l1, l2) # 결합
unlist(l1) # 이름 있는 벡터
# 리스트 각 요소에 함수 적용:
lapply(l1, function(x) x + 1)
# 리스트 이름 변경:
names(l) <- c('x', 'y')
# 조건으로 요소 추출:
l <- list(1, 'a', TRUE)
Filter(is.numeric, l)
# 한 층 평탄화:
list_of_lists <- list(list(1, 2), list(3))
unlist(list_of_lists, recursive = TRUE)
# purrr:
# purrr::map(l, ~ .x * 2)
# purrr::keep(l, is.numeric)

dplyr 데이터 조작

select/filter/mutate/arrange/summarise는 파이프식으로 데이터 프레임을 조작하며, group_by는 그룹화합니다. 핵심 tidyverse.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# library(dplyr)
df <- data.frame(
name = c('a', 'b', 'c'),
age = c(25, 35, 40),
salary = c(50, 60, 80)
)
df |> filter(age > 30)
df |> select(name, salary)
df |> mutate(salary2 = salary * 2)
df |> arrange(desc(age))
# 그룹 집계:
df |> group_by(age > 30) |>
summarise(avg = mean(salary))
# 중복 제거:
df |> distinct(age)
# 계산 열 추가 후 필터:
df |> mutate(ratio = salary / age) |>
filter(ratio > 1.5)

9.메모리 관리

가비지 컬렉션, 객체 크기, 복사 비용, 사전 할당과 성능 프로파일링.

가비지 컬렉션

R은 자동 참조 카운팅 + 주기적 GC를 사용합니다. gc는 수동으로 회수하고 보고합니다. 대규모 데이터 루프 후 호출할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
gc() # 강제 회수, 통계 반환
# gc 는 Ncells/Vcells 사용과 피크를 반환:
gc(reset = TRUE) # 피크 통계 리셋
# 큰 객체는 안 쓰면 NULL 로:
big <- 1:1e8
big <- NULL # 회수 가능
gc()
# gc 트리거 임계값 확인:
gc(verbose = TRUE) # 회수 상세 출력
# 메모리 사용 함수:
ls() # 현재 객체 확인

객체 크기

object.size는 단일 객체의 바이트 수를 확인합니다. format은 가독성 있는 단위로 변환합니다. 대규모 데이터 세트는 메모리를 예측해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
object.size(1:1e6)
format(object.size(1:1e6), units = 'MB')
# 리스트 크기:
object.size(list(1:1e5, 1:1e5))
# 데이터프레임:
df <- data.frame(x = 1:1e5, y = rnorm(1e5))
format(object.size(df), units = 'MB')
# 공유 객체의 실제 점유:
# lobstr::obj_size(x, y)
# 대규모 데이터 예측:
# 1e7 행 데이터프레임은 수백 MB 정도

복사 비용

R의 값 의미론은 수정 시 전체 객체를 복사하므로, 대형 객체 수정 비용이 높습니다. 대형 벡터 요소를 자주 수정하지 마세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
x <- 1:1e7
# 객체 전체 연산은 빠름(벡터화):
y <- x * 2
# 루프 안에서 요소별 변경:
for (i in 1:1000) x[i] <- x[i] + 1 # 대량 복사 발생
# 사전 할당으로 대체:
out <- numeric(1e5) # 먼저 할당
for (i in seq_along(out)) out[i] <- i
# 루프 안에서 c() 로 늘리는 것 회피:
# res <- c() # 느림
# 벡터화나 사전 할당 사용
# 복사 여부 확인:
# tracemem(x)

사전 할당

먼저 길이가 있는 결과 벡터를 할당한 다음 루프로 채워서, 반복적인 연결 복사를 피합니다. numeric/character 사전 할당.

1
2
3
4
5
6
7
8
9
10
11
12
13
n <- 1e4
# numeric 벡터 사전 할당:
out <- numeric(n)
for (i in 1:n) out[i] <- i^2
# 문자열 사전 할당:
res <- character(n)
# 리스트 사전 할당:
results <- vector('list', n)
for (i in seq_len(n)) results[[i]] <- i * 2
# 피해야 할 방식(느림):
# res <- c(); for (i in 1:n) res <- c(res, i)
# 이상:완전히 벡터화
sq <- (1:n)^2

벡터화와 성능

가능한 한 벡터 전체로 연산하세요. 행 결합 rbind는 느리므로, do.call(rbind, list) 또는 data.table을 사용하세요. 마이크로 벤치마크.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
x <- 1:1e6
# 빠름:벡터 전체
system.time(y <- x * 2 + sin(x))
# 느림:루프
# for (i in seq_along(x)) y[i] <- x[i] * 2
# 행 결합 누적은 느림:
# for (...) df <- rbind(df, row) # 나쁨
# 올바른 방법:리스트에 모아 do.call
rows <- lapply(1:100, function(i) data.frame(i = i))
big <- do.call(rbind, rows)
# 계측:
system.time(mean(x))
# 마이크로벤치마크:
# microbenchmark::microbenchmark(a, b)

메모리 상한

memory.limit(Windows) 상한을 확인/조정합니다. object.size로 검사. ulimit가 세션에 영향.

1
2
3
4
5
6
7
8
9
10
11
memory.limit() # Windows 메모리 상한 MB
# 필요 시:
# memory.limit(size = 8192)
# 현재 사용량:
memory.size()
# 다른 플랫폼에는 memory.limit 없음:
# 시스템 리소스 도구로 모니터링
# 큰 객체 확인:
ls() |> sapply(function(x) object.size(get(x)))
# 정렬해 최대 확인:
sort(sapply(ls(), function(x) object.size(get(x))), decreasing = TRUE)[1:5]

성능 프로파일링

Rprof는 함수 호출 시간을 기록합니다. summaryRprof는 요약합니다. system.time은 대략적인 타이밍. profvis는 시각화.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Rprof('prof.out')
slow <- function() {
for (i in 1:1e5) sqrt(i)
}
slow()
Rprof(NULL)
summaryRprof('prof.out')
# 간단한 계측:
system.time(slow())
# 세밀한 계측:
# tictoc::tic(); slow(); tictoc::toc()
# 시각화:
# profvis::profvis(slow())
# 벤치마크 비교:
# bench::mark(a, b)

data.table 메모리

data.table은 참조 의미론을 사용하여 복사를 줄입니다: :=는 제자리 수정, setDT는 변환, copy는 명시적 복사. 대규모 데이터 세트에서 메모리를 절약합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# library(data.table)
dt <- data.table(a = 1:1e5, b = rnorm(1e5))
# 그 자리에서 열 추가/변경(복사 없음):
dt[, c := a * 2]
# set 계열:
setkey(dt, a) # 정렬 키
setorder(dt, a) # 그 자리에서 정렬
# 부분 집합 시 복사 회피:
sub <- dt[a > 100] # 부분 집합 복사
# 명시적 복사:
copy(dt)
# 열 참조 공유:
# set(dt, j = 'd', value = dt$a)
# 메모리 비교:
# df 는 열 변경 시 프레임 전체 복사, dt 는 안 함

10.객체지향 (S3 / S4 / R6)

S3 간이 제네릭, S4 형식적 클래스, R6 참조 클래스, 세 가지 OO 시스템이 각자 역할.

S3 클래스 기본

S3는 R의 간이 OO: class 속성 + 제네릭 함수. unclass로 기반을 확인. 가장 흔히 사용되며 가장 가볍습니다.

1
2
3
4
5
6
7
8
9
10
11
12
x <- 1:3
class(x) # "integer"
# 사용자 정의 클래스:
obj <- structure(list(a = 1), class = 'myclass')
class(obj) # "myclass"
# 제네릭 디스패치:
print(obj) # print.myclass 호출
# 제네릭 함수 목록:
methods('print')
# 디스패치 대상 확인:
class(1:3)
unclass(1:3) # 클래스 속성 제거

S3 메서드

class.method 함수를 정의하면 제네릭이 구현됩니다. UseMethod가 디스패치합니다. NextMethod가 부모 메서드를 호출합니다.

1
2
3
4
5
6
7
8
9
10
11
12
shape <- function(x) UseMethod('shape')
shape.default <- function(x) paste('default:', class(x)[1])
shape.circle <- function(x) paste('圆,半径', x$r)
# 객체 구성:
c1 <- structure(list(r = 3), class = 'circle')
shape(c1) # 圆,半径 3
shape(1:3) # default
# print 오버라이드:
print.circle <- function(x) cat('Circle r =', x$r, '\n')
print(c1)
# 전체 메서드 확인:
methods('shape')

S3 생성

커스텀 클래스는 생성기와 검증 제공이 필요합니다. structure로 한 번에 클래스를 설정합니다. print / summary로 커스텀 출력.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new_point <- function(x, y) {
stopifnot(is.numeric(x), is.numeric(y))
structure(list(x = x, y = y), class = 'point')
}
p <- new_point(1, 2)
# 제네릭 print:
print.point <- function(obj, ...) {
cat('point(', obj$x, ',', obj$y, ')\n')
}
print(p)
# 제네릭 summary:
summary.point <- function(obj, ...) {
cat('均值:', mean(c(obj$x, obj$y)), '\n')
}
summary(p)
# 제네릭 + 검증:
# new_ 접두사는 tidyverse 관례

S4 클래스

S4는 형식적 OO: setClass로 슬롯을 정의하고, validity로 검증하며, setGeneric / setMethod가 제네릭.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
setClass('Person',
slots = c(name = 'character', age = 'numeric'),
validity = function(obj) {
if (obj@age < 0) 'age 不能为负' else TRUE
}
)
p <- new('Person', name = 'Rex', age = 5)
p@age # 5
slot(p, 'name') # "Rex"
# 제네릭:
setGeneric('describe', function(x) standardGeneric('describe'))
setMethod('describe', 'Person', function(x) paste(x@name, x@age))
describe(p)
# 확인:
isS4(p)
# 상속:
# setClass('Student', contains = 'Person')

R6 클래스

R6는 참조 시맨틱 캡슐화 클래스로, 메서드 안에서 self$로 접근한다. 객체 지향 프로그래밍이 다른 언어와 더 가깝다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# library(R6)
Animal <- R6Class('Animal',
public = list(
name = NULL,
initialize = function(name) self$name <- name,
speak = function() paste(self$name, '叫')
)
)
a <- Animal$new('狗')
a$speak() # "狗 叫"
# private 멤버:
Class <- R6Class('Class',
public = list(init = NULL),
private = list(hidden = 42)
)
# 상속:
Dog <- R6Class('Dog',
inherit = Animal,
public = list(speak = function() paste(self$name, '汪汪'))
)
d <- Dog$new('旺财')
d$speak()

제네릭 디스패치

디스패치는 객체의 class 속성에 기반하며, UseMethod는 class별로 해당 메서드를 찾는다. 레거시 클래스(S3)는 차례로 시도한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
f <- function(x) UseMethod('f')
f.default <- function(x) '默认'
f.numeric <- function(x) '数字'
f.factor <- function(x) '因子'
f(1L) # 数字
f(factor('a')) # 因子
f('a') # 默认
# 다중 인수 디스패치(S4):
# setMethod(..., signature = c('x', 'y'))
# 디스패치 메서드 표 확인:
methods('mean')
# class는 벡터가 될 수 있음:
class(x) <- c('myclass', 'numeric')

상속

S3는 class 속성으로 상속(NextMethod 체인)하고, S4는 contains를, R6는 inherit을 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
# S3 상속:
structure(list(a = 1), class = c('sub', 'base'))
# NextMethod로 부모 클래스 메서드 호출:
f.base <- function(x) 'base 实现'
f.sub <- function(x) paste('sub:', NextMethod())
f(structure(list(), class = c('sub', 'base')))
# R6 상속은 r6 토픽 참조:
# super$로 부모 클래스 메서드에 접근
# S4 상속:
# setClass('Base')
# setClass('Derived', contains = 'Base')
# 상속 확인:
# inherits(obj, 'base')

세 시스템 비교

S3는 대부분의 패키지에서 가볍게 쓰이고, S4는 엄격한 정의에 쓰이며, R6의 참조 시맨틱은 상태 객체에 적합하다. 실제로는 섞어 쓰는 경우가 많다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# S3: 클래스 속성 + 제네릭, 선언 불필요
# 빠름, 강제 없음, 많은 패키지에서 사용
# S4: setClass로 엄격한 슬롯과 검증
# 공식 API와 대규모 프레임워크용
# R6: 참조 시맨틱스, 메서드 캡슐화
# 가변 상태, 모델링 객체용
# 혼용 예:
# ggplot2는 S3;
# Bioconductor는 S4;
# 새 패키지의 대부분은 R6
# 선택 기준:
# 단순한 데이터 객체는 S3,
# 복잡한 공식 인터페이스는 S4,
# 객체를 변경하고 싶으면 R6

class와 속성

class는 특수 속성으로, 제네릭이 이를 기준으로 디스패치한다. attr은 사용자 정의 속성을 추가할 수 있으며 디스패치에는 영향을 주지 않는다. class<-는 클래스를 직접 설정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
x <- 1:3
class(x) # "integer"
class(x) <- 'myclass' # 클래스 직접 변경
x
# 전체 속성 확인:
attributes(x)
# 커스텀 속성:
attr(x, 'note') <- '说明'
attr(x, 'note')
# 클래스 판정:
inherits(x, 'myclass') # TRUE
is.numeric(1:3)
# 제네릭은 class 순서로 탐색:
# 먼저 class[1], 다음 class[2]..., 마지막 default
# 클래스 제거:
unclass(x)

11.오류 처리

stop으로 오류, warning으로 경고, tryCatch로 포착, 그리고 조건 시스템.

stop으로 오류 발생

stop은 오류를 던져 실행을 종료한다. stopifnot는 조건을 빠르게 검증한다. 오류 메시지는 원인을 명확히 적는 것이 좋다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
divide <- function(a, b) {
if (b == 0) stop('除数不能为 0')
a / b
}
divide(1, 0) # 오류 발생
# stopifnot:
check <- function(x) {
stopifnot(is.numeric(x), length(x) > 0)
x
}
check('a') # 오류 발생
# 커스텀 오류 클래스:
stop('自定义错误', call. = FALSE)
# 조건 객체 부여:
# stop(simpleError('msg'))

warning 경고

warning은 치명적이지 않은 알림을 주며 중단하지 않는다. suppressWarnings로 침묵시킨다. options(warn=2)로 오류로 변환한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
warn_if <- function(x) {
if (any(is.na(x))) warning('存在 NA')
x
}
warn_if(c(1, NA))
# 침묵시키기:
suppressWarnings(warn_if(c(NA)))
# 모든 경고를 오류로:
options(warn = 2)
# 되돌리기:
options(warn = 0)
# 경고를 한 번만 포착:
withCallingHandlers(
expr,
warning = function(w) print('有警告')
)

tryCatch

tryCatch는 오류/경고를 포착해 처리 결과를 반환한다. error, warning, finally 세 부분으로 구성된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
safe_div <- function(a, b) {
tryCatch(
a / b,
error = function(e) paste('错误:', conditionMessage(e)),
warning = function(w) paste('警告:', conditionMessage(w)),
finally = cat('完成\n')
)
}
safe_div(1, 0) # 错误: 除数不能为 0
safe_div(1, 2) # 0.5
# 오류만 보기:
tryCatch(stop('boom'), error = function(e) '捕获')
# 결과와 오류 모두 유지:
tryCatch(list(ok = TRUE, val = 1), error = function(e) list(ok = FALSE))

try 내결함성

try는 표현식 결과를 반환하며, 오류 발생 시 try-error 클래스 객체를 돌려주고 전체를 중단하지 않는다. 일괄 처리에 자주 쓰인다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
out <- try(log('a'), silent = TRUE)
class(out) # "try-error"
inherits(out, 'try-error') # TRUE
# 오류에 관대한 루프:
res <- vector('list', 3)
for (i in 1:3) {
res[[i]] <- try(log(i - 2), silent = TRUE)
}
# 실패한 항목 제외:
failed <- sapply(res, inherits, 'try-error')
# 성공한 항목:
res[!failed]
# 오류 시 기본값 부여:
out2 <- tryCatch(log(-1), error = function(e) NA)
out2

조건 시스템

R의 오류/경고/메시지는 모두 조건 객체다. signalCondition이 발생시키고, withCallingHandlers가 포착한 뒤 계속 진행한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
signalCondition(simpleError('出错了'))
# 조건 클래스:
stopifnot(identical(conditionMessage(simpleError('x')), 'x'))
# 메시지:
message('普通提示')
# 메시지 포착:
withCallingHandlers(
message('hi'),
message = function(m) cat('捕获:', conditionMessage(m), '\n')
)
# 조건은 호출 스택을 포함:
f <- function() stop('deep')
# tryCatch로 포착:
tryCatch(f(), error = function(e) conditionCall(e))
# 조건 객체에 자체 필드를 실을 수 있음:
# simpleError('msg', call = sys.call())

경고 가로채기

withCallingHandlers는 경고를 잡되 실행은 멈추지 않으며, 기록하고 계속 진행한다. invokeRestart와 함께 쓰인다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
log_warnings <- function(expr) {
ws <- list()
res <- withCallingHandlers(
expr,
warning = function(w) {
ws[[length(ws) + 1]] <<- conditionMessage(w)
invokeRestart('muffleWarning')
}
)
list(result = res, warnings = ws)
}
out <- log_warnings({ warning('w1'); 42 })
out$result # 42
out$warnings # "w1"
# tryCatch와의 차이:
# withCallingHandlers는 포착한 후에도 원래 식을 계속 실행 가능

restarts 복원 지점

신호 처리의 고급 메커니즘이다. 호출자가 복원 동작을 제공할 수 있다. invokeRestart는 포착 쪽에서 호출한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 재개 지점 정의:
withRestarts(
{ signalCondition(simpleError('x')); '继续' },
my_restart = function() '恢复动作'
)
# 포착 후 재개:
withCallingHandlers(
withRestarts(stop('err'), abort = function() 'aborted'),
error = function(e) invokeRestart('abort')
)
# 흔한 재개 지점:
# muffleWarning、muffleMessage
# 커스텀: 사용자가 error handler에서 선택 가능
# 대화형 재시도 로직용

루프 안의 오류

일괄 처리에서 한 항목의 실패가 전체를 중단해서는 안 된다. tryCatch로 항목별 내결함 처리를 하고 결과를 모은다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
items <- list(1, 'a', 3, -1)
process <- function(x) if (x < 0) stop('负数') else x * 2
# 안전하게 처리:
out <- lapply(items, function(x) {
tryCatch(process(x), error = function(e) NA)
})
unlist(out) # 2 NA 6 NA
# 실패 이유 기록:
out2 <- lapply(items, function(x) {
tryCatch(list(ok = TRUE, v = process(x)),
error = function(e) list(ok = FALSE, msg = conditionMessage(e)))
})
# 처리를 계속:
# purrr::possibly / safely 사용 가능

사용자 정의 오류

사용자 정의 오류 클래스는 추가 필드를 담는다. simpleError / structure으로 구성하며, class로 타입을 식별한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
my_error <- function(msg, code = 500) {
structure(
list(message = msg, call = NULL, code = code),
class = c('my_error', 'error', 'condition')
)
}
stop(my_error('余额不足', code = 400))
# 포착 후 필드 읽기:
fail <- function() stop(my_error('boom'))
tryCatch(
fail(),
my_error = function(e) paste('自定义:', e$code),
error = function(e) '其他错误'
)
# 조건 클래스 생성:
# condition <- structure(list(), class = c('x', 'condition'))

12.파일 및 데이터 I/O

CSV, readr, 줄 단위 읽기, RDS, JSON, 연결 객체와 바이너리.

CSV 읽기/쓰기

read.csv / write.csv로 CSV를 읽고 쓴다. stringsAsFactors=FALSE로 팩터 변환을 막는다. check.names는 열 이름을 처리한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
df <- data.frame(a = 1:3, b = c('x', 'y', 'z'))
write.csv(df, 'out.csv', row.names = FALSE)
# 다시 읽기:
d <- read.csv('out.csv')
# 팩터 피하기:
read.csv('out.csv', stringsAsFactors = FALSE)
# 커스텀 구분자:
write.table(df, 'out.tsv', sep = '\t', row.names = FALSE)
read.delim('out.tsv')
# 헤더 없음:
read.csv('f.csv', header = FALSE)
# 인코딩 지정:
# read.csv('f.csv', fileEncoding = 'UTF-8')

readr 읽기/쓰기

readr은 tidyverse의 고속 CSV 읽기 패키지다. read_csv는 타입을 자동 추론하고, write_csv는 빠르게 쓴다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# library(readr)
write_csv(df, 'out.csv')
read_csv('out.csv')
# 형 자동 추정, 더 빠름:
read_csv('out.csv', col_types = 'di') # d=double i=integer
# 대규모 데이터:
read_csv('big.csv', show_col_types = FALSE)
# 열 이름 지정:
read_csv('f.csv', col_names = c('x', 'y'))
# 다시 쓰기:
write_csv(df, 'out2.csv')
# 기타:
# read_tsv / write_tsv 탭 구분
# read_delim(file, delim = '|')
# 진행률과 형은 README 참조

줄 단위 읽기

readLines는 줄 단위로 읽어 문자 벡터로 만든다. writeLines로 쓴다. 대용량 파일은 청크 단위로 읽는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
writeLines(c('第一行', '第二行'), 'f.txt')
lines <- readLines('f.txt')
lines # "第一行" "第二行"
# 인코딩 지정:
readLines('f.txt', encoding = 'UTF-8')
# 앞의 몇 줄만 읽기:
readLines('f.txt', n = 1)
# 큰 파일은 분할:
con <- file('f.txt', 'r')
while (length(chunk <- readLines(con, n = 10)) > 0) {
cat('块大小', length(chunk), '\n')
}
close(con)
# 인코딩 지정으로 쓰기:
writeLines(lines, 'out.txt', useBytes = FALSE)

read.table로 표 읽기

read.table은 범용 표 읽기 함수이며, read.csv는 그 변형이다. header / sep / na.strings가 자주 쓰는 인자다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 탭 구분:
read.table('data.tsv', header = TRUE, sep = '\t')
# 헤더 없음:
read.table('f.txt', sep = ',')
# 커스텀 NA 값:
read.table('f.csv', header = TRUE, sep = ',', na.strings = c('NA', ''))
# 행 건너뛰기:
read.table('f.csv', skip = 2)
# 열 형 지정:
read.table('f.csv', colClasses = c('numeric', 'character'))
# 행 이름:
read.table('f.csv', row.names = 1)
# 건수 제한:
read.table('f.csv', nrows = 100)

RDS 저장/읽기

saveRDS는 단일 R 객체를 저장하고, readRDS는 읽어온다. 타입과 속성을 그대로 보존하므로 CSV보다 완전하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
saveRDS(df, 'df.rds')
d2 <- readRDS('df.rds')
identical(df, d2) # TRUE
# 여러 객체 저장:
save(df, x, file = 'all.RData')
load('all.RData') # df와 x 복원
# 압축:
saveRDS(df, 'df.rds', compress = TRUE)
# 다른 도구에서 읽기:
# readRDS는 이전 버전도 읽을 수 있음
# 대규모 데이터 권장:
# data.table::fread / fwrite가 더 효율적
# 파일 확인:
# file.info('df.rds')

JSON 읽기/쓰기

jsonlite는 주류 JSON 패키지다. fromJSON은 파싱, toJSON은 직렬화다. 행 단위 JSON은 API 응답 처리에 쓴다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# library(jsonlite)
json <- '{"name":"R","ver":4.4}'
fromJSON(json) # 이름 있는 리스트
# 데이터프레임을 JSON으로:
df <- data.frame(a = 1:2, b = c('x', 'y'))
toJSON(df)
# 중첩 구조:
fromJSON('{"x":[1,2],"y":{"z":true}}')
# 행별 JSON(각 행이 객체 1개):
stream_in(file('rows.jsonl'))
# 배열:
fromJSON('[1,2,3]')
# 파일로 쓰기:
write(toJSON(df), 'out.json')
# 복잡한 구조를 데이터프레임으로:
# fromJSON('...', flatten = TRUE)

연결 객체

연결은 파일/네트워크 등 데이터 소스를 추상화한 것이다. file로 열고, readLines / writeLines로 읽고 쓰며, close로 닫는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
con <- file('f.txt', 'w')
writeLines('一行', con)
close(con)
# 읽기용 연결:
con <- file('f.txt', 'r')
content <- readLines(con)
close(con)
# 한 줄씩 처리:
con <- file('big.txt', 'r')
while (length(line <- readLines(con, n = 1)) > 0) {
# line 처리
}
close(con)
# 텍스트/바이너리 모드:
file('f.bin', 'rb')
# url 연결:
url('https://example.com')
# 자동 닫기:
# on.exit(close(con))

바이너리 읽기/쓰기

readBin / writeBin으로 바이너리를 읽고 쓴다. raw 타입은 바이트를 담는다. 대용량 파일과 이미지 데이터는 바이너리로 다룬다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
x <- as.raw(c(0x48, 0x49)) # "H" "I"
writeBin(x, 'f.bin')
back <- readBin('f.bin', 'raw', n = 2)
# 정수 읽기:
writeBin(1:3, 'ints.bin')
readBin('ints.bin', 'integer', n = 3)
# 바이트 길이:
length(x)
# 파일 크기:
file.info('f.bin')$size
# 이미지:
# png::readPNG / jpeg::readJPEG
# 대규모 데이터 읽기/쓰기:
# readBin으로 분할 읽기
# raw 뷰 확인:
charToRaw('A') # 41

기타 형식

readxl은 Excel을, haven은 SPSS/Stata를, feather/parquet는 컬럼 지향 형식을 읽는다. 필요할 때 로드해 쓴다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Excel:
# library(readxl)
# read_excel('book.xlsx', sheet = 1)
# readxl::excel_sheets('book.xlsx')
# SPSS / Stata:
# library(haven)
# read_sav('data.sav'); read_dta('data.dta')
# 열 지향 스토리지:
# library(arrow)
# write_parquet(df, 'df.parquet')
# read_parquet('df.parquet')
# feather:
# arrow::write_feather(df, 'df.feather')
# 데이터베이스:
# library(DBI); dbConnect(RSQLite::SQLite())
# dbReadTable(con, 'tbl')

13.자주 만나는 함정

R을 일상적으로 쓸 때 가장 자주 만나는 함정과 올바른 작성법.

조건 길이가 1이 아님

if는 길이가 1인 조건이 필요하다. 벡터 조건은 첫째 값만 판단하고 경고를 낸다. any/all로 모으거나 ifelse를 쓴다.

1
2
3
4
5
6
7
8
x <- c(1, 2, 3)
// BAD if는 첫 요소만 판단하고 경고 있음
if (x == 2) print('找到')
// GOOD any / all로 벡터 전체 판단
if (any(x == 2)) print('找到')
if (all(x > 0)) print('全正')
// GOOD 요소별 조건은 ifelse
ifelse(x == 2, '命中', '未命中')

팩터 → 숫자 변환

팩터를 바로 as.numeric으로 바꾸면 내부 코드가 나온다. 먼저 문자로 바꾼 뒤 숫자로 바꿔야 실제 값이다.

1
2
3
4
5
6
7
8
9
f <- factor(c('10', '20', '30'))
// BAD 내부 코드 1 2 3이 된다
as.numeric(f)
// GOOD 문자로 되돌린 뒤 숫자로
as.numeric(as.character(f))
// GOOD 또는 levels로 인덱싱
as.numeric(levels(f))[f]
# CSV 읽을 때 stringsAsFactors = FALSE로 예방
read.csv('f.csv', stringsAsFactors = FALSE)

차원 손실

행렬에서 단일 행/열을 뽑으면 기본적으로 벡터로 축소된다. drop = FALSE로 차원을 유지한다.

1
2
3
4
5
6
7
8
9
10
11
12
m <- matrix(1:9, nrow = 3)
m[1, ] # 벡터 1 4 7
// BAD 행을 결합하려 할 때 차원이 떨어짐
rbind(m[1, ], m[2, ])
// GOOD 행의 차원 유지
rbind(m[1, , drop = FALSE], m[2, , drop = FALSE])
# 부분집합 후 행렬을 유지하고 싶을 때:
m[1:2, , drop = FALSE]
# 데이터프레임에서 단일 열은 벡터가 반환됨:
df <- data.frame(a = 1:3, b = 4:6)
df[1] # 여전히 data.frame
df[[1]] # 벡터

루프로 벡터 키우기

루프에서 c()로 이어 붙일 때마다 벡터 전체가 복사되어 매우 느리다. 미리 할당하거나 벡터화 연산으로 바꾼다.

1
2
3
4
5
6
7
8
9
10
11
n <- 5000
// BAD 매번 c()가 복사, O(n^2)
res <- numeric()
for (i in 1:n) res <- c(res, i)
// GOOD 길이를 미리 확보
res <- numeric(n)
for (i in 1:n) res[i] <- i
// GOOD 바로 벡터화(최속)
res <- 1:n
# 리스트로 모은 뒤 결합:
# do.call(c, lapply(1:n, function(i) i))

NA와 비교

NA가 비교에 들어가면 결과가 NA가 되어 FALSE가 나오지 않는다. NA를 걸러낼 때는 is.na로 판정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
x <- c(1, NA, 3)
x == NA # NA NA NA(이렇게 하지 말 것)
// BAD NA를 남긴 채 비교
x[x == 1] # NA가 섞여 들어감
// GOOD 먼저 NA 제외
x[!is.na(x) & x == 1]
// GOOD which라면 NA를 무시 가능
which(x == 1)
# NA 존재 판단:
any(is.na(x))
# NA와 NaN은 다르다:
is.nan(NaN); is.na(NaN) # TRUE TRUE
# 통계 시 건너뛰기:
sum(x, na.rm = TRUE)

부분 일치

$와 [[은 유일한 접두사 일치를 한다. 열 이름을 줄여 쓰는 침묵 동작이 버그를 숨기기 쉽다. 전체 이름을 쓰거나 dplyr을 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
df <- data.frame(age_group = 1:3, score = 4:6)
// BAD age_group에 조용히 매치됨
df$age_g
// GOOD 완전한 열 이름 사용
df$age_group
// GOOD [[ 는 완전 일치
df[['age_group']]
# 부분 일치 끄기:
df[['age_g', exact = FALSE]] # 그래도 애매한 매치
# dplyr는 tidyselect를 쓰며 애매하지 않음:
# dplyr::select(df, age_g) # 오류로 지적됨
# 열 이름 확인:
names(df)

재활용 (recycling)

짧은 벡터는 연산에 참여할 때 긴 벡터 길이에 맞게 순환하며 채워진다. 길이가 배수가 아닐 때만 경고가 떠서 숨은 버그가 나오기 쉽다.

1
2
3
4
5
6
7
8
9
10
11
x <- c(1, 2, 3, 4)
y <- c(10, 20)
// BAD 재활용: y는 c(10, 20, 10, 20)으로 채워짐
x + y # 11 22 13 24
// 배수가 아닐 때만 경고:
x + c(1, 2) # 길이 4 vs 2, 경고 없음
x + c(1, 2, 3) # 경고: 4는 3으로 나누어지지 않음
// GOOD 명시적으로 길이가 같음을 확인
stopifnot(length(x) == length(y))
# 비교에서도 순환 재활용은 비슷하게 혼동스럽다:
# x > c(1, 100)

리스트 평탄화

c()는 리스트의 최상위를 평탄화하려 한다. 리스트의 원소를 다시 리스트에 넣을 때는 list()를 쓴다. unlist의 재귀 평탄화에는 위험이 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
l <- list(a = list(x = 1), b = list(y = 2))
// BAD c()가 최상위 리스트를 평평하게 만든다
c(l$a, l$b)
// GOOD 중첩 유지
list(l$a, l$b)
// GOOD 평평하게 하려면 의도를 명시
unlist(l, recursive = FALSE)
# 리스트 2개 결합:
c(list(a = 1), list(b = 2))
# 구조 확인:
str(c(l$a, l$b))
# 벡터화된 요소 수집:
# do.call(list, l)

패키지 가림

library로 실은 패키지는 같은 이름의 함수를 가리며, search 순서에서 뒤에 실린 것이 먼저 숨겨진다. ::로 모호함을 없앤다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 두 패키지가 filter를 내보냄:
# library(stats)
# library(dplyr) # dplyr이 stats::filter를 가림
// BAD 가림 후 filter 의미가 로딩 순서에 따라 바뀜
filter(x, rep(1, 3)) # dplyr::filter일 수도 있음
// GOOD 명시적으로 네임스페이스를 지정해 호출
stats::filter(x, rep(1, 3))
dplyr::filter(df, a > 1)
# 가림 확인:
# find('filter')
# 패키지 언로드:
# detach('package:dplyr')
# 특정 함수만 가져오기:
# dplyr::select
# conflicted 패키지로 감지:
# library(conflicted); conflict_prefer('filter', 'dplyr')

14.병렬 처리와 동시성

parallel, mclapply, foreach, future: R의 다중 프로세스 병렬 처리.

parallel 개요

R의 병렬 처리는 다중 프로세스(fork)나 소켓에 기반한다. parallel 패키지에 mcapply과 클러스터가 내장돼 있다. 병렬은 시작 오버헤드가 있어서 작업이 충분히 커야 가성이 맞다.

1
2
3
4
5
6
7
8
9
10
library(parallel)
detectCores() # CPU 코어 수
detectCores(logical = FALSE) # 물리 코어
# 직렬과 병렬의 시간 비교:
f <- function(i) sqrt(i)
system.time(lapply(1:1e5, f))
# 병렬 버전:
# system.time(mclapply(1:1e5, f))
# 작은 태스크는 병렬이 더 느림(프로세스 시작 오버헤드)
# 적합한 경우: 시간이 오래 걸리는 함수, 대규모 데이터 일괄 처리

mclapply 병렬

mclapply는 parallel의 병렬 lapply로, fork 방식으로 구현된다. Unix 계열에서만 쓸 수 있다(Windows는 클러스터 필요).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
library(parallel)
# Unix/macOS:
f <- function(i) {
Sys.sleep(0.01); i^2
}
# res <- mclapply(1:8, f, mc.cores = 4)
# Windows는 mc.cores = 1 또는 클러스터 사용
res <- lapply(1:8, f) # 직렬 폴백
# mc.preschedule: 일괄 스케줄링
# mclapply(x, f, mc.cores = 2, mc.preschedule = TRUE)
# 반환 형은 lapply와 동일:
unlist(res)
# 오류 확인:
# is.atomic(res)

클러스터 병렬

makeCluster로 프로세스 클러스터를 만들고, parLapply로 병렬 적용한다. Windows도 지원한다. 사용 후 stopCluster로 정리한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
library(parallel)
cl <- makeCluster(2) # worker 2개
# 각 worker는 독립 환경이라 데이터를 전달해야 함:
clusterExport(cl, 'shared_data')
res <- parLapply(cl, 1:4, function(i) i * 2)
stopCluster(cl)
unlist(res)
# 각 worker에서 패키지 로드:
# clusterEvalQ(cl, library(dplyr))
# 변수 사전 설정:
# clusterExport(cl, varlist = c('x', 'y'))
# 난수 시드:
# clusterSetRNGStream(cl)
# Windows에서는 병렬에 클러스터가 필수

foreach 병렬

foreach 루프는 결과를 모아 주고, %dopar%는 병렬 실행(doParallel 필요)이다. %do%는 순차 버전이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# library(foreach)
# library(doParallel)
# registerDoParallel(cores = 2)
res <- foreach(i = 1:4) %do% {
i * 2 # 직렬
}
# 병렬 버전:
# res <- foreach(i = 1:4, .combine = c) %dopar% i * 2
# 결과 결합:
# .combine = c / rbind / list
# 패키지와 의존성 전달:
# .packages = 'dplyr', .export = 'fun'
# 중지:
# stopImplicitCluster()
# 결과 결합:
unlist(res)

future 비동기

future 패키지는 병렬 처리를 '미래 값'으로 추상화한다. future()로 비동기 작업을 시작하고, value()로 결과를 가져온다. plan으로 전략을 고른다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# library(future)
plan(multisession) # 멀티세션 전략
f <- future({
Sys.sleep(0.1)
42
}) # 즉시 future 반환
value(f) # 42, 블록하며 결과 획득
# 일괄:
# library(furrr)
# plan(multisession)
# future_map(1:4, ~ .x * 2)
# 전략 전환:
# plan(sequential) # 직렬로 되돌림
# 오류 전파:
# try(value(future(stop('x'))))
# 전역 변수는 자동으로 전달:
# future({ x + 1 })의 x는 자동으로 상속됨

공유 상태

병렬 워커는 메모리를 공유하지 않는다: 각자 환경을 복사해 쓴다. 파일 쓰기는 동시 접근 충돌을 방지해야 하며, 결과는 return으로 모아 돌려준다.

1
2
3
4
5
6
7
8
9
10
11
12
# 데이터는 worker에 명시적으로 전달:
shared <- 1:10
# mclapply(shared, function(x) x * 2)
# clusterExport 또는 future의 자동 캡처 사용
# 같은 파일에 병렬 쓰기는 충돌함:
# 각 worker가 독립 파일에 씀:
# file <- paste0('out_', i, '.csv')
# 결과 정리:
res <- parLapply(cl, 1:4, function(i) i^2)
final <- Reduce(`+`, res)
# 전역 변수는 worker에서 보이지 않음:
# 명시적으로 내보내지 않으면 '객체를 찾을 수 없음'

난수 시드

병렬에서는 워커마다 독립적인 난수 시드가 필요하다. set.seed를 전역에 둔 뒤 clusterSetRNGStream 또는 future.seed로 재현 가능성을 확보한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
set.seed(42)
rnorm(3) # 재현 가능한 직렬
# 클러스터 병렬 시드:
# clusterSetRNGStream(cl)
# 또는:
# clusterSetRNGStream(cl, 123)
# foreach 병렬 시드:
# registerDoParallel(); set.seed(1)
# future는 자동 관리:
# future.seed = TRUE
# 재현성 검증:
set.seed(7)
a <- rnorm(5)
set.seed(7)
b <- rnorm(5)
identical(a, b) # TRUE

병렬 성능

병렬의 이득은 작업 단위 크기, 코어 수, 통신 비용에 따라 제한된다. 먼저 순차 코드의 병목을 프로파일링하고, 가장 큰 블록부터 병렬화한다.

1
2
3
4
5
6
7
8
9
10
11
12
# 병렬 오버헤드가 태스크보다 크면 병렬로 하지 않음
f <- function(i) sqrt(i)
system.time(lapply(1:1e5, f))
# system.time(mclapply(1:1e5, f, mc.cores = 4))
# 적합: 태스크 1건당 수십 ms 이상
# 선형 스케일링 관찰:
# time(1 core) vs time(4 cores)
# 통신 병목: 큰 데이터의 왕복 복사는 느림
# 권장: 결과를 작은 객체로 모아 반환
# 큰 공유 객체를 자주 넘기지 않기
# 속도 측정 도구:
# microbenchmark::microbenchmark(...)

15.네트워크 요청

다운로드, httr2/httr 요청, JSON API, URL 처리, 웹 스크레이핑.

파일 다운로드

download.file로 파일을 다운로드한다. mode로 바이너리를 지정한다. R.utils는 이어받기를 지원한다. 느린 연결은 timeout을 설정할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
download.file(
'https://example.com/data.csv',
'data.csv',
mode = 'wb'
)
# 타임아웃 지정:
# download.file(url, dest, timeout = 60)
# 결과 확인:
file.info('data.csv')$size
# 다운로드한 데이터 읽기:
read.csv('data.csv')
# 바이너리 데이터:
# download.file(url, 'img.png', mode = 'wb')
# 미러 관련:
# setInternet2(TRUE) # 구 Windows

httr2 요청

httr2는 차세대 HTTP 클라이언트다. req_perform으로 요청을 실행하고, resp_body_json으로 응답을 파싱한다. 파이프라인 방식으로 요청을 구성한다. </content> </invoke>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# library(httr2)
req <- request('https://api.example.com')
req <- req %>%
req_url_path_append('v1') |>
req_url_query(q = 'rlang') |>
req_headers('Authorization' = 'Bearer xx')
# resp <- req_perform(req)
# resp_body_json(resp)
# 오류 처리:
# tryCatch(req_perform(req), error = function(e) ...)
# 요청 제한:
# req_throttle(req, rate = 10)
# 인증:
# req_auth_bearer_token(req, 'token')

GET 요청

GET은 리소스를 읽습니다. base의 readLines로 텍스트를 가져올 수 있고, httr GET + content로 파싱합니다. query 파라미터는 URL에 붙입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
# base로 간단한 GET:
txt <- readLines('https://example.com')
# library(httr)
resp <- httr::GET('https://api.example.com/items')
httr::status_code(resp) # 200
httr::content(resp, 'text')
# 쿼리 파라미터 포함:
httr::GET('https://api.example.com/search',
query = list(q = 'rlang', page = 2))
# 응답 헤더:
httr::headers(resp)
# 연결 타임아웃:
httr::GET(url, httr::timeout(30))

POST 요청

POST는 데이터를 전송합니다. body에 JSON이나 폼을 담습니다. httr POST와 httr2 req_body_json 두 가지 스타일이 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# library(httr)
httr::POST(
'https://api.example.com/login',
body = list(user = 'a', pass = 'b'),
encode = 'form'
)
# JSON 바디:
httr::POST(
'https://api.example.com/submit',
body = '{"x":1}',
content_type_json()
)
# httr2 스타일:
# req_body_json(req, list(x = 1))
# 응답 파싱:
# resp <- httr::POST(...)
# httr::content(resp, 'parsed')
# 파일 업로드:
# httr::POST(url, body = list(f = upload_file('f.csv')))

JSON API

JSON API 호출: 요청 + jsonlite 파싱. 결과는 보통 중첩 리스트이므로 flatten으로 데이터 프레임으로 바꿉니다. 페이지네이션과 오류 처리도 함께 고려합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# library(httr)
# library(jsonlite)
url <- 'https://api.github.com/repos/tidyverse/dplyr'
resp <- httr::GET(url)
stopifnot(httr::status_code(resp) == 200)
js <- httr::content(resp, 'text')
info <- jsonlite::fromJSON(js)
info$full_name
info$stargazers_count
# 리스트를 데이터프레임으로:
# fromJSON(js, flatten = TRUE)
# 페이지네이션:
# query 파라미터 page / per_page로 루프
# 통일된 오류 처리:
# tryCatch(..., error = ...)

URL 처리

parse_url로 URL 구성 요소를 분해하고, URLencode로 인코딩합니다. URLdecode로 되돌립니다. base에 내장되어 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# library(httr)
u <- parse_url('https://user:[email protected]/a?b=1&c=2')
u$scheme; u$hostname; u$path
u$query # b=1 c=2
# 수정 후 재구성:
u$query$d <- 3
build_url(u)
# URL 인코딩:
URLencode('a b/c') # a%20b%2Fc
URLdecode('%E4%BD%A0%E5%A5%BD')
# 중국어 인코딩:
URLencode('你好')
# 상대 경로 해석:
# url_absolute('/a', 'https://x.com')
# 쿼리 문자열 파싱:
# parse_url('https://x.com/?a=1')$query

웹 스크레이핑

rvest로 HTML을 파싱합니다. html_elements로 노드를 선택하고, html_text로 텍스트를 가져옵니다. robots 규칙과 요청 빈도 제한을 지키세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# library(rvest)
url <- 'https://example.com'
page <- read_html(url)
page %>% html_elements('h1') %>% html_text()
page %>% html_elements('a') %>%
html_attr('href')
# 테이블:
# page %>% html_table()
# CSS 선택자:
page %>% html_elements('#main p')
page %>% html_elements('.item')
# XPath:
page %>% html_elements(xpath = '//div[@class="x"]')
# 사이트 규칙 존중:
# 요청 빈도 제한, User-Agent 명시
# 합법적 용도: 공개 데이터 분석

TCP 소켓

socketConnection으로 TCP 연결을 만들어 문자열을 읽고 씁니다. 간단한 프로토콜과 사내 서비스에 적합합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
con <- socketConnection(
host = 'localhost',
port = 9999,
server = FALSE, # 클라이언트
open = 'r+b'
)
# 쓰기:
writeLines('hello', con)
# 읽기:
line <- readLines(con, n = 1)
close(con)
# 서버 구성:
# srv <- socketConnection(host = 'localhost',
# port = 9999, server = TRUE)
# 주의: 블로킹으로 대기
# 복잡한 프로토콜은 httpuv / plumber 권장
# 운영 API는 raw socket보다 프레임워크 권장

16.시간과 날짜

Sys.Date / POSIXct, 포매팅, lubridate와 타임존.

현재 시각

Sys.Date는 오늘 날짜, Sys.time은 현재 날짜와 시간입니다. date는 현재 시각 문자열입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
Sys.Date() # 2026-08-02
Sys.time() # POSIXct의 날짜시간
format(Sys.time())
date() # "Sat Aug ... 2026"
# 구성 요소 가져오기:
as.POSIXlt(Sys.time())
unclass(as.POSIXlt(Sys.time()))
# 타임스탬프:
unclass(Sys.time()) # 초
# 시간대 확인:
Sys.timezone()
# 수동 설정:
# Sys.setenv(TZ = 'Asia/Shanghai')

날짜 기초

Date 클래스가 날짜 타입입니다. as.Date로 문자열을 변환하고, 일수를 더하거나 뺍니다. unclass로 일수 값을 확인합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
d <- as.Date('2026-08-02')
class(d) # "Date"
d + 1 # 내일
Sys.Date() - d # 차이 일수
difftime(Sys.Date(), d, units = 'days')
# 다른 형식 파싱:
as.Date('2026/08/02')
as.Date('02-08-2026', format = '%d-%m-%Y')
# 시퀀스:
seq(as.Date('2026-01-01'), by = 'day', length.out = 3)
# 구성 요소:
format(d, '%Y-%m-%d')
format(d, '%A') # 요일

POSIXct

POSIXct는 초 단위 타임스탬프를 저장하고, POSIXlt는 구성 요소로 분해합니다. as.POSIXct로 문자열을 파싱하며 타임존을 처리합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
t <- as.POSIXct('2026-08-02 10:30:00')
class(t) # POSIXct POSIXt
unclass(t) # 1970년부터의 초 수
# 시간대 지정:
as.POSIXct('2026-08-02 10:30:00', tz = 'UTC')
# 구성 요소로 변환:
lt <- as.POSIXlt(t)
lt$year + 1900 # 2026
lt$mon + 1 # 8월
lt$mday; lt$hour; lt$min
# 문자열로 포맷:
format(t, '%Y-%m-%d %H:%M:%S')
# 더하기/빼기:
t + 3600 # +1시간
# Date와 상호 변환:
as.Date(t)

포맷팅

strftime / format으로 지정자에 따라 출력합니다. %Y 연 %m 월 %d 일 %H 시 %M 분 %S 초.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
d <- Sys.Date()
t <- Sys.time()
format(d, '%Y-%m-%d')
format(d, '%d/%m/%Y')
format(d, '%A') # 요일명
format(t, '%H:%M:%S')
format(t, '%Y 年第 %j 天')
# 파싱:
as.Date('02/08/2026', format = '%d/%m/%Y')
# strptime:
strptime('2026-08-02 10:00', format = '%Y-%m-%d %H:%M')
# 플레이스홀더 요약:
# %Y 4자리 연도 %y 2자리 연도 %m 월 %d 일
# %H %M %S 시/분/초 %A 요일명
# %j 연의 몇 번째 날

lubridate 소개

lubridate는 친숙한 날짜 함수를 제공합니다: ymd / ymd_hms로 파싱, year / month로 구성 요소 추출, 간격 덧셈과 뺄셈.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# library(lubridate)
ymd('20260802')
ymd('2026-08-02')
ymd_hms('2026-08-02 10:30:00')
# 구성 요소 가져오기:
year(t); month(t); day(t)
hour(t); minute(t); second(t)
wday(t) # 요일(숫자)
# 더하기/빼기:
t + days(1); t - weeks(2)
months(3) # 3개월
# 간격:
interval(d, Sys.Date()) |> as.duration()
# 시간대 설정:
with_tz(t, 'UTC')
# 반올림:
round_date(t, 'hour')
floor_date(t, 'day')

날짜 시퀀스

seq로 날짜 시퀀스를 만듭니다: by로 일/월/년 단위 지정. 임의 간격이나 영업일에는 seq.Date를 씁니다.

1
2
3
4
5
6
7
8
9
10
11
seq(as.Date('2026-01-01'), as.Date('2026-01-10'), by = 'day')
seq(as.Date('2026-01-01'), by = 'month', length.out = 3)
seq(as.Date('2026-01-01'), by = 'year', length.out = 2)
# 개수 지정:
seq(as.Date('2026-01-01'), as.Date('2026-12-31'), length.out = 5)
# seq.Date가 더 명확:
seq.Date(as.Date('2026-01-01'), by = '2 days', length.out = 3)
# 평일과의 관계:
# 주말 제외:
x <- seq(as.Date('2026-01-01'), by = 'day', length.out = 30)
x[!weekdays(x) %in% c('Saturday', 'Sunday')]

시간 차이

날짜를 빼면 difftime이 됩니다. units로 단위를 지정합니다. as.numeric으로 수치를 얻습니다. 소요 시간 측정에 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
t0 <- Sys.time()
Sys.sleep(0.5)
Sys.time() - t0 # Time difference
# 단위 지정:
difftime(Sys.time(), t0, units = 'secs')
difftime(Sys.time(), t0, units = 'mins')
# 숫자로 추출:
as.numeric(Sys.time() - t0)
# 날짜 차이:
Sys.Date() - as.Date('2026-01-01')
# 평균 시간:
# mean은 difftime에 유효
# 단위 커스텀:
# difftime(t2, t1, units = 'hours')
# 측정 도구:
# system.time(expr)
# tictoc::tic() / toc()

타임존

tz 인자로 타임존을 지정합니다. Sys.timezone은 현재 타임존입니다. OlsonNames는 유효한 타임존을 나열합니다. 타임존이 다른 시각도 비교할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
Sys.timezone() # 현재 시간대
OlsonNames() # 유효한 모든 시간대 이름
# 시간대 지정 파싱:
as.POSIXct('2026-08-02 10:00', tz = 'Asia/Shanghai')
# 같은 시각을 다른 시간대로 표시:
t <- as.POSIXct('2026-08-02 10:00', tz = 'UTC')
format(t, tz = 'Asia/Shanghai')
format(t, tz = 'America/New_York')
# 시간대 변환:
with_tz(t, 'Asia/Shanghai') # lubridate
# UTC 타임스탬프:
# as.integer(as.POSIXct('2026-08-02', tz = 'UTC'))

17.프로세스와 환경

시스템 명령 실행, 환경 변수, 명령줄 인자, 플랫폼 정보와 경로.

명령 실행

system은 시스템 명령을 실행하고 종료 코드를 반환합니다. system2는 인자 전달이 더 안전합니다. 출력 캡처는 capture나 intern을 씁니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
system('echo hello')
system2('echo', 'hello')
# 출력 캡처:
system('date', intern = TRUE)
system2('date', stdout = TRUE)
# 종료 코드:
code <- system('ls')
code # 0 성공
# 인자 전달로 셸 인젝션 회피:
# system2('cp', c('a.txt', 'b.txt'))
# 출력 무시:
system('echo x', ignore.stdout = TRUE)
# 타임아웃:
# system2('cmd', timeout = 10)

환경 변수

Sys.getenv로 읽고, Sys.setenv로 씁니다. unsetenv로 삭제합니다. PATH나 R 관련 변수를 자주 다룹니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Sys.getenv('PATH')
Sys.setenv(MY_VAR = 'hello')
Sys.getenv('MY_VAR') # "hello"
Sys.unsetenv('MY_VAR')
# 전체 나열:
Sys.getenv()
# 기본값 지정:
Sys.getenv('NOPE', unset = 'default')
# 자주 쓰는 변수:
Sys.getenv('HOME')
Sys.getenv('R_VERSION') # 또는:
R.version.string
# 조건 판단:
if (nzchar(Sys.getenv('CI'))) print('在 CI 环境')

명령행 인수

commandArgs로 스크립트 인자를 가져옵니다. trailingOnly로 R 자체 인자를 제거합니다. 파싱은 optparse를 쓰거나 base로 직접 작성합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 실행: Rscript app.R --name Rex
args <- commandArgs(trailingOnly = TRUE)
args # "--name" "Rex"
# 간단한 파싱:
get_arg <- function(name) {
i <- which(args == name)
if (length(i)) args[i + 1] else NULL
}
get_arg('--name')
# 본격적인 파싱:
# library(optparse)
# parser <- OptionParser(option_list = list(
# make_option('--name', type = 'character')))
# opts <- parse_args(parser)
# 기본값:
# 인자 개수 검증:
stopifnot(length(args) >= 1)

종료 상태

quit으로 R을 종료합니다. q('no')는 작업 공간을 저장하지 않습니다. 오류 종료는 quit(status = 1)로 스크립트가 판단하게 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
quit(save = 'no') # 저장하지 않고 종료
# 상태 코드 지정:
# quit(status = 1) # 실패로 종료
# 조건부 종료:
if (!file.exists('data.csv')) {
message('缺文件')
quit(status = 1)
}
# 정상 종료:
# Rscript는 스크립트 끝에서 자연 종료
# 종료 코드 확인(shell):
# Rscript app.R; echo $?
# 종료를 멈추고 browse:
# browser() # 디버그에서 일시 정지

플랫폼 정보

R.version은 버전, Sys.info는 시스템 정보, .Platform은 플랫폼 세부 정보입니다. 경로 구분자 등 크로스 플랫폼 처리에 씁니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
R.version.string # R 4.4.x
version # 완전한 버전
Sys.info() # 시스템 정보 리스트
Sys.info()['sysname'] # Windows/Linux/Darwin
.Platform$file.sep # / 또는 \
.Platform$OS.type # "windows" / "unix"
# 조건 판단:
if (.Platform$OS.type == 'windows') 'Win' else 'Unix'
# 아키텍처:
R.version$arch
# 경로 구분자:
# file.path가 자동 처리:
file.path('a', 'b', 'c.txt') # 플랫폼에 맞는 구분자

경로 관리

file.path로 경로를 결합하고, dirname / basename으로 분해하며, normalizePath로 절대 경로를 정규화합니다. file.exists로 존재 여부를 확인합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
file.path('data', 'sub', 'f.csv')
basename('a/b/f.csv') # "f.csv"
dirname('a/b/f.csv') # "a/b"
file.exists('f.csv')
dir.exists('data')
# 작업 디렉터리:
getwd()
setwd('data') # 변경은 신중히
# 정규화 경로:
normalizePath('..')
# ~ 펼치기:
path.expand('~/R')
# 디렉터리 나열:
list.files('.')
list.files('.', pattern = '\\.csv$')
# 디렉터리 생성:
dir.create('out', showWarnings = FALSE)

지연 대기

Sys.sleep은 지정한 초만큼 일시 정지합니다. 요청 속도 제한이나 외부 리소스 준비 대기에 쓸 수 있습니다. 단위는 초입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Sys.sleep(1) # 1초 중지
t0 <- Sys.time()
Sys.sleep(0.5)
Sys.time() - t0
# 루프의 레이트 제한:
for (i in 1:3) {
cat(i, '\n')
Sys.sleep(0.2) # 0.2초마다
}
# 파일 등장 대기:
# while (!file.exists('done.txt')) Sys.sleep(1)
# 네트워크 리소스 대기:
# httr의 timeout과 조합
# 주의: 현재 프로세스를 블록

Rscript 스크립트

Rscript는 비대화식 실행 진입점으로, 예약 작업과 배치 처리에 적합합니다. 스크립트 첫 줄에 shebang을 넣을 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 스크립트 시작 부분(Unix):
# #!/usr/bin/env Rscript
# 실행:
# $ Rscript script.R arg1
# 사용:
args <- commandArgs(trailingOnly = TRUE)
message('开始处理: ', args[1])
# 구조화된 결과 출력:
cat('RESULT:', mean(1:100), '\n')
# 종료 코드:
# 오류 시 quit(status = 1)
# 배치 파이프라인:
# stdin 읽기:
input <- readLines(file('stdin'), n = 1)
# stdout에 쓰면 파이프로 다음에 전달 가능

18.정규 표현식

grep / grepl, gsub 치환, regexpr 위치 탐색, stringr와 자주 쓰는 패턴.

grep과 grepl

grep은 일치한 인덱스를, grepl은 논리 벡터를 반환합니다. ignore.case로 대소문자를 무시합니다. value는 일치한 값을 반환합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
x <- c('apple', 'banana', 'apricot')
grepl('^ap', x) # TRUE FALSE TRUE
grep('^ap', x) # 1 3
grep('^ap', x, value = TRUE) # "apple" "apricot"
grep('^AP', x, ignore.case = TRUE)
# 고정 매치:
grepl('a.', x, fixed = TRUE) # 리터럴 .
# 카운트:
sum(grepl('a', x))
# 반전 선택:
x[!grepl('^ap', x)]
# 복수 패턴:
grepl('a|b', c('x', 'a'))

gsub 치환

gsub은 모든 일치를 치환하고, sub은 첫 번째만 치환합니다. \\1로 캡처 그룹을 참조합니다. perl=TRUE로 확장 문법을 씁니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
gsub('a', 'o', 'banana') # "bonono"
sub('a', 'o', 'banana') # "bonana"
# 매치 삭제:
gsub('[0-9]', '', 'a1b2')
# 캡처 그룹 참조:
gsub('(\\d{4})-(\\d{2})', '\\2/\\1', '2026-08')
# 복수 치환:
gsub('a|b', 'x', 'abacus')
# 대소문자 무시:
gsub('a', 'o', 'ABC', ignore.case = TRUE)
# 고정 리터럴:
gsub('a.b', 'x', 'a.b', fixed = TRUE)
# 벡터화:
gsub('s', 'S', c('sun', 'sea'))

regexpr 위치 탐색

regexpr은 첫 일치 위치와 길이를 반환하고, gregexpr은 전부 반환합니다. regmatches로 일치 텍스트를 추출합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
s <- 'price is 99 dollars'
m <- regexpr('[0-9]+', s)
m # 위치와 길이
regmatches(s, m) # "99"
# 전체 매치:
ms <- gregexpr('[a-z]+', 'a1bc2def')
regmatches('a1bc2def', ms) # 리스트
# 매치와 캡처 추출:
# 정규식 매치 후 unlist:
unlist(regmatches('x12y34', gregexpr('[0-9]+', 'x12y34')))
# 매치되지 않을 때:
regexpr('z', 'abc') # -1
# 매치 위치를 인덱스로:
# substring과 조합해 사용

문법 기초

기본 정규식 메타 문자: ^ 시작 $ 끝 . 임의 문자 . 문자 클래스 [] 그룹 () 수량자 * + ? {} 이스케이프 \\.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
'^abc' # abc로 시작
'abc$' # abc로 끝남
'^[a-z]+$' # 전부 소문자
'[0-9]{2,4}' # 2~4자리 숫자
'colou?r' # colour/color
'a.c' # a?c 사이 임의 문자
'\\.' # 리터럴 마침표
'[^0-9]' # 숫자 외
'(ab|cd)' # 그룹 OR
'\\d' '\\w' '\\s' # 숫자/영숫자/공백
# R에서는 백슬래시를 이중으로 씀:
# 정규식 \d는 R 문자열에서 \\d
# 검증:
grepl('^[0-9]+$', '123') # TRUE

자주 쓰는 패턴

자주 쓰는 패턴 모음: 이메일, 전화번호, 날짜, 공백 정리, 숫자 추출. 필요에 맞게 조정하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 숫자 추출:
regmatches('v1.2.3', regexpr('[0-9]+', 'v1.2.3'))
# 공백 제거:
gsub('[[:space:]]', '', 'a b c')
# 간단한 이메일:
pat <- '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$'
grepl(pat, '[email protected]') # TRUE
# 날짜 YYYY-MM-DD:
pat <- '^\\d{4}-\\d{2}-\\d{2}$'
grepl(pat, '2026-08-02')
# 중국어:
grepl('[\\u4e00-\\u9fa5]', '你好')
# 괄호 안 내용 추출:
sub('.*\\((.+)\\).*', '\\1', 'name(value)')
# 구분:
strsplit('a,b;c', '[,;]')

stringr 매칭

stringr은 정규식 문법은 같지만 인터페이스가 일관됩니다. str_detect / str_extract / str_match와 _all 버전이 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# library(stringr)
str_detect(c('a1', 'b2'), '\\d')
str_extract('price 99', '\\d+')
str_extract_all('a1 b22', '\\d+')
str_match('a=1;b=2', 'a=(\\d+)')
str_match_all('a1b2', '([ab])(\\d)')
# 위치:
str_locate('abcabc', 'b')
str_locate_all('abcabc', 'b')
# 치환:
str_replace('a-b', '-', '_')
str_replace_all('a-b-c', '-', '+')
# 정규식 경계 조립:
str_detect('apple', regex('^ap'))

stringr 치환

str_replace / str_replace_all로 치환합니다. regexp에는 fixed나 perl을 쓸 수 있습니다. str_remove는 일치 부분을 삭제합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# library(stringr)
str_replace('one-two', '-', '_')
str_replace_all('a-b-c', '-', '')
# 캡처 참조:
str_replace('2026-08', '(\\d{4})-(\\d{2})', '\\2/\\1')
# 삭제:
str_remove('xx-abc', 'xx-')
str_remove_all('a1b2', '\\d')
# 고정 리터럴:
str_replace_all('a.b', fixed('.'), 'X')
# 대소문자 무시:
str_replace('ABC', 'a', 'x', regex(ignore_case = TRUE))
# 벡터화는 자동:
str_replace_all(c('a1', 'b2'), '\\d', '#')
# 경계 매치:
str_replace('abc', '^a', 'X')

플래그와 확장

perl=TRUE는 PCRE 확장을 활성화합니다: 전방 탐색, 명명 그룹. fixed=TRUE는 리터럴 매칭입니다. ignore.case도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 전방 탐색:
grepl('a(?=b)', 'ab', perl = TRUE) # TRUE
# 부정 전방 탐색:
grepl('a(?!b)', 'ac', perl = TRUE) # TRUE
# 이름 있는 그룹:
# gsub('(?<y>\\d+)', '\\k<y>', s, perl = TRUE)
# 비탐욕적:
regmatches('a<b>c<b>', regexpr('<.+?>', 'a<b>c<b>', perl = TRUE))
# 복수행 / 점이 개행에 매치:
# grepl('a.b', 'a\nb', perl = TRUE)
# fixed 리터럴:
grepl('a.b', 'a.b', fixed = TRUE)
# 플래그 조합:
# grepl(pat, x, ignore.case = TRUE, perl = TRUE)

19.패키지 관리와 빌드

CRAN 설치, renv 의존성 관리, 패키지 구조와 R CMD, testthat, roxygen.

패키지 설치

install.packages로 CRAN에서 설치합니다. update.packages로 갱신합니다. library로 로드합니다. 개발 버전은 devtools / remotes를 씁니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
install.packages('dplyr')
# 미러에서 설치:
install.packages('ggplot2', repos = 'https://cloud.r-project.org')
# 업데이트:
# update.packages()
# 개발 버전:
# remotes::install_github('tidyverse/dplyr')
# 로컬:
# install.packages('path/pkg_0.1.0.tar.gz', repos = NULL)
# 설치된 패키지 확인:
rownames(installed.packages())
# 로드:
library(dplyr)
# 의존성 설치:
# install.packages(c('dplyr', 'tidyr'))

CRAN과 저장소

CRAN은 공식 패키지 저장소입니다. repos로 미러를 지정합니다. available.packages로 설치 가능한 목록을 봅니다. CRAN 정책이 배포를 제약합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 현재 리포지토리:
getOption('repos')
# 사용 가능한 전체 패키지:
ap <- available.packages()
ap[1:5, 'Package']
# 패키지 검색:
# available.packages()를 필드로 grep
# 패키지 정보:
# packageDescription('dplyr')
# 의존성:
# packageDescription('dplyr')$Depends
# CRAN 체크:
# R CMD check를 통과해야 공개 가능
# 미러 목록:
# https://cran.r-project.org/mirrors.html
# 중국 미러:
# https://mirrors.tuna.tsinghua.edu.cn/CRAN/

renv 의존성 관리

renv는 프로젝트 의존성 버전을 고정하며, Python venv와 비슷합니다. renv::init으로 초기화하고, snapshot / restore로 잠금 파일을 동기화합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 초기화:
# renv::init()
# 의존성 설치:
# renv::install('dplyr')
# 의존성 기록:
# renv::snapshot() # renv.lock 작성
# 다른 머신에서 복원:
# renv::restore()
# 상태 확인:
# renv::status()
# 프로젝트 라이브러리 분리:
# renv::activate()
# 업그레이드:
# renv::update()
# 사용상 주의:
# .Rprofile이 renv를 자동 로드
# renv.lock을 버전 관리에 커밋

패키지 구조

R 패키지 표준 구조: DESCRIPTION, R/, man/, tests/, NAMESPACE. R/에는 소스 코드를, man/에는 문서를 둡니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# mypkg/
# ├── DESCRIPTION 메타데이터: 패키지 이름/버전/의존
# ├── NAMESPACE 내보낼 함수
# ├── R/ 소스 파일 *.R
# ├── man/ 문서 *.Rd
# ├── tests/ 테스트
# └── data/ 내장 데이터
# DESCRIPTION의 주요 필드:
# Package: mypkg
# Version: 0.1.0
# Imports: dplyr
# usethis로 뼈대 생성:
# usethis::create_package('mypkg')
# 문서:
# usethis::use_roxygen_md()

R CMD 명령

R CMD 계열 명령으로 패키지를 빌드/검사합니다: build로 패키징, check로 검사, INSTALL로 설치. 배포 전에는 반드시 check를 거칩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# tar.gz 빌드:
# R CMD build mypkg
# 체크(중요한 단계):
# R CMD check mypkg_0.1.0.tar.gz
# 설치:
# R CMD INSTALL mypkg
# 자주 쓰는 체크 항목:
# --as-cran으로 CRAN 엄격 체크 시뮬레이션
# 실행 시 대체:
# system('R CMD INSTALL --help')
# R 세션 내에서:
# system2('Rscript', c('-e', '1+1'))
# devtools 래퍼:
# devtools::check()

testthat 테스트

testthat은 주류 테스트 프레임워크입니다. expect_equal 같은 단언과 test_that으로 케이스를 구성합니다. usethis로 테스트 파일을 생성합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# library(testthat)
f <- function(x) x * 2
test_that('f 正确加倍', {
expect_equal(f(2), 4)
expect_identical(f(0), 0)
expect_true(f(-1) < 0)
expect_error(f('a'))
})
# 실행:
# testthat::test_dir('tests')
# 또는 개발 중:
# devtools::test()
# 자주 쓰는 단언:
# expect_equal / expect_identical
# expect_true / expect_false
# expect_warning / expect_message
# CI와 결합해 자동 실행

포맷과 검사

styler로 코드 스타일을 통일하고, lintr로 정적 검사를 합니다. RStudio Addins나 pre-commit과 함께 쓰면 깔끔하게 유지됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# library(styler)
# 파일 포맷:
# styler::style_file('R/foo.R')
# 패키지 전체 포맷:
# styler::style_pkg()
# 스타일 차이 확인:
# styler::style_file('R/foo.R', dry = 'only')
# lintr 체크:
# lintr::lint('R/foo.R')
# 자주 쓰는 설정:
# .lintr 파일에서 linters 지정
# 자동 실행:
# pre-commit hooks
# CI 통합:
# lintr를 check 단계로

roxygen 문서

roxygen2는 주석으로 .Rd 문서를 생성합니다. #'로 시작하며 @param / @return / @export 태그를 씁니다. 함수 문서가 소스 코드와 같은 곳에 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#' 두 수를 더함
#'
#' @param a 첫 번째 수
#' @param b 두 번째 수
#' @return
#' @export
add2 <- function(a, b) a + b
# 문서 생성:
# devtools::document()
# 또는:
# roxygen2::roxygenise()
# NAMESPACE의 export 생성:
# @export가 자동으로 NAMESPACE에 기록됨
# 패키지 레벨 문서:
# @keywords internal
# 문서 확인:
# R CMD check

세션 재현

sessionInfo는 버전과 의존성을 기록해 재현성을 보장합니다. renv.lock으로 의존성을 고정합니다. R 버전도 함께 기록해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sessionInfo() # R 버전 + 로드된 패키지
R.version.string
# 출력 예:
# R version 4.4.x
# Platform: ...
# 첨부된 패키지와 그 버전
# 재현의 핵심:
# 1. sessionInfo() 출력 기록
# 2. renv::snapshot()으로 의존성 잠금
# 3. R 버전 고정(Docker 이미지)
# 누락 확인:
# 의존성 목록:
# renv::dependencies()
# 문제 보고 시 첨부:
# sessionInfo() + 최소 재현 코드

공식 링크

공식 문서와 리소스에 대한 직접 링크.

이 치트시트에 대해

이 페이지는 R 4.4의 자기 완결형 치트시트로, 실제 데이터 분석에서 base R과 tidyverse가 가장 많이 쓰이는 약 80%의 시나리오를 다룹니다. 내용은 현대적 관용구에 초점을 둡니다: 네이티브 파이프 `|>`, 익명 함수 `\(x)` 축약, `stringsAsFactors = FALSE`와 함께 쓰는 `data.frame`, 벡터화된 `ifelse`, `purrr::map_*` 타입 기반 집합 연산, 그리고 R 특유의 수정 시 복사(copy-on-modify)와 environment 참조 의미론. 권위 있는 참고 자료로는 공식 R 매뉴얼과 R for Data Science가 있습니다. 19개 장은 각각 하나의 주제에 집중합니다 — 첫 프로그램, 변수와 타입부터 apply 계열, S3/R6 객체 지향, 병렬과 네트워크까지. 각 장은 예제가 있는 8–9개의 소절(각 5–20줄)로 나뉘며, 총 약 160개 주제입니다. 코드 조각은 일부러 짧고 자기 설명적으로 작성했으며, 복사해서 R이나 RStudio에 바로 붙여 넣어 실행할 수 있습니다. 모든 처리는 브라우저 안에서 이루어집니다 — 업로드도, 추적도 없습니다. 이 페이지는 GuruToolkit 무료 개발자 도구 모음의 일부이며, 코드 조각은 어떠한 보증도 없이 자유롭게 사용할 수 있습니다.

버전 2.1.0