벡터와 기본
변수, 타입과 할당
R은 <-를 선호하는 할당 연산자로 사용합니다(=도 작동하지만 함수 인자에서 모호할 수 있습니다). R의 모든 것은 벡터입니다 — 단일 숫자조차 길이 1 벡터입니다. 핵심 타입은 character, numeric(double), integer, logical, complex입니다. NA는 결측 데이터를 나타내고 연산을 통해 전파됩니다(건너뛰려면 na.rm=TRUE 사용). NULL은 값의 부재(빈 객체)로, NA와 다릅니다. 분석 전에 항상 NA를 확인하세요.
# assignment operators (all equivalent for simple values)
name <- "Alice" # preferred
age = 30L # = also works (avoid in functions)
30L -> age2 # rightward assignment
# basic types (called "modes" in R)
class("text") # "character"
class(42) # "numeric" (double)
class(42L) # "integer"
class(3.14) # "numeric"
class(TRUE) # "logical"
class(2 + 3i) # "complex"
# check and convert types
is.numeric(age) # TRUE
is.character(name) # TRUE
as.integer(3.9) # 3 (truncates, not rounds)
as.character(42) # "42"
as.numeric("3.14") # 3.14
# special values
NA # missing value (Not Available)
NULL # null object (no value)
NaN # Not a Number (0/0)
Inf # infinity (1/0)
is.na(NA) # TRUE
is.null(NULL) # TRUE벡터 생성과 인덱싱
c()는 값을 벡터로 결합 — 가장 근본적인 R 함수입니다. R은 1부터 인덱싱합니다(첫 번째 요소는 [1]이지 [0]이 아님). 음수 인덱스는 요소를 제외: nums[-1]은 첫 번째를 제거합니다. 논리 인덱싱(nums[nums > 3])은 조건으로 필터링 — 매우 강력합니다. 벡터는 이름을 가질 수 있어 라벨로 접근 가능합니다. 벡터의 모든 요소는 같은 타입이어야 하며; 타입을 혼합하면 R이 강제 변환합니다(예: c(1, 'a')는 c('1', 'a')가 됨).
# create vectors with c() (combine)
nums <- c(1, 2, 3, 4, 5)
chars <- c("a", "b", "c")
logs <- c(TRUE, FALSE, TRUE)
# sequences
1:5 # 1 2 3 4 5
seq(1, 10, by = 2) # 1 3 5 7 9
seq(0, 1, length.out = 5) # 0 0.25 0.5 0.75 1
rep(1:3, times = 2) # 1 2 3 1 2 3
rep(1:3, each = 2) # 1 1 2 2 3 3
# indexing (1-indexed!)
nums[1] # 1 (first element)
nums[length(nums)] # 5 (last element)
nums[c(1, 3, 5)] # 1 3 5 (multiple indices)
nums[-1] # 2 3 4 5 (all EXCEPT first)
nums[-c(1, 2)] # 3 4 5 (exclude first two)
nums[2:4] # 2 3 4 (range)
# logical indexing
nums[nums > 2] # 3 4 5
nums[nums %% 2 == 0] # 2 4 (even numbers)
nums[nums > 2] <- 0 # modify in place
# named vectors
ages <- c(alice = 30, bob = 25, carol = 28)
ages["alice"] # 30
ages[c("alice", "bob")] # 30 25벡터 연산과 함수
R의 벡터화는 핵심 기능 — 연산이 자동으로 요소별로 적용되며, 루프가 필요 없습니다. 재활용은 더 짧은 벡터를 재사용하여 더 긴 것에 맞춥니다(c(1,2,3,4) + c(10,20)은 11,22,13,24를 줌). order()는 벡터를 정렬할 인덱스를 반환 — 한 벡터를 다른 것으로 정렬하는 데 필수적입니다. unique()는 중복을 제거합니다. 이 모든 함수는 최적화된 C 코드로 구현되어 R의 벡터 연산을 빠르게 합니다.
# arithmetic is element-wise (vectorized)
a <- c(1, 2, 3)
b <- c(4, 5, 6)
a + b # 5 7 9
a * b # 4 10 18
a ^ 2 # 1 4 9
a / b # 0.25 0.4 0.5
# recycling: shorter vector repeats
c(1, 2, 3, 4) + c(10, 20) # 11 22 13 24
# summary functions
sum(a) # 6
mean(a) # 2
median(a) # 2
sd(a) # 1
var(a) # 1
min(a); max(a) # 1; 3
range(a) # 1 3
cumsum(a) # 1 3 6
cumprod(a) # 1 2 6
# sorting and ordering
sort(c(3, 1, 2)) # 1 2 3
sort(c(3, 1, 2), decreasing = TRUE) # 3 2 1
order(c(3, 1, 2)) # 2 3 1 (indices that would sort)
v[order(v)] # sort using order
# useful functions
length(a) # 3
unique(c(1, 1, 2, 2, 3)) # 1 2 3
rev(a) # 3 2 1
head(a, 2) # 1 2 (first n)
tail(a, 2) # 2 3 (last n)문자와 문자열 조작
paste/paste0는 R의 문자열 결합 함수 — paste0는 구분자가 없습니다(Python의 +처럼). substr은 부분문자열을 추출(1부터 인덱싱). gsub는 모든 일치를 교체; sub는 첫 번째만 교체합니다. grep은 일치하는 요소의 인덱스를 반환; grepl은 논리 벡터를 반환(필터링에 더 유용). R은 기본적으로 POSIX 확장 정규식을 사용합니다. sprintf는 C 스타일 형식화를 제공합니다. stringr 패키지(tidyverse)는 더 깔끔하고 일관된 API를 제공합니다.
# paste and paste0 (concatenation)
paste("Hello", "World") # "Hello World"
paste("Hello", "World", sep = "_") # "Hello_World"
paste0("a", "b", "c") # "abc" (no separator)
paste(c("a", "b", "c"), collapse = "-") # "a-b-c"
paste("file", 1:3, ".csv", sep = "") # "file1.csv" "file2.csv" "file3.csv"
# case conversion
toupper("hello") # "HELLO"
tolower("WORLD") # "world"
# substring
substr("Hello World", 1, 5) # "Hello"
nchar("Hello") # 5 (character count)
# split and replace
strsplit("a,b,c", ",")[[1]] # "a" "b" "c"
gsub("o", "0", "Hello World") # "Hell0 W0rld" (all matches)
sub("o", "0", "Hello World") # "Hell0 World" (first match only)
gsub("[0-9]+", "N", "a1b22c333") # "aNbNcN" (regex)
# grep and grepl (pattern matching)
grep("^A", c("Alice", "Bob", "Anna")) # 1 3 (indices)
grepl("^A", c("Alice", "Bob")) # TRUE FALSE
# sprintf (C-style formatting)
sprintf("Pi = %.2f", pi) # "Pi = 3.14"
sprintf("%s is %d", "Alice", 30) # "Alice is 30"결측값과 강제 변환
NA(사용 불가)는 결측 데이터를 나타내고 대부분의 연산을 통해 전파 — 항상 na.rm=TRUE를 사용하거나 필터링하세요. NULL은 다릅니다: 값의 부재이며 벡터에서 제거됩니다. R은 결합 시 가장 일반적인 타입으로 강제 변환합니다(logical < integer < numeric < character). 숫자가 아닌 문자열에 as.numeric을 사용하면 경고와 함께 NA를 생성합니다. ifelse는 벡터화된 삼항 연산자 — 연속형에서 범주형 변수를 만드는 데 매우 유용합니다.
# NA propagation and handling
x <- c(1, 2, NA, 4, 5)
mean(x) # NA (NA propagates!)
mean(x, na.rm = TRUE) # 3 (skip NA)
sum(x, na.rm = TRUE) # 12
is.na(x) # FALSE FALSE TRUE FALSE FALSE
sum(is.na(x)) # 1 (count of NAs)
x[!is.na(x)] # 1 2 4 5 (remove NAs)
# NULL vs NA
length(c(1, NA, 3)) # 3 (NA is an element)
length(c(1, NULL, 3)) # 2 (NULL is dropped)
# type coercion hierarchy: logical < integer < double < character
c(TRUE, 1) # 1 1 (logical -> numeric)
c(1L, 2.5) # 1.0 2.5 (integer -> double)
c(1, "a") # "1" "a" (numeric -> character)
# explicit coercion
as.numeric(c("1", "2", "abc")) # 1 2 NA (with warning)
as.logical(c(0, 1, 2)) # FALSE TRUE TRUE
factor(c("low", "high", "low")) # factor with 2 levels
# ifelse vectorized conditional
ifelse(x > 2, "big", "small") # "small" "small" NA "big" "big"데이터 구조
리스트(이질적 컨테이너)
리스트는 R의 가장 유연한 데이터 구조 — 모든 타입과 크기의 요소를 보유할 수 있습니다(Python dict나 JavaScript 객체처럼). $ 연산자는 이름 접근을 위한 편리한 단축키입니다. 핵심 구분: [ ]은 서브리스트를 반환(여전히 리스트), [[ ]]은 실제 요소를 추출합니다. 이것이 R 초보자의 #1 혼란 원인입니다. 값 자체를 원할 때 [[ ]]을 사용하고, 서브셋을 원할 때 [ ]을 사용하세요. lapply/sapply는 리스트 요소에 함수를 적용하며 반복합니다.
# lists can hold different types and sizes
user <- list(
name = "Alice",
age = 30,
scores = c(90, 85, 88),
active = TRUE
)
# access by name ($ or [[]])
user$name # "Alice"
user[["age"]] # 30
user[["scores"]][2] # 85
# [] returns a sublist (list); [[]] returns the element
user["name"] # list with one element
user[["name"]] # "Alice" (the string itself)
user[1:2] # sublist with first 2 elements
# modify and add
user$age <- 31
user$email <- "[email protected]" # add new element
user[["scores"]] <- NULL # remove element
# iterate over a list
for (key in names(user)) {
cat(key, ":", user[[key]], "\n")
}
# lapply and sapply on lists
lapply(user$scores, sqrt) # list of square roots
sapply(user$scores, sqrt) # vector of square roots데이터 프레임
데이터 프레임은 R의 주요 표 형식 데이터 구조 — 스프레드시트나 SQL 테이블처럼 각 열이 다른 타입일 수 있습니다. $ 또는 [[ ]]로 열에 접근; 논리 인덱싱으로 행 필터링(df[df$age > 25, ]). subset()이 더 깔끔한 대안입니다. cbind는 열을 추가; rbind는 행을 추가합니다. str()은 구조를 보여줍니다(타입과 미리보기). 항상 stringsAsFactors=FALSE를 설정하세요(또는 R 4.0+를 사용 — 여기서는 기본값). 문자열을 팩터가 아닌 문자로 유지합니다.
# create a data frame (like a table/spreadsheet)
df <- data.frame(
name = c("Alice", "Bob", "Carol"),
age = c(30, 25, 28),
score = c(90, 85, 88),
stringsAsFactors = FALSE
)
# dimensions
nrow(df) # 3
ncol(df) # 3
dim(df) # 3 3
names(df) # "name" "age" "score"
str(df) # structure summary
head(df, 2) # first 2 rows
# access columns
df$name # vector (by name)
df[["age"]] # vector (by name, alternative)
df[, "age"] # vector (column)
df[, 2] # vector (by index)
df[2] # data frame with one column
# access rows
df[1, ] # first row (as data frame)
df[1:2, ] # first 2 rows
df[c(1, 3), ] # rows 1 and 3
# filter rows (logical indexing)
df[df$age > 26, ] # rows where age > 26
df[df$score >= 88, c("name", "score")]
subset(df, age > 26, select = c(name, score))
# add and modify columns
df$grade <- c("A", "B", "A") # add column
df$age <- df$age + 1 # modify column
df <- cbind(df, pass = df$score > 60) # column bind
# add rows
new_row <- data.frame(name = "Dan", age = 22, score = 75, grade = "C")
df <- rbind(df, new_row)행렬과 배열
행렬은 모든 요소가 같은 타입이어야 하는 2D 배열(데이터 프레임과 다름). %*%는 행렬 곱셈; *는 요소별입니다. solve()는 행렬의 역을 계산; det()는 행렬식입니다. rowSums/colSums/rowMeans/colMeans는 빠른 내장 단축키입니다. apply(m, MARGIN, FUN)은 행(MARGIN=1) 또는 열(MARGIN=2)에 걸쳐 함수를 적용하는 일반적 방법입니다. 배열은 행렬을 n차원으로 확장합니다. 데이터 분석에는 데이터 프레임을 선호; 선형 대수에는 행렬을 사용하세요.
# create a matrix (2D, all same type)
m <- matrix(1:12, nrow = 3, ncol = 4)
# [,1] [,2] [,3] [,4]
# [1,] 1 4 7 10
# [2,] 2 5 8 11
# [3,] 3 6 9 12
# fill by row instead of column
m2 <- matrix(1:6, nrow = 2, byrow = TRUE)
# dimensions and attributes
dim(m) # 3 4
nrow(m) # 3
ncol(m) # 4
rownames(m) <- c("r1", "r2", "r3")
colnames(m) <- c("c1", "c2", "c3", "c4")
# indexing
m[2, 3] # 8 (single element)
m[1, ] # 1 4 7 10 (first row)
m[, 2] # 4 5 6 (second column)
m[1:2, 2:3] # 2x2 submatrix
m["r1", "c2"] # 4 (by name)
# matrix operations
t(m) # transpose
m * m # element-wise multiply
m %*% t(m) # matrix multiplication
solve(m[, 1:3]) # inverse (square matrix)
det(matrix(1:4, 2)) # determinant
rowSums(m) # sum of each row
colMeans(m) # mean of each column
apply(m, 1, sum) # row sums (general)
apply(m, 2, max) # column maxima
# arrays (n-dimensional)
arr <- array(1:24, dim = c(2, 3, 4)) # 2x3x4 array
arr[1, 2, 3] # single element팩터와 범주형 데이터
팩터는 범주형 데이터를 정수 코드와 라벨 매핑으로 효율적으로 저장 — 통계 모델링에 필수(lm, glm은 그룹화에 팩터 사용). 흔한 함정: as.numeric(factor)는 원래 값이 아닌 정수 코드를 줍니다 — 항상 as.character를 통해 먼저 변환하세요. cut()은 연속형 데이터를 팩터 수준으로 구간화합니다. ordered=TRUE는 비교 연산자를 지원하는 순서 팩터를 생성합니다. relevel은 참조 범주를 변경(회귀 해석에 중요). table()은 빈도 수를 생성합니다.
# factors represent categorical data (stored as integers with labels)
gender <- factor(c("male", "female", "male", "female"))
print(gender)
# [1] male female male female
# Levels: female male
levels(gender) # "female" "male" (sorted alphabetically)
table(gender) # frequency table
nlevels(gender) # 2
# ordered factor (for ordinal data)
size <- factor(c("M", "S", "L", "XL"),
levels = c("S", "M", "L", "XL"),
ordered = TRUE)
size[1] > size[2] # TRUE (M > S)
# convert to factor and back
nums <- factor(c(1, 2, 3, 2, 1))
as.numeric(nums) # 1 2 3 2 1 (level codes, NOT original values!)
as.numeric(as.character(nums)) # 1 2 3 2 1 (correct way)
# relevel (change reference level)
gender <- relevel(gender, ref = "male")
# cut: convert numeric to factor (binning)
ages <- c(15, 25, 35, 45, 55, 65)
age_groups <- cut(ages, breaks = c(0, 18, 35, 50, 100),
labels = c("child", "young", "adult", "senior"))
table(age_groups) # frequency per group
# interaction of factors
f1 <- factor(c("A", "A", "B", "B"))
f2 <- factor(c("X", "Y", "X", "Y"))
interaction(f1, f2) # A.X A.Y B.X B.Y리스트를 데이터 프레임으로와 재구조화
넓은 형식과 긴 형식 간의 재구조화는 흔한 데이터 랭글링 작업입니다. pivot_longer/pivot_wider(tidyr, tidyverse)는 현대적이고 직관적인 함수입니다. 넓은 형식은 주체당 한 행에 각 시간점의 열이 있고; 긴 형식은 관찰당 한 행이 있습니다. 긴 형식은 ggplot2와 대부분의 분석에 선호됩니다. split()은 데이터 프레임을 팩터별 리스트로 분할; do.call(rbind, ...)은 재결합합니다. 기본 R의 reshape() 함수는 강력하지만 혼란스러운 인터페이스를 가짐 — tidyr을 선호하세요.
# convert list to data frame
my_list <- list(
a = 1:3,
b = c("x", "y", "z")
)
df <- as.data.frame(my_list)
# stack multiple vectors
do.call(rbind, list(
data.frame(name = "A", val = 1),
data.frame(name = "B", val = 2)
))
# reshape: wide to long
library(tidyr)
wide_df <- data.frame(
id = 1:2,
q1 = c(10, 20),
q2 = c(15, 25)
)
long_df <- pivot_longer(wide_df, cols = starts_with("q"),
names_to = "quarter", values_to = "value")
# long to wide
pivot_wider(long_df, names_from = quarter, values_from = value)
# base R reshape (without tidyr)
reshape(wide_df, direction = "long",
varying = c("q1", "q2"), v.names = "value",
timevar = "quarter", idvar = "id")
# split and combine
split_results <- split(df, df$group) # list of data frames by group
combined <- do.call(rbind, split_results) # recombine
# melt and cast (reshape2 package, legacy)
# library(reshape2)
# melt(wide_df, id.vars = "id")
# dcast(long_df, id ~ quarter, value.var = "value")제어 흐름과 함수
If / Else와 Switch
R의 if/else는 여러 줄 본문에 중괄호가 필요하고 else는 닫는 중괄호와 같은 줄에 있어야 합니다(그렇지 않으면 R이 if가 완료되었다고 생각). ifelse(test, yes, no)는 벡터화 — 전체 벡터에 한 번에 적용되어 결과 벡터를 반환합니다. 여러 조건의 경우 dplyr의 case_when이 중첩 ifelse보다 훨씬 깔끔합니다. switch는 문자열(또는 숫자 위치)로 디스패치 — 긴 if-else 체인의 깔끔한 대안입니다.
# if-else if-else
score <- 85
if (score >= 90) {
grade <- "A"
} else if (score >= 80) {
grade <- "B"
} else {
grade <- "C"
}
# ifelse (vectorized ternary)
ages <- c(15, 25, 35, 45)
ifelse(ages >= 18, "adult", "minor")
# "minor" "adult" "adult" "adult"
# nested ifelse (avoid deep nesting!)
ifelse(ages < 18, "minor",
ifelse(ages < 65, "adult", "senior"))
# dplyr::case_when is cleaner for multiple conditions
# case_when(
# ages < 18 ~ "minor",
# ages < 65 ~ "adult",
# TRUE ~ "senior"
# )
# switch (dispatch on a value)
day_type <- function(day) {
switch(day,
"Mon" = "weekday",
"Tue" = "weekday",
"Wed" = "weekday",
"Thu" = "weekday",
"Fri" = "weekday",
"Sat" = "weekend",
"Sun" = "weekend",
"unknown"
)
}루프: For, While, Repeat
R의 for 루프는 요소(또는 시퀀스)를 반복합니다. seq_along(x)는 x가 비어 있을 때 1:length(x)보다 안전합니다(integer(0)을 반환). next는 다음 반복으로 건너뛰고(continue처럼); break는 종료합니다. repeat는 명시적으로 중단되어야 하는 무한 루프입니다. 항상 결과 벡터를 미리 할당(result <- numeric(N)) — c()로 루프에서 벡터를 키우는 것은 O(n²)이고 매우 느립니다. 가능하면 루프보다 벡터화 연산이나 apply 계열을 선호하세요.
# for loop
for (i in 1:5) {
print(i)
}
# iterate over a vector
fruits <- c("apple", "banana", "cherry")
for (fruit in fruits) {
print(paste("Fruit:", fruit))
}
# iterate with index
for (i in seq_along(fruits)) {
print(paste(i, fruits[i]))
}
# while loop
count <- 0
while (count < 5) {
count <- count + 1
if (count == 3) next # skip (like continue)
print(count)
}
# repeat loop (infinite, must break)
i <- 0
repeat {
i <- i + 1
if (i >= 3) break # exit loop
print(i)
}
# preallocate for speed (IMPORTANT!)
result <- numeric(1000)
for (i in 1:1000) {
result[i] <- i^2
}
# nested loops
mat <- matrix(0, 3, 3)
for (i in 1:3) {
for (j in 1:3) {
mat[i, j] <- i * j
}
}함수 정의와 인자
R 함수는 마지막으로 평가된 표현식을 자동으로 반환(명시적 return이 필요 없지만, 조기 종료에는 return()이 더 명확). 기본 인자는 함수를 유연하게 만듭니다. ... (줄임표)는 전달할 추가 인자를 캡처 — 래퍼 함수에 필수. 이름 있는 인자는 순서에 상관없이 가능합니다. R은 지연 평가 사용: 인자는 처음 사용될 때만 평가되어, 사용되지 않은 인자는 오류를 일으키지 않습니다. 리스트에 패키징하여 여러 값을 반환하세요.
# basic function (last expression is returned)
add <- function(a, b) {
a + b
}
add(3, 4) # 7
# explicit return
is_positive <- function(x) {
if (x > 0) return(TRUE)
FALSE
}
# default arguments
greet <- function(name, greeting = "Hello", punctuation = "!") {
paste0(greeting, ", ", name, punctuation)
}
greet("Alice") # "Hello, Alice!"
greet("Bob", greeting = "Hi") # "Hi, Bob!"
greet(name = "Carol", punctuation = "?") # named args
# ... (ellipsis: pass arguments through)
my_plot <- function(x, y, ...) {
plot(x, y, col = "blue", ...)
}
my_plot(1:10, 1:10, main = "My Plot", type = "l")
# return multiple values via list
stats <- function(x) {
list(mean = mean(x), sd = sd(x), n = length(x))
}
result <- stats(1:10)
result$mean # 5.5
# lazy evaluation (args evaluated only when used)
f <- function(a, b) {
a * 2 # b is never used, so not evaluated
}
f(5) # 10 (no error despite missing b)Apply 계열
apply 계열은 루프를 함수형 반복으로 대체 — 더 관용적이고 종종 더 빠릅니다. lapply는 항상 리스트 반환; sapply는 벡터/행렬로 단순화 시도(편리하지만 예측 불가); vapply는 보장된 반환 타입의 안전한 버전입니다. apply는 행렬에 작동(MARGIN=1은 행, 2는 열). tapply는 팩터별로 데이터를 그룹화하고 함수 적용 — 미니 GROUP BY처럼. replicate는 무작위 시뮬레이션을 반복합니다. 데이터 프레임의 경우 purrr 패키지(tidyverse)가 더 깔끔하고 일관된 map() 계열을 제공합니다.
# lapply: apply function to each element of a list, returns list
my_list <- list(a = 1:3, b = 4:6, c = 7:9)
lapply(my_list, mean) # list: 2, 5, 8
lapply(my_list, sum) # list: 6, 15, 24
# sapply: like lapply but simplifies to vector/matrix
sapply(my_list, mean) # 2 5 8 (named vector)
sapply(my_list, range) # matrix with 2 rows
# vapply: like sapply but with guaranteed output type (safer)
vapply(my_list, mean, numeric(1)) # always numeric vector
# apply: apply function over matrix/array margins
m <- matrix(1:12, nrow = 3)
apply(m, 1, sum) # row sums: 22 26 30
apply(m, 2, mean) # column means: 2 5 8 11
apply(m, c(1, 2), sqrt) # element-wise sqrt
# mapply: multivariate apply (vectorized over multiple args)
mapply(rep, 1:3, 3:1) # rep(1,3), rep(2,2), rep(3,1)
# tapply: apply function by groups
df <- data.frame(
group = c("A", "A", "B", "B", "B"),
value = c(10, 20, 30, 40, 50)
)
tapply(df$value, df$group, mean) # A: 15, B: 40
# replicate: repeat an expression n times
replicate(3, mean(rnorm(10))) # 3 random means스코프와 환경
R은 어휘 스코프 사용: 함수는 정의된 환경에서 자유 변수를 조회(호출된 곳이 아님). 이것은 클로저를 가능하게 — 자신을 둘러싼 환경을 캡처하는 함수입니다. <<- 연산자는 부모 환경의 변수에 할당(슈퍼 할당), 클로저가 상태를 유지하는 방법입니다(카운터 예제처럼). 각 함수 호출은 새 환경을 만듭니다. 검색 경로(search())는 R이 객체를 찾는 위치를 결정 — globalenv가 작업 공간, 다음 연결된 패키지들입니다.
# lexical scoping: functions look up variables in defining environment
y <- 10
f <- function(x) {
x + y # y found in global env
}
f(5) # 15
# local variables shadow globals
g <- function(x) {
y <- 100 # local y
x + y
}
g(5) # 105
y # 10 (global unchanged)
# <<- assigns to parent environment (super-assignment)
counter <- function() {
count <- 0
function() {
count <<- count + 1 # modifies count in enclosing scope
count
}
}
c1 <- counter()
c1() # 1
c1() # 2
# search path for variables
search() # shows environments: globalenv, package namespaces
ls() # list objects in current environment
ls(envir = .GlobalEnv) # explicitly global
exists("y") # TRUE if variable exists
# new environment (isolated scope)
e <- new.env()
e$x <- 42
e$x # 42 (separate from global x)데이터 조작 (dplyr & tidyr)
dplyr: Filter, Select & Arrange
dplyr(tidyverse의 일부)는 SQL 연산을 반영하는 직관적인 데이터 조작 동사를 제공합니다. filter는 조건으로 행 선택; select는 열 선택; arrange는 정렬합니다. %in% 연산자는 멤버십을 테스트합니다. starts_with, ends_with, contains 같은 도우미 함수는 열 선택을 유연하게 만듭니다. rename()은 복사 없이 열 이름을 변경합니다. 이 동사들은 읽기 쉬운 데이터 파이프라인을 위해 파이프(%>%)와 결합됩니다. dplyr는 내부적으로 C++를 사용하여 큰 데이터에 기본 R보다 훨씬 빠릅니다.
library(dplyr)
df <- data.frame(
name = c("Alice", "Bob", "Carol", "Dan"),
age = c(30, 25, 28, 35),
dept = c("Eng", "Sales", "Eng", "Sales"),
salary = c(80000, 50000, 75000, 90000)
)
# filter rows (like WHERE in SQL)
filter(df, age > 26)
filter(df, dept == "Eng" & salary > 70000)
filter(df, dept %in% c("Eng", "Sales"))
filter(df, age > 26 | salary > 85000)
# select columns (like SELECT in SQL)
select(df, name, age)
select(df, name:dept) # range of columns
select(df, -salary) # exclude column
select(df, starts_with("s")) # columns starting with 's'
select(df, ends_with("e")) # columns ending with 'e'
select(df, contains("am")) # columns containing 'am'
select(df, everything()) # all columns (useful for reordering)
# arrange (sort, like ORDER BY)
arrange(df, age) # ascending
arrange(df, desc(age)) # descending
arrange(df, dept, desc(salary)) # sort by dept, then salary desc
# rename columns
rename(df, years = age, department = dept)
# distinct rows
distinct(df, dept) # unique departments
distinct(df, dept, .keep_all = TRUE) # keep all columnsdplyr: Mutate, Summarize & Group By
mutate는 열을 추가하거나 수정(벡터화). summarize는 각 그룹을 단일 요약 행으로 축소. 힘은 group_by + summarize에서 옵니다 — SQL GROUP BY의 R 버전. n()는 행을 세고; across()는 여러 열에 함수 적용(dplyr 1.0의 새 기능). 윈도우 함수(rank, cumsum, lag, lead)는 그룹 내에서 작동, '부서 내 순위' 같은 계산을 가능하게 합니다. 파이프 %>는 작업을 좌에서 우로 연결, 복잡한 파이프라인을 읽기 쉽게 만듭니다.
library(dplyr)
# mutate: add/modify columns
df %>%
mutate(
bonus = salary * 0.1,
total = salary + bonus,
category = ifelse(age > 30, "senior", "junior")
)
# transmute: like mutate but only keeps new columns
transmute(df, name, annual = salary, monthly = salary / 12)
# summarize (reduces to single row per group)
summarize(df,
avg_salary = mean(salary),
max_age = max(age),
n = n()
)
# group_by + summarize (like GROUP BY in SQL)
df %>%
group_by(dept) %>%
summarize(
count = n(),
avg_salary = mean(salary),
avg_age = mean(age)
) %>%
arrange(desc(avg_salary))
# multiple summaries
df %>%
group_by(dept) %>%
summarize(across(everything(), list(mean, sd)))
# count and tally
count(df, dept) # count per dept
df %>% group_by(dept) %>% tally()
# window functions (within groups)
df %>%
group_by(dept) %>%
mutate(
rank = rank(desc(salary)),
cumsum = cumsum(salary)
) %>%
arrange(dept, rank)파이프 연산자 (%>%)
파이프(%>%, magrittr/dplyr)는 좌변을 우변의 첫 번째 인자로 전달, 중첩된 함수 호출을 읽기 쉬운 좌에서 우로 파이프라인으로 변환합니다. 이것이 tidyverse 스타일의 핵심 기능입니다. 점(.)은 명시적으로 필요할 때 파이프된 데이터를 나타냅니다. %$%는 열 이름을 노출; %<>%는 다시 할당; %T>%는 부작용(플롯처럼) 후 파이프를 계속합니다. R 4.1+는 네이티브 파이프 |>를 가지지만, 덜 유연합니다(점 자리표시자 없음). 파이프는 데이터 랭글링 코드를 극적으로 읽기 쉽게 만듭니다.
library(magrittr) # or library(dplyr)
# without pipe: nested, hard to read
result <- arrange(
filter(
select(df, name, age, salary),
age > 25
),
desc(salary)
)
# with pipe: linear, readable
result <- df %>%
select(name, age, salary) %>%
filter(age > 25) %>%
arrange(desc(salary))
# the dot (.) refers to the piped data
df %>% plot(.$age, .$salary) # . = df
# %$% exposes column names (magrittr)
df %$% cor(age, salary) # correlation
# %<>% assigns result back (compound assignment)
df %<>% filter(age > 20)
# T pipe: returns first argument (for side effects)
rnorm(100) %T>%
hist() %>% # plot histogram (returns input)
mean() # then compute mean
# native pipe (R 4.1+): |>
df |>
subset(age > 25) |>
colMeans()데이터 프레임 조인
dplyr의 조인 함수는 SQL 조인을 반영: inner_join(교집합), left_join(모든 좌측 행), right_join, full_join(합집합). semi_join은 일치하는 행으로 필터(열 추가 없이); anti_join은 일치하지 않는 행을 찾습니다 — 둘 다 데이터 검증에 유용. by 인자는 조인 키를 지정; 열 이름이 다를 때 이름 있는 벡터(c('id' = 'emp_id'))를 사용하세요. 조인은 기본 R의 merge()보다 훨씬 빠릅니다. 조인 전후로 행 수를 확인하여 예상치 못한 중복을 잡으세요.
library(dplyr)
employees <- data.frame(
id = c(1, 2, 3, 4),
name = c("Alice", "Bob", "Carol", "Dan")
)
salaries <- data.frame(
id = c(1, 2, 3, 5),
salary = c(80000, 50000, 75000, 60000)
)
# inner join: only matching rows
inner_join(employees, salaries, by = "id")
# id name salary
# 1 Alice 80000
# 2 Bob 50000
# 3 Carol 75000
# left join: all rows from left, NA for non-matches
left_join(employees, salaries, by = "id")
# Dan has NA salary
# right join: all rows from right
right_join(employees, salaries, by = "id")
# id 5 has NA name
# full join: all rows from both
full_join(employees, salaries, by = "id")
# different column names
left_join(employees, salaries, by = c("id" = "id"))
# semi join: rows in left that have a match (no right columns)
semi_join(employees, salaries, by = "id")
# anti join: rows in left with NO match
anti_join(employees, salaries, by = "id")
# Dan (id 4 has no salary)
# multiple keys
left_join(df1, df2, by = c("dept" = "department", "year" = "yr"))tidyr: 데이터 재구조화
tidyr(tidyverse)은 데이터 재구조화를 처리합니다. pivot_longer/wider는 기존 gather/spread를 대체 — 더 직관적이고 유연합니다. Tidy 데이터는 관찰당 한 행, 변수당 한 열; pivot_longer는 넓은 데이터를 이 형식으로 변환(ggplot2에 필요). separate/unite는 열을 분할하고 병합합니다. separate_rows는 구분된 값을 여러 행으로 폭발시킵니다. drop_na/replace_na/fill은 결측 데이터를 깔끔하게 처리합니다. 이 동사들은 강력한 데이터 파이프라인을 위해 파이프를 통해 dplyr과 결합됩니다.
library(tidyr)
# wide to long
wide_df <- data.frame(
id = 1:2,
q1_sales = c(100, 200),
q2_sales = c(150, 250),
q3_sales = c(120, 220)
)
long_df <- wide_df %>%
pivot_longer(
cols = starts_with("q"),
names_to = "quarter",
values_to = "sales"
)
# long to wide
long_df %>%
pivot_wider(
names_from = quarter,
values_from = sales
)
# separate one column into multiple
df <- data.frame(id = 1, name = "Alice_Smith")
df %>% separate(name, into = c("first", "last"), sep = "_")
# unite multiple columns into one
df %>% unite("full_name", first, last, sep = " ")
# separate_rows: split delimited values into rows
df <- data.frame(id = 1:2, tags = c("a,b,c", "x,y"))
df %>% separate_rows(tags, sep = ",")
# drop NA values
df %>% drop_na() # drop rows with any NA
df %>% drop_na(salary) # drop rows where salary is NA
# replace NA with a value
df %>% replace_na(list(salary = 0, name = "Unknown"))
# fill missing values (carry forward)
df %>% fill(salary, .direction = "down")통계와 모델링
기술 통계
summary()는 모든 데이터의 개요를 얻는 가장 빠른 방법 — 최소, 사분위수, 중위수, 평균, 최대를 표시합니다. sd()와 var()는 표본(n-1) 통계를 계산합니다. cor()는 선형 연관(Pearson) 또는 순위 연관(Spearman)을 측정합니다. NA를 포함한 실제 데이터에는 항상 na.rm=TRUE를 사용하세요. 데이터 프레임의 경우 summary(df)는 열별 통계를 제공합니다. psych와 Hmisc 패키지는 확장된 기술 통계를 제공합니다.
data <- c(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
# central tendency
mean(data) # 5.5
median(data) # 5.5
# R has no built-in mode; use:
as.numeric(names(sort(table(data), decreasing = TRUE)[1]))
# spread
sd(data) # 3.028 (sample standard deviation)
var(data) # 9.167 (sample variance)
IQR(data) # 5 (interquartile range)
range(data) # 1 10
diff(range(data)) # 9
# quantiles
quantile(data) # 0% 25% 50% 75% 100%
quantile(data, c(0.1, 0.9)) # 10th and 90th percentiles
# summary (all at once)
summary(data)
# Min. 1st Qu. Median Mean 3rd Qu. Max.
# 1.00 3.25 5.50 5.50 7.75 10.00
# correlation and covariance
x <- c(1, 2, 3, 4, 5)
y <- c(2, 4, 5, 4, 5)
cor(x, y) # 0.77 (Pearson correlation)
cor(x, y, method = "spearman") # rank correlation
cov(x, y) # covariance
# handling NA
data_na <- c(1, 2, NA, 4, 5)
mean(data_na, na.rm = TRUE) # 3
sd(data_na, na.rm = TRUE) # 1.826확률 분포
R은 일관된 명명 규칙으로 모든 일반 분포를 가집니다: d/p/q/r 접두사 + 분포 이름. d는 밀도(연속형) 또는 확률 질량(이산형); p는 누적 확률; q는 분위수(백분위수); r은 무작위 표본을 생성합니다. 일반 분포: norm(정규), binom(이항), pois(포아송), unif(균일), exp(지수), t, chisq, f. 재현 가능한 결과를 위해 무작위 연산 전에 항상 set.seed()를 호출하세요. sample()은 벡터에서 무작위 요소를 추출합니다.
# R uses d/p/q/r prefix for distributions:
# d = density (PDF/PMF)
# p = cumulative (CDF: P(X <= x))
# q = quantile (inverse CDF)
# r = random samples
# Normal distribution
dnorm(0) # 0.399 (density at 0)
pnorm(1.96) # 0.975 (P(Z <= 1.96))
qnorm(0.975) # 1.96 (97.5th percentile)
rnorm(5, mean = 100, sd = 15) # 5 random samples
# Binomial distribution
dbinom(3, size = 10, prob = 0.5) # P(X=3)
pbinom(3, size = 10, prob = 0.5) # P(X<=3)
rbinom(10, size = 1, prob = 0.5) # 10 coin flips
# Poisson distribution
dpois(2, lambda = 3) # P(X=2) with rate 3
rpois(100, lambda = 5) # 100 random samples
# Uniform distribution
runif(5, min = 0, max = 1) # 5 uniform random numbers
# Exponential
rexp(10, rate = 0.5) # 10 exponential samples
# set random seed for reproducibility
set.seed(42)
rnorm(3) # same results every time with same seed
# sample from a vector
sample(1:10, 5) # 5 random numbers without replacement
sample(1:10, 5, replace = TRUE) # with replacement
sample(c("A", "B", "C"), 2) # sample from categories가설 검정
R은 가설 검정을 간단하게 만듭니다. t.test는 평균 비교(단일 표본, 두 표본, 또는 대응). p-value < 0.05는 일반적으로 통계적 유의성을 나타냅니다. var.equal=TRUE는 등분산 가정(Student's t); 기본값은 Welch's(더 강건). chisq.test는 범주형 변수의 독립성 검사. wilcox.test는 비모수 대안(정규성 가정 없음). aov는 3+ 그룹 비교를 위한 ANOVA 수행. 모든 검정 함수는 $p.value, $statistic, $conf.int를 가진 리스트를 반환하여 프로그래밍 방식으로 추출 가능합니다.
# one-sample t-test (is mean different from hypothesized?)
data <- c(5.1, 4.9, 5.0, 5.2, 4.8, 5.1, 5.0)
t.test(data, mu = 5.0)
# t = 0.527, df = 6, p-value = 0.616
# (fail to reject H0: mean = 5)
# two-sample t-test (are two means different?)
group1 <- c(5.1, 4.9, 5.0, 5.2)
group2 <- c(4.5, 4.7, 4.6, 4.4)
t.test(group1, group2)
t.test(group1, group2, var.equal = TRUE) # assume equal variance
# paired t-test (before/after)
before <- c(70, 80, 65, 90, 75)
after <- c(75, 85, 70, 92, 80)
t.test(before, after, paired = TRUE)
# chi-squared test (independence of categorical vars)
tbl <- table(c("A","A","B","B"), c("X","Y","X","Y"))
chisq.test(tbl)
# Wilcoxon (non-parametric alternative to t-test)
wilcox.test(group1, group2)
# ANOVA (compare means across multiple groups)
df <- data.frame(
value = c(5, 6, 7, 8, 9, 10),
group = factor(c("A","A","B","B","C","C"))
)
result <- aov(value ~ group, data = df)
summary(result) # F-test p-value
# extract p-value from any test
test_result <- t.test(data, mu = 5)
test_result$p.value # 0.616선형 회귀 (lm)
lm()은 공식 문법으로 선형 모델 피팅: y ~ x(단순), y ~ x1 + x2(다중), y ~ .(모든 열), y ~ x1*x2(상호작용 포함), y ~ I(x^2)(변환). summary()는 계수, 표준 오차, t-값, p-값, R², F-검정을 표시합니다. 팩터는 자동으로 더미 변수로 변환됩니다. 공식 미니 언어는 강력: -는 항 제거, :는 상호작용, *는 주효과 + 상호작용입니다. 모델 가정을 확인하기 위해 항상 진단 플롯(잔차, Q-Q 플롯)을 검사하세요.
# simple linear regression: y ~ x
x <- c(1, 2, 3, 4, 5)
y <- c(2, 4, 5, 4, 5)
model <- lm(y ~ x)
# view results
print(model) # coefficients
summary(model) # full summary with R², t-tests, F-test
coef(model) # (Intercept) 2.2, x 0.6
fitted(model) # predicted values
residuals(model) # y - predicted
confint(model) # 95% confidence intervals
# make predictions
predict(model, newdata = data.frame(x = c(6, 7)))
# multiple regression
df <- data.frame(
y = c(10, 12, 15, 18, 20),
x1 = c(1, 2, 3, 4, 5),
x2 = c(5, 4, 3, 2, 1)
)
model2 <- lm(y ~ x1 + x2, data = df)
model3 <- lm(y ~ ., data = df) # all predictors
model4 <- lm(y ~ x1 + I(x1^2), data = df) # polynomial
# interactions
model5 <- lm(y ~ x1 * x2, data = df) # x1 + x2 + x1:x2
# diagnostic plots
par(mfrow = c(2, 2))
plot(model) # 4 diagnostic plots
# with factors (automatic dummy variables)
model6 <- lm(y ~ x1 + factor(group), data = df)GLM과 기타 모델
glm()은 family 인자를 통해 lm()을 비정규 결과로 일반화: binomial(이진을 위한 로지스틱 회귀), poisson(카운트 데이터), gaussian(lm과 동일). 로지스틱 모델의 경우 type='response'와 predict()는 확률을 줍니다(log-odds가 아닌). step()은 자동 변수 선택 수행(AIC 기반). anova(model1, model2)는 더 큰 모델이 유의하게 더 나은지 검사. 고급 방법의 경우, R은 모든 것을 위한 패키지가 있습니다: lme4(혼합 모델), survival(Kaplan-Meier, Cox), randomForest, caret(ML 파이프라인), glmnet(정규화).
# logistic regression (binary outcome)
df <- data.frame(
admit = c(0, 1, 0, 1, 1, 0),
gre = c(380, 660, 800, 640, 520, 760),
gpa = c(3.6, 3.7, 3.8, 3.9, 3.5, 3.6)
)
logit <- glm(admit ~ gre + gpa, data = df, family = binomial)
summary(logit)
# predict probabilities (type = "response")
predict(logit, newdata = df, type = "response")
# Poisson regression (count data)
counts <- c(2, 3, 1, 4, 2, 5)
pois <- glm(counts ~ x, family = poisson)
# stepwise model selection
full_model <- lm(y ~ x1 + x2 + x3, data = df)
step_model <- step(full_model) # AIC-based selection
# anova to compare models
model1 <- lm(y ~ x1, data = df)
model2 <- lm(y ~ x1 + x2, data = df)
anova(model1, model2) # is x2 significant?
# other models
# nls() - nonlinear least squares
# glm.nb() - negative binomial (MASS package)
# lme()/lmer() - mixed effects (nlme/lme4)
# rpart()/randomForest() - tree-based methods
# coxph() - survival analysis (survival package)
# cross-validation
library(boot)
cv_result <- cv.glm(df, logit, K = 10) # 10-fold CV
cv_result$delta # cross-validation error플로팅 (Base R & ggplot2)
Base R: plot, hist & boxplot
Base R 그래픽은 탐색적 분석에 빠르고 충분합니다. plot()은 제네릭 — 입력 타입에 따라 디스패치(두 벡터의 산점도, 공식의 박스플롯). type은 점/선 스타일 제어; pch는 점 기호 설정; lty는 선 타입 설정. par(mfrow=c(r,c))는 여러 플롯을 그리드로 배치합니다. freq=FALSE와 hist()는 밀도를 표시(밀도 곡선을 오버레이 가능). 출판 품질 그래픽의 경우 ggplot2를 대신 사용. Base R 플롯은 명령형 — 단계별로 구축합니다.
# scatter plot
x <- 1:10
y <- x^2
plot(x, y, type = "p", # p=points, l=lines, b=both, o=overplotted
main = "Quadratic", # title
xlab = "x", ylab = "y", # axis labels
col = "blue", pch = 16, # color and point character
xlim = c(0, 10), ylim = c(0, 100))
# add lines and points to existing plot
lines(x, x*10, col = "red", lty = 2) # dashed line
points(x, y + 5, col = "green")
legend("topleft", legend = c("x^2", "10x"),
col = c("blue", "red"), lty = c(NA, 2), pch = c(16, NA))
# histogram
data <- rnorm(1000)
hist(data, breaks = 30, col = "lightblue",
main = "Normal Distribution", xlab = "Value", freq = FALSE)
lines(density(data), col = "red", lwd = 2) # add density curve
# boxplot (compare groups)
boxplot(count ~ spray, data = InsectSprays,
col = "lightgreen", main = "Insect Count by Spray")
# barplot
counts <- table(mtcars$cyl)
barplot(counts, col = c("red","green","blue"),
main = "Cars by Cylinders", xlab = "Cylinders")
# multiple plots in one window
par(mfrow = c(2, 2)) # 2x2 grid
plot(x, y); hist(data); boxplot(data); plot(density(data))
par(mfrow = c(1, 1)) # resetggplot2: 그래픽의 문법
ggplot2(tidyverse)는 그래픽의 문법 구현: 플롯은 레이어(데이터, 미학, 기하, 스케일, 패싯, 테마)를 +로 결합하여 구축됩니다. aes()는 데이터 열을 시각적 속성(x, y, color, size)에 매핑. 각 geom_*은 레이어 추가: geom_point(산점도), geom_line, geom_bar, geom_histogram, geom_boxplot, geom_smooth(추세선). 이 레이어 방식은 복잡한 플롯을 점진적으로 구축할 수 있음을 의미합니다. base R과 달리 ggplot2는 선언적 — 원하는 것을 설명, 그리는 방법이 아닌.
library(ggplot2)
# basic structure: data + aesthetic mapping + geometry
ggplot(data = mtcars, aes(x = wt, y = mpg)) +
geom_point()
# add aesthetics (color, size, shape mapped to data)
ggplot(mtcars, aes(x = wt, y = mpg, color = cyl, size = hp)) +
geom_point()
# add layers
ggplot(mtcars, aes(x = wt, y = mpg)) +
geom_point(color = "blue", size = 3) +
geom_smooth(method = "lm", se = TRUE) + # regression line
labs(title = "MPG vs Weight",
x = "Weight (1000 lbs)", y = "MPG",
color = "Cylinders") +
theme_minimal()
# the + operator adds layers (like %>% but for ggplot)
p <- ggplot(mtcars, aes(x = wt, y = mpg)) +
geom_point()
p + geom_smooth() # add to saved plot
p + facet_wrap(~ cyl) # facet by cylinders
# key components:
# data - the data frame
# aes() - aesthetic mappings (x, y, color, size, shape)
# geom_*() - geometry (point, line, bar, histogram, etc.)
# scale_*() - control axes, colors, legends
# facet_*() - small multiples (subplots)
# theme_*() - overall appearance
# labs() - titles and labelsggplot2: Geoms와 미학
ggplot2는 모든 차트 타입을 위한 수십 개의 geom이 있습니다. geom_bar/geom_col은 막대, geom_histogram/geom_density는 분포, geom_boxplot은 비교, geom_line/geom_area는 시계열. stat_summary는 요약 계산과 플롯(평균, 오차 막대). alpha는 투명도 제어(0-1) — 겹치는 점에 필수. coord_flip은 플롯 회전. geom_jitter는 오버플롯팅 방지를 위해 노이즈 추가. 각 geom은 이해하는 특정 미학이 있습니다(예: geom_point는 x와 y 필요; geom_bar는 x만 필요).
library(ggplot2)
# bar chart (counts)
ggplot(mpg, aes(class)) +
geom_bar(fill = "steelblue") +
coord_flip() # horizontal bars
# histogram
ggplot(mpg, aes(hwy)) +
geom_histogram(binwidth = 2, fill = "orange", color = "black")
# density plot
ggplot(mpg, aes(hwy, fill = class)) +
geom_density(alpha = 0.5) # semi-transparent
# boxplot by group
ggplot(mpg, aes(class, hwy)) +
geom_boxplot() +
geom_jitter(width = 0.2, alpha = 0.5) # overlay points
# line plot (time series)
ggplot(economics, aes(date, unemploy)) +
geom_line(color = "red") +
geom_area(fill = "pink", alpha = 0.3)
# scatter with smoothing
ggplot(mpg, aes(displ, hwy, color = drv)) +
geom_point(size = 2) +
geom_smooth(method = "loess", se = FALSE)
# error bars
ggplot(df, aes(group, value)) +
stat_summary(fun = mean, geom = "bar") +
stat_summary(fun.data = mean_se, geom = "errorbar")
# text labels
ggplot(mtcars, aes(wt, mpg, label = rownames(mtcars))) +
geom_text(size = 3)ggplot2: 패싯, 스케일과 테마
패싯은 작은 배수를 생성 — 범주당 하나의 서브플롯 — 그룹을 비교하는 최선의 방법. facet_wrap(~var)은 1D 리본 생성; facet_grid(row~col)은 2D 그리드 생성. 스케일은 데이터를 시각적 속성에 매핑하는 방법 제어: log 축의 scale_x_log10, 색맹 친화적 팔레트의 scale_color_brewer, 사용자 정의 색의 scale_fill_manual. 테마는 비데이터 요소 제어(글꼴, 그리드선, 범례). theme_minimal/bw/classic은 사전 설정; theme()은 개별 요소 사용자 정의. ggsave는 크기와 DPI 제어로 PNG/PDF/SVG로 내보냅니다.
library(ggplot2)
# facets: small multiples (subplots by a variable)
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_wrap(~ class) # one panel per class
# facet_grid: 2D grid of panels
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_grid(drv ~ cyl) # rows ~ cols
# scales: control axes and colors
ggplot(mpg, aes(displ, hwy, color = class)) +
geom_point() +
scale_x_log10() + # log scale
scale_color_brewer(palette = "Set1") + # color palette
scale_size_continuous(range = c(2, 8))
# manual colors
ggplot(mpg, aes(class, fill = drv)) +
geom_bar() +
scale_fill_manual(values = c("red", "green", "blue"))
# themes: overall appearance
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
theme_minimal() + # or theme_classic, theme_bw
theme(
text = element_text(size = 14, family = "serif"),
plot.title = element_text(face = "bold", hjust = 0.5),
axis.text.x = element_text(angle = 45, hjust = 1),
legend.position = "bottom",
panel.grid.major = element_line(color = "gray90")
)
# save plot
ggsave("plot.png", width = 8, height = 6, dpi = 300)
ggsave("plot.pdf", device = "pdf")데이터 저장과 내보내기
RDS는 단일 R 객체 저장에 최선(타입 보존, 빠름, 간결). RData는 여러 객체 저장. CSV는 가장 이식 가능(Excel, Python 등이 읽을 수 있음)하지만 타입 정보 손실 — 항상 stringsAsFactors=FALSE 설정. readr 패키지(tidyverse)는 더 빠르고 일관된 CSV I/O를 제공하여 tibble(개선된 데이터 프레임) 반환. readxl/writexl은 Excel 처리. 큰 데이터셋의 경우 data.table::fread(매우 빠름) 또는 열 저장소의 parquet(arrow 패키지) 고려. CSV 작성 시 항상 row.names=FALSE 사용.
# save single object to RDS (binary, preserves type)
saveRDS(my_data, "data.rds")
restored <- readRDS("data.rds")
# save multiple objects to RData
save(df1, df2, model, file = "workspace.RData")
load("workspace.RData") # loads all objects into workspace
# CSV (most portable)
write.csv(df, "data.csv", row.names = FALSE)
df <- read.csv("data.csv")
df <- read.csv("data.csv", stringsAsFactors = FALSE)
df <- read.csv("data.csv", na.strings = c("", "NA", "N/A"))
# readr (tidyverse): faster, smarter CSV I/O
library(readr)
write_csv(df, "data.csv")
df <- read_csv("data.csv") # returns a tibble
df <- read_csv("data.csv", col_types = cols(
age = col_integer(),
name = col_character()
))
# Excel (requires readxl/writexl packages)
library(readxl)
df <- read_excel("data.xlsx", sheet = 1, range = "A1:D100")
library(writexl)
write_xlsx(df, "output.xlsx")
# RDS vs RData vs CSV
# RDS: one object, binary, fast, preserves types
# RData: multiple objects, binary, fast
# CSV: text, portable to other tools, loses type info
# save and load workspace
save.image("project.RData") # save everything
load("project.RData") # restore everything
# serialize to JSON (jsonlite package)
library(jsonlite)
write_json(df, "data.json")
df <- fromJSON("data.json")OOP와 함수형 프로그래밍
S3 클래스와 메서드 (단순 OOP)
S3는 R의 가장 흔한 OOP 시스템 — 가볍고 비공식적. '클래스'는 단지 class 속성을 가진 리스트; 메서드는 generic.classname으로 명명된 함수. 제네릭 내의 UseMethod()는 객체의 클래스에 따라 적절한 메서드로 디스패치. 대부분의 R 객체(data.frame, lm, ggplot)는 S3. print(), summary(), plot()은 확장 가능한 제네릭. S3는 비공식적(검증 없음), 유연하지만 오류 발생하기 쉽습니다. 모든 메서드를 보려면 methods(generic), 클래스의 메서드는 methods(class='x') 사용.
# S3 is R's simplest OOP system: a class is just an attribute
# create an object: list + class attribute
account <- list(owner = "Alice", balance = 1000)
class(account) <- "BankAccount"
# generic function dispatches by class
print.BankAccount <- function(x, ...) {
cat("Account owner:", x$owner, "\n")
cat("Balance: $", x$balance, "\n", sep = "")
}
print(account)
# Account owner: Alice
# Balance: $1000
# define a custom method for an existing generic
deposit <- function(obj, amount) {
UseMethod("deposit") # dispatch based on class of obj
}
deposit.BankAccount <- function(obj, amount) {
obj$balance <- obj$balance + amount
obj
}
account <- deposit(account, 500)
account$balance # 1500
# check available methods for a generic
methods(print) # all print methods
methods(class = "lm") # methods for lm objects
# default method (fallback)
deposit.default <- function(obj, amount) {
stop("No deposit method for this class")
}S4 클래스 (공식 OOP)
S4는 R의 공식 OOP 시스템: 클래스는 타입, 검증, 상속을 가진 정의된 슬롯(필드). setClass()로 정의; new()로 인스턴스화. @($가 아닌)로 슬롯 접근. setMethod()는 제네릭의 메서드 정의. setValidity()는 제약 강제. S4는 Bioconductor와 엄격한 계약이 필요한 패키지(예: Matrix, sp)에서 사용. S4는 S3보다 더 강건하지만 더 장황. 대부분의 일상적 R은 S3 사용; 타입 안전성과 공식 상속이 필요할 때 S4를 사용하세요.
# S4 is formal: classes are defined with setClass and validated
setClass("Person",
slots = list(
name = "character",
age = "numeric"
),
prototype = list(name = NA_character_, age = NA_real_)
)
# constructor: new(ClassName, ...)
alice <- new("Person", name = "Alice", age = 30)
# access slots with @ (not $)
alice@name # "Alice"
slot(alice, "age") # 30
# define a method
setMethod("show", "Person", function(object) {
cat("Person:", object@name, "(", object@age, "y)\n")
})
show(alice) # Person: Alice ( 30 y)
# validity check
setValidity("Person", function(object) {
if (object@age < 0) return("age must be non-negative")
TRUE
})
# new("Person", name="Bob", age=-5) # error
# inheritance
setClass("Student", contains = "Person",
slots = list(gpa = "numeric"))
stu <- new("Student", name="Bob", age=20, gpa=3.8)클로저와 함수 팩토리
클로저는 자신을 정의한 환경의 변수에 접근을 유지하는 함수 — R이 상태 저장 함수를 만들고 개인 데이터를 캡슐화하는 주요 방법. <<- 연산자는 로컬이 아닌 둘러싼(부모) 환경에 할당. 함수 팩토리(함수를 반환하는 함수)는 특수화된 함수 생성에 강력. 일반 용도: 카운터, 메모이제이션(결과 캐싱), 모듈 패턴(개인 상태를 공유하는 함수 리스트 반환). 이것이 개인 필드를 가진 클래스에 가장 가까운 R의 아날로그입니다.
# a closure is a function that remembers its enclosing environment
# function factory: creates functions with private state
make_counter <- function() {
count <- 0
function() {
count <<- count + 1 # <<- modifies in enclosing env
count
}
}
c1 <- make_counter()
c1() # 1
c1() # 2
c1() # 3
c2 <- make_counter()
c2() # 1 (independent counter)
# memoization (cache expensive results)
memoize <- function(f) {
cache <- new.env(parent = emptyenv())
function(x) {
key <- as.character(x)
if (!exists(key, envir = cache)) {
assign(key, f(x), envir = cache)
}
get(key, envir = cache)
}
}
slow_sqrt <- function(x) { Sys.sleep(0.1); sqrt(x) }
fast_sqrt <- memoize(slow_sqrt)
fast_sqrt(16) # slow first time
fast_sqrt(16) # instant second time
# capturing state in a list (module pattern)
bank_account <- function(initial) {
balance <- initial
list(
deposit = function(amt) { balance <<- balance + amt },
withdraw = function(amt) { balance <<- balance - amt },
get_balance = function() balance
)
}
acc <- bank_account(100)
acc$deposit(50)
acc$get_balance() # 150함수형 프로그래밍: Map/Reduce/Filter
R은 내장 함수형 프로그래밍 기본 요소를 가집니다: Map(각 요소에 함수 적용), Reduce(접기/누적), Filter(일치하는 요소 유지), Find/Position(검색), Negate(술어 반전). 이들은 리스트나 벡터를 반환하고 명시적 루프를 피합니다. purrr 패키지(tidyverse)는 더 일관된 API 제공: map_dbl/map_chr은 타입화된 벡터 반환, keep/discard 필터, reduce 누적. ~ .x 공식 문법은 익명 함수를 간결하게 생성. 함수형 프로그래밍은 코드를 더 선언적으로 만들고 추론하기 쉽게, 특히 데이터 변환 파이프라인에서.
# Map: apply a function to each element (returns list)
Map(function(x) x^2, 1:5)
# [[1]] 1 [[2]] 4 [[3]] 9 [[4]] 16 [[5]] 25
# Reduce: combine elements with a binary function
Reduce("+", 1:5) # 15 (1+2+3+4+5)
Reduce("*", 1:5) # 120 (factorial 5!)
Reduce(c, list(1:2, 3:4, 5:6)) # 1 2 3 4 5 6
# Filter: keep elements satisfying a predicate
Filter(function(x) x > 2, 1:5) # 3 4 5
Filter(is.numeric, list(1, "a", 2, "b")) # 1 2
# Negate: invert a predicate
Negate(is.null)
is_not_null <- Negate(is.null)
# Position: find first index satisfying a predicate
Position(function(x) x > 3, 1:10) # 4
# Find: first element satisfying a predicate
Find(function(x) x > 3, 1:10) # 4
# purrr (tidyverse): cleaner functional programming
library(purrr)
map_dbl(1:5, ~ .x^2) # 1 4 9 16 25 (vector output)
map_chr(c("a","b"), toupper) # "A" "B"
keep(1:5, ~ .x > 2) # 3 4 5
reduce(1:5, `+`) # 15
walk(1:3, print) # side effects only디버깅과 오류 처리
browser()는 R의 대화형 디버거 — 변수 검사를 위해 코드에 삽입(n=다음, c=계속, Q=종료). debug(fn)은 함수를 한 줄씩 단계. traceback()은 충돌 후 호출 스택 표시. tryCatch()는 R의 try/catch: 핸들러 함수를 통해 오류와 경고를 잡고 대체 값을 반환. withCallingHandlers()는 실행을 중단하지 않고 경고 처리. options(warn=2) 설정은 경고를 오류로 변환(소스 찾기에 유용). options(error=browser)는 잡히지 않은 오류 시 자동으로 디버거 진입. 이 도구들을 숙달하는 것은 복잡한 R 코드의 문제 진단에 필수.
# browser(): pause execution and inspect
f <- function(x) {
browser() # pauses here; use n, c, Q, ls, print
y <- x * 2
if (y > 10) stop("y too big")
y
}
# f(6) # enters browser at the browser() line
# debug() / undebug(): debug a function
debug(lm)
# lm(y ~ x) # steps through lm line by line
undebug(lm)
# traceback(): see call stack after an error
# log("abc") # error
# traceback() # shows the call stack
# tryCatch(): handle errors gracefully
safe_log <- function(x) {
tryCatch(
log(x),
error = function(e) {
message("Error: ", conditionMessage(e))
NA_real_
},
warning = function(w) {
message("Warning: ", conditionMessage(w))
log(x) # retry or handle
}
)
}
safe_log("abc") # NA with message
safe_log(-1) # NA with warning (NaN)
# withCallingHandlers(): handle warnings without stopping
withCallingHandlers(
{ warn("oops"); 42 },
warning = function(w) { message("caught: ", w$message); invokeRestart("muffleWarning") }
)
# options for debugging
options(warn = 2) # warnings become errors (for debugging)
options(error = browser) # enter browser on error
options(warn = 0) # reset시계열과 예측
시계열 객체 생성 (ts)
ts()는 base R의 시계열 클래스 — 시간 속성(start, frequency)을 가진 벡터. frequency는 주기를 인코딩: 월간은 12, 분기는 4, 주간은 52. window()는 시간 범위로 서브셋. diff()는 차분 계산(정상계열 만들기에 유용). lag()는 값을 이동. aggregate()는 더 낮은 주기로 변환(예: 월간에서 분기). 정규적이고 고정 주기 데이터에 ts 적합. 불규칙 타임스탬프(예: 갭이 있는 주가)의 경우 xts/zoo 패키지를 대신 사용.
# ts(): create a regular time series
# frequency: 12=monthly, 4=quarterly, 52=weekly, 365.25=daily
monthly_sales <- ts(c(30, 35, 40, 38, 42, 45, 50, 48, 52, 55, 60, 58),
start = c(2020, 1), frequency = 12)
print(monthly_sales)
# Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
# 2020 30 35 40 38 42 45 50 48 52 55 60 58
# attributes
start(monthly_sales) # 2020 1
end(monthly_sales) # 2020 12
frequency(monthly_sales) # 12
time(monthly_sales) # time points
# window(): subset a time range
q1 <- window(monthly_sales, start = c(2020, 3), end = c(2020, 5))
# multiple series (multivariate)
ts1 <- ts(rnorm(12), start = 2020, frequency = 12)
ts2 <- ts(rnorm(12), start = 2020, frequency = 12)
mts <- cbind(ts1, ts2) # multivariate ts
# lag and diff
lag(monthly_sales, k = 1) # shift by 1 period
diff(monthly_sales) # first differences (month-over-month change)
diff(monthly_sales, lag = 12) # year-over-year change
# aggregate to lower frequency
aggregate(monthly_sales, nfrequency = 4, FUN = mean) # monthly -> quarterly분해와 평활
시계열 분해는 계열을 추세, 계절, 잔차 구성 요소로 분리. decompose()는 고전적 이동평균 분해 사용(가법 또는 승법). stl()은 LOESS 평활 사용, 이상치에 더 강건하고 변화하는 계절성 처리. HoltWinters()는 지수 평활 피팅(수준 + 추세 + 계절). forecast 패키지(현재 tidyverse의 fable)는 신뢰 구간과 함께 예측을 위한 forecast() 제공. 모델링 전에 항상 분해를 플롯하여 구조 이해. 계절 진폭이 추세와 함께 성장할 때 승법 분해가 적절.
# decompose(): split a series into trend/seasonal/random
decomp <- decompose(monthly_sales, type = "additive")
# type = "additive" (T+S+R) or "multiplicative" (T*S*R)
plot(decomp) # shows observed, trend, seasonal, random panels
decomp$trend # trend component
decomp$seasonal # seasonal component
decomp$random # remainder
# stl(): Seasonal-Trend-Loess decomposition (more flexible)
stl_fit <- stl(monthly_sales, s.window = "periodic")
plot(stl_fit)
stl_fit$time.series[, "trend"] # extract trend
# moving average smoothing
ma3 <- filter(monthly_sales, filter = rep(1/3, 3), sides = 2)
ma12 <- filter(monthly_sales, filter = rep(1/12, 12), sides = 1)
# HoltWinters(): exponential smoothing
hw <- HoltWinters(monthly_sales, seasonal = "additive")
plot(hw)
hw$fitted # fitted values
hw$coefficients # alpha, beta, gamma
# forecast with HoltWinters
library(forecast)
fc <- forecast(hw, h = 12) # 12-step ahead forecast
plot(fc) # with prediction intervals
autoplot(fc) # ggplot2 versionACF, PACF와 ARIMA 모델링
ACF/PACF 플롯은 자기상관 구조 진단: ACF는 각 지연에서 총 상관 표시, PACF는 직접(부분) 상관 표시. 패턴은 ARIMA 차수 제안: ACF 절단은 MA 제안, PACF 절단은 AR 제안. adf.test()는 정상성 검사(p<0.05는 정상계열 의미). auto.arima()(forecast 패키지)는 AICc로 최적 ARIMA(p,d,q)(P,D,Q) 모델 자동 선택. d는 차분 차수(정상성 달성); (P,D,Q)는 계절 구성 요소. checkresiduals()는 모델이 잘 피팅되었는지 검증(잔차는 백색 잡음이어야). ARIMA는 시계열 예측의 주력.
# ACF (autocorrelation) and PACF (partial autocorrelation)
acf(monthly_sales, main = "ACF") # correlation with lagged self
pacf(monthly_sales, main = "PACF") # direct correlation at each lag
# interpret:
# ACF tails off slowly -> need differencing (non-stationary)
# ACF cuts off at lag p -> AR(p) process
# PACF cuts off at lag q -> MA(q) process
# stationarity test
library(tseries)
adf.test(monthly_sales) # Augmented Dickey-Fuller
# p < 0.05 -> stationary
# auto.arima(): automatically select ARIMA order
library(forecast)
fit <- auto.arima(monthly_sales)
summary(fit) # shows ARIMA(p,d,q)(P,D,Q)[m]
# ARIMA(0,1,1)(1,0,0)[12] example output
# forecast
fc <- forecast(fit, h = 12)
plot(fc)
accuracy(fc) # ME, RMSE, MAE, MAPE
# manual ARIMA
fit2 <- arima(monthly_sales, order = c(1, 1, 1),
seasonal = list(order = c(1, 0, 0), period = 12))
# residuals should look like white noise
checkresiduals(fit) # Ljung-Box test
tsdisplay(residuals(fit)) # ACF + PACF of residualsxts & zoo (불규칙 시계열)
xts/zoo는 불규칙 시계열(예: 주말/휴일 누락이 있는 금융 데이터)을 위해 ts 확장. xts 객체는 실제 날짜/시간으로 인덱스, 직관적 서브셋팅 가능(1월 전체를 prices['2024-01']). merge()는 여러 계열을 날짜별 정렬, 갭을 NA로 채움(fill=na.locf로 전진 채우기 사용). rollmean/rollapply는 이동 통계 계산. endpoints/period.apply는 더粗한 주기(주, 월)로 집계. xts는 대부분의 R 금융 패키지(quantmod, TTR, PerformanceAnalytics)의 기반. tidy 시계열의 경우 tsibble/fable 패키지가 현대적 대안 제공.
library(xts)
library(zoo)
# create xts from a matrix and time index
dates <- as.Date(c("2024-01-01", "2024-01-03", "2024-01-10"))
prices <- xts(c(100, 102, 98), order.by = dates)
colnames(prices) <- "AAPL"
# subset by date range (very intuitive)
prices["2024-01-03"] # specific date
prices["2024-01-01/2024-01-05"] # date range
prices["2024-01"] # entire January
prices["/2024-01-05"] # up to a date
# lag and diff (returns xts)
lag(prices, k = 1) # previous day's price
diff(prices) # daily change
daily_returns <- diff(prices) / lag(prices, 1)
# rolling operations (zoo)
rollmean(prices, k = 3) # 3-day rolling mean
rollapply(prices, 3, sd) # 3-day rolling std dev
rollmax(prices, 3) # 3-day rolling max
# period.apply: aggregate by period
ep <- endpoints(prices, on = "weeks") # week endpoints
period.apply(prices, ep, mean) # weekly averages
# merge multiple series (aligns by date)
aapl <- xts(c(100, 102, 98), as.Date(c("2024-01-01","2024-01-02","2024-01-03")))
msft <- xts(c(200, 201), as.Date(c("2024-01-01","2024-01-03")))
merged <- merge(aapl, msft) # NA fills gaps
merged <- merge(aapl, msft, fill = na.locf) # carry forward
# to.ts <- as.ts(prices) # convert to base ts (loses dates)예측 평가와 시각화
항상 in-sample이 아닌 보류된 테스트 셋에서 예측 평가. accuracy(fit, test)는 오차 지표 계산: MAE와 RMSE(절대 스케일), MAPE(백분율, 스케일 자유이지만 0 근처에서 불안정), MASE(순진한 예측 오차로 스케일링; <1은 순진한 것보다 나음을 의미). tsCV()는 시계열 교차 검증(롤링 원점) 수행. 여러 모델(순진한 기준선, ETS, ARIMA)을 비교하고 가장 낮은 오차를 선택. autoplot() + autolayer()는 예측 구간과 함께 예측 시각화. 순진한 예측(마지막 값)은 중요한 기준선 — 모델이 유용하려면 이것을 이겨야. 시계열에 무작위 훈련/테스트 분할 절대 사용(미래 정보 누출); 항상 연대순으로 분할.
library(forecast)
library(ggplot2)
# split into train/test
train <- window(monthly_sales, end = c(2020, 9))
test <- window(monthly_sales, start = c(2020, 10))
# fit multiple models
fit_naive <- naive(train, h = 3) # naive: last value
fit_snaive <- snaive(train, h = 3) # seasonal naive
fit_ets <- ets(train) %>% forecast(h = 3) # exponential smoothing
fit_arima <- auto.arima(train) %>% forecast(h = 3)
# compare accuracy on test set
accuracy(fit_naive, test)
accuracy(fit_arima, test)
# metrics: ME, RMSE, MAE, MPE, MAPE, MASE
# time series cross-validation
ts_cv <- tsCV(train, forecastfunction = naive, h = 3)
sqrt(mean(ts_cv^2, na.rm = TRUE)) # CV RMSE
# visualize forecasts with ggplot
autoplot(monthly_sales, series = "Actual") +
autolayer(fit_arima, series = "ARIMA", PI = FALSE) +
autolayer(fit_ets, series = "ETS", PI = FALSE) +
labs(title = "Forecast Comparison", x = "Time", y = "Sales") +
theme_minimal()
# prediction intervals
fc <- forecast(auto.arima(train), h = 3, level = c(80, 95))
autoplot(fc) +
autolayer(test, series = "Actual")
# accuracy measures explained:
# MAE = Mean Absolute Error (same units as data)
# RMSE = Root Mean Squared Error (penalizes large errors)
# MAPE = Mean Absolute Percentage Error (scale-free, %)
# MASE = Mean Absolute Scaled Error (< 1 beats naive)dplyr 심층
핵심 동사: filter, select, mutate, summarize
dplyr의 5개 핵심 동사는 대부분의 데이터 조작을 다룹니다: filter(조건으로 행), select(이름으로 열), mutate(열 추가/변환), summarize(요약 통계로 축소), arrange(정렬). 파이프 %> (또는 네이티브 |>)는 작업을 좌에서 우로 연결, 코드를 읽기 쉽게 만듭니다. starts_with, ends_with, contains, everything() 같은 도우미 함수는 열 선택을 간결하게. 그룹별 통계를 위해 summarize 전에 항상 group_by; n()는 그룹당 행 계산.
library(dplyr)
df <- tibble(
name = c("Alice","Bob","Carol","Dave"),
dept = c("Eng","Sales","Eng","Sales"),
salary = c(90000, 70000, 95000, 72000),
years = c(5, 3, 8, 4)
)
# filter rows
eng <- df %>% filter(dept == "Eng", salary > 85000)
# select columns (with helpers)
df %>% select(name, salary)
df %>% select(starts_with("s"))
df %>% select(-years)
df %>% select(name:dept) # range
df %>% select(dept, everything())# reorder
# mutate: add/modify columns
df %>% mutate(
bonus = salary * 0.1,
total = salary + bonus,
level = if_else(years >= 5, "Senior", "Junior")
)
# transmute: keep only new columns
df %>% transmute(name, annual_k = salary / 1000)
# summarize (with group_by)
df %>%
group_by(dept) %>%
summarize(
avg_salary = mean(salary),
max_years = max(years),
n = n()
)조인과 집합 연산
dplyr 조인은 SQL 반영: 결합을 위한 inner, left, right, full; 다른 테이블로 필터링을 위한 semi와 anti. 의도치 않은 열에서 조용한 일치를 피하기 위해 항상 by 지정. 다른 키 이름의 경우 by = c('left_col' = 'right_col') 사용. 집합 연산(union, intersect, setdiff)은 동일한 열 집합 필요. bind_rows는 쌓음(누락된 열을 NA로 채움); bind_cols는 키 확인 없이 나란히 붙임 — 보통 조인을 원할 것. 조인은 미묘한 데이터 버그의 가장 흔한 소스이므로 전후 행 수 확인.
employees <- tibble(id = 1:4, name = c("Al","Bo","Cy","Di"))
salaries <- tibble(id = c(1,2,4,5), salary = c(50,60,80,90))
# mutating joins (combine columns)
inner_join(employees, salaries, by = "id") # only matching ids
left_join(employees, salaries, by = "id") # all from left
right_join(employees, salaries, by = "id") # all from right
full_join(employees, salaries, by = "id") # all rows
# filtering joins (filter rows, no new columns)
semi_join(employees, salaries, by = "id") # rows in left with match
anti_join(employees, salaries, by = "id") # rows in left WITHOUT match
# different column names
left_join(x, y, by = c("emp_id" = "id"))
# set operations (require identical columns)
union(a, b) # unique rows in either
union_all(a, b) # all rows (with dups)
intersect(a, b) # rows in both
setdiff(a, b) # rows in a but not b
# bind rows/columns
bind_rows(list(df1, df2)) # stack vertically
bind_cols(df1, df2) # side by side (no key check!)윈도우 함수와 그룹화된 Mutate
윈도우 함수는 group_by로 정의된 그룹 내에서 작동. row_number, min_rank, dense_rank는 동점 처리가 다름. lead/lag는 인접 행 접근 — 시계열과 변화 감지에 필수. 누적 함수(cumsum, cummax, cummean)는 실행 집계 계산. slice_max/min/head/sample은 그룹당 특정 행 추출. rowwise() + c_across()는 열에 걸쳐 행별 연산 가능(벡터화보다 느리지만 때로 필요). 이 함수들은 dplyr를 SQL 윈도우 함수만큼 강력하게 만듭니다.
df <- tibble(
dept = c("A","A","A","B","B","B"),
name = c("x","y","z","p","q","r"),
salary = c(50, 70, 60, 80, 90, 75)
)
# ranking within groups
df %>% group_by(dept) %>%
mutate(
rank = row_number(), # 1, 2, 3 (ties get distinct)
min_rank = min_rank(salary), # ties get same rank, gaps
dense_rank = dense_rank(salary),
pct_rank = percent_rank(salary)
)
# offsets (lead / lag)
df %>% group_by(dept) %>%
mutate(
prev = lag(salary),
next = lead(salary),
change = salary - lag(salary)
)
# cumulative aggregates
df %>% group_by(dept) %>%
mutate(
cum_total = cumsum(salary),
cum_max = cummax(salary),
running_avg = cummean(salary)
)
# top N per group
df %>% group_by(dept) %>%
slice_max(salary, n = 2) # or slice_min, slice_head, slice_sample
# row-wise operations
df %>% rowwise() %>%
mutate(total = sum(c_across(where(is.numeric))))across, where와 다중 열 연산
across()(dplyr 1.0+)는 여러 열에 함수 적용하는 현대적 방법 — 기존 _at, _if, _all 접미사 대체. where(is.numeric)은 술어로 열 선택. .names 인자는 {col}과 {fn} 템플릿으로 출력 이름 제어. if_all/if_any는 모든/어떤 선택된 열이 조건을 만족하는 행 필터링. .data 대명사는 프로그래밍 방식 열 접근 가능(함수와 Shiny 앱에서 유용). 이 도구들은 dplyr를 많은 열에 대한 배치 연산에 고도로 표현력 있게 만듭니다.
df <- tibble(id = 1:3, a = c(1,2,3), b = c(4,5,6), c = c("x","y","z"))
# apply function to multiple columns
df %>% mutate(across(a:b, ~ .x * 10))
df %>% mutate(across(where(is.numeric), log))
df %>% mutate(across(everything(), as.character))
# summarize multiple columns
df %>% summarize(across(where(is.numeric), list(
mean = ~mean(.x),
sd = ~sd(.x)
), .names = "{.col}_{.fn}"))
# rename multiple columns
df %>% rename_with(toupper, starts_with("a"))
# conditional transformation
df %>% mutate(across(where(is.numeric), ~ if_else(.x > 3, "high", "low")))
# use .data pronoun for programmatic access
col <- "a"
df %>% mutate(new = .data[[col]] * 2)
# pick columns by type in any verb
df %>% filter(if_all(a:b, ~ .x > 1)) # all must satisfy
df %>% filter(if_any(a:b, ~ .x > 4)) # any must satisfy데이터베이스 백엔드와 dbplyr
dbplyr는 dplyr 동사를 SQL로 변환, 로컬 데이터 프레임과 같은 문법으로 데이터베이스 테이블 조작 가능. 빅 데이터에 큰 장점: 무거운 계산은 데이터베이스에서(종종 열형/병렬), 결과만 collect()로 R로 가져옵니다. show_query()는 디버깅을 위해 생성된 SQL 공개. 대부분의 dplyr 동사는 직접 변환; 윈도우 함수와 일부 문자열 연산은 SQL 특정 함수가 필요할 수 있음. 완료 시 항상 dbDisconnect. 프로덕션의 경우 연결 풀과 SQL 주입 방지 매개변수화 쿼리 사용.
library(DBI)
library(dbplyr)
# connect to a database
con <- dbConnect(RSQLite::SQLite(), "my.db")
# or: con <- dbConnect(odbc::odbc(), "PostgreSQL")
# copy data to database
copy_to(con, mtcars, "mtcars_db", temporary = FALSE)
# reference a remote table
mtcars_remote <- tbl(con, "mtcars_db")
# dplyr verbs translate to SQL!
mtcars_remote %>%
group_by(cyl) %>%
summarize(avg_mpg = mean(mpg), n = n()) %>%
arrange(desc(avg_mpg))
# see the generated SQL
mtcars_remote %>%
filter(mpg > 20) %>%
select(mpg, cyl, hp) %>%
show_query()
# collect: pull results into local tibble
result <- mtcars_remote %>%
filter(cyl == 4) %>%
collect()
# write back to database
copy_to(con, result, "efficient_cars")
dbDisconnect(con)ggplot2 심층
그래픽의 문법과 레이어 플롯
ggplot2는 그래픽의 문법 위에 구축: 모든 플롯은 데이터, 미학 매핑(aes), 기하 객체(geom_*), 통계(stat_*), 스케일, 좌표계, 패싯의 조합. 레이어는 +로 추가. 미학은 데이터 열을 시각적 속성(x, y, color, size, shape)에 매핑; 고정 값은 aes() 밖에. facet_wrap과 facet_grid는 작은 배수 생성 — 가장 강력한 탐색 도구 중 하나. 대부분의 geom은 기본 stat(예: geom_bar는 stat_count 사용), 그러나 사용자 정의 집계를 위해 stat_summary로 재정의 가능.
library(ggplot2)
df <- data.frame(x = 1:10, y = (1:10)^2 + rnorm(10))
# basic structure: data + aesthetic + geom
ggplot(df, aes(x = x, y = y)) +
geom_point()
# multiple layers
ggplot(df, aes(x, y)) +
geom_point(color = "blue", size = 3) +
geom_smooth(method = "lm", se = TRUE)
# aesthetics map data to visual properties
ggplot(mpg, aes(displ, hwy, color = class)) +
geom_point() +
geom_smooth(method = "loess", se = FALSE)
# facets: small multiples
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_wrap(~ class, ncol = 4)
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_grid(drv ~ cyl) # rows ~ cols
# statistics (compute then plot)
ggplot(diamonds, aes(price)) +
geom_histogram(bins = 50) # stat_bin
ggplot(diamonds, aes(cut, price)) +
stat_summary(fun = "mean", geom = "bar")스케일, 테마와 주석
스케일은 데이터에서 시각적 속성으로의 매핑 제어 — 모든 미학은 해당 스케일(scale_x_*, scale_color_* 등)을 가짐. scale_*_log10, scale_*_sqrt는 축 변환; scale_*_continuous/discrete/manual은 값 사용자 정의. Viridis 팔레트는 색맹 안전이고 그레이스케일에서 잘 인쇄. labs()는 한 호출에 모든 라벨 설정. theme()은 비데이터 요소 제어(글꼴, 그리드선, 범례 위치); theme_minimal 또는 theme_classic에서 시작하여 조정. annotate()는 데이터와 무관한 고정 요소(텍스트, 직사각형, 선분) 추가.
p <- ggplot(mpg, aes(displ, hwy, color = class)) +
geom_point(size = 3)
# scales control how data maps to visuals
p + scale_x_log10() +
scale_y_continuous(limits = c(0, 50), breaks = seq(0, 50, 10))
# manual colors
p + scale_color_manual(values = c("red","blue","green"))
# color brewer / viridis (colorblind-safe)
p + scale_color_brewer(palette = "Set1")
p + scale_color_viridis_d()
# labels
p + labs(
title = "Fuel Efficiency",
subtitle = "By vehicle class",
x = "Engine Displacement (L)",
y = "Highway MPG",
color = "Class",
caption = "Source: EPA"
)
# themes
p + theme_minimal()
p + theme_classic()
p + theme(
plot.title = element_text(face = "bold", size = 16),
panel.grid.minor = element_blank(),
legend.position = "bottom"
)
# annotations
p + annotate("text", x = 5, y = 40, label = "Outlier", color = "red") +
annotate("rect", xmin = 4, xmax = 6, ymin = 30, ymax = 45,
alpha = 0.2, fill = "blue")통계 Geoms와 분포
통계 geom은 분포와 요약 시각화. 박스플롯은 사분위수와 이상치 표시; 바이올린은 밀도 형태 추가. geom_density는 히스토그램 평활; geom_bin2d/hex는 큰 데이터셋의 2D 분포 표시. geom_qq는 정규성 검사(점이 선을 따라야). geom_errorbar/geom_pointrange는 불확실성 표시. 대응 비교의 경우 ggsignif가 유의성 괄호 추가. ggridges 패키지는 능선 플롯 생성 — 많은 그룹에 걸쳐 분포 비교에 탁월. 항상 기저 데이터를 정직하게 표현하는 geom 선택.
# boxplot by group
ggplot(mpg, aes(class, hwy)) +
geom_boxplot() +
coord_flip()
# violin + boxplot overlay
ggplot(mpg, aes(class, hwy, fill = class)) +
geom_violin() +
geom_boxplot(width = 0.1)
# density plot
ggplot(mpg, aes(hwy, fill = drv)) +
geom_density(alpha = 0.5)
# 2D density / heatmap
ggplot(diamonds, aes(carat, price)) +
geom_bin2d(bins = 50) # or geom_hex()
# quantile-quantile plot
ggplot(mtcars, aes(sample = mpg)) +
geom_qq() + geom_qq_line()
# error bars
df <- data.frame(group = c("A","B","C"),
mean = c(5, 7, 4), se = c(0.5, 0.8, 0.3))
ggplot(df, aes(group, mean)) +
geom_col() +
geom_errorbar(aes(ymin = mean - se, ymax = mean + se), width = 0.2)
# ridgeline plot (ggridges)
# library(ggridges)
# ggplot(diamonds, aes(price, cut, fill = cut)) + geom_density_ridges()패싯, 좌표계와 확장
scales = 'free' 패싯은 각 패널이 자체 축 범위를 가짐 — 그룹이 매우 다른 스케일을 가질 때 유용. coord_polar는 막대 차트를 파이/레이더 차트로; coord_flip은 축 교환(수평 막대 차트에 편리). sf 패키지는 지도를 위해 geom_sf와 공간 데이터 통합. patchwork는 +, /, | 연산자로 여러 플롯 결합 — gridExtra보다 훨씬 유연. ggsave는 PNG/PDF/SVG로 내보냄; cairo_pdf는 사용자 정의 글꼴 처리. ggplotly는 ggplot을 대화형 HTML 위젯으로 변환하여 웹 배포.
# free scales per facet
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_wrap(~ class, scales = "free")
# polar coordinates (pie chart from bar)
ggplot(mtcars, aes(x = factor(1), fill = factor(cyl))) +
geom_bar(width = 1) +
coord_polar(theta = "y")
# flipped coordinates
ggplot(mpg, aes(class, hwy)) +
geom_boxplot() +
coord_flip()
# map coordinates (sf)
# library(sf)
# ggplot(nc_sf) + geom_sf(aes(fill = AREA))
# patchwork: combine multiple plots
# library(patchwork)
# p1 <- ggplot(...)
# p2 <- ggplot(...)
# p1 + p2
# p1 / (p2 + p3)
# save plots
ggsave("plot.png", width = 8, height = 6, dpi = 300)
ggsave("plot.pdf", device = cairo_pdf) # vector, supports fonts
# interactive (plotly)
# library(plotly)
# ggplotly(p)재현 가능한 플로팅과 함수
ggplot 호출을 함수로 감싸면 재사용 가능. {{ }} 연산자(tidy 평가)는 인용되지 않은 열 이름 전달; .data[[string]]은 프로그래밍 방식 접근 처리. 저장된 플롯 객체(.rds)는 다시 로드하고 수정 가능 — 약간의 변형이 필요한 보고서에 유용. 사용자 정의 테마는 한 번 정의하여 모든 곳에 적용, 프로젝트 전반의 시각적 일관성 보장. 배치 생성의 경우 .data[[]]와 ggsave로 열 이름 반복. 이 함수형 접근은 Shiny 앱과 자동 보고 파이프라인에 필수.
# wrap plots in functions for reuse
make_scatter <- function(data, x, y, color = NULL) {
ggplot(data, aes({{ x }}, {{ y }}, color = {{ color }})) +
geom_point() +
theme_minimal()
}
make_scatter(mpg, displ, hwy, class)
# using .data pronoun (string column names)
plot_col <- function(data, col) {
ggplot(data, aes(.data[[col]])) +
geom_bar() +
theme_minimal()
}
plot_col(mpg, "class")
# save and load plot objects
p <- ggplot(mpg, aes(displ, hwy)) + geom_point()
saveRDS(p, "plot.rds")
p_loaded <- readRDS("plot.rds")
# modify saved plot
p_loaded + geom_smooth()
# themes as reusable objects
my_theme <- theme_minimal() +
theme(text = element_text(family = "Helvetica"),
plot.title = element_text(face = "bold"))
ggplot(mpg, aes(displ, hwy)) + geom_point() + my_theme
# programmatic plot generation
for (col in c("hwy","cty","cyl")) {
p <- ggplot(mpg, aes(.data[[col]])) + geom_histogram()
ggsave(paste0("hist_", col, ".png"), p)
}tidyr 데이터 정리
Pivot Longer & Wider
Tidy 데이터는 관찰당 한 행, 변수당 한 열 — 대부분의 분석 함수가 이 형식 기대. pivot_longer는 넓은 것을 긴 것으로 변환(열을 키-값 쌍으로 수집); pivot_wider는 역방향. names_to의 .value 센티넬은 열 이름의 부분을 별도 열로 유지(예: 'a_1'이 a=1, b=1이 됨). 항상 플롯이나 모델링 전에 재구조화: ggplot2는 그룹화된 미학에 긴 형식 원함; 일부 모델링 함수는 넓은 형식 원함. names_pattern 인자는 정규식으로 더 복잡한 열 이름 구조 처리.
library(tidyr)
# wide format (one row per observation, columns for each variable)
wide <- tibble(
country = c("A","B","C"),
`2020` = c(100, 150, 200),
`2021` = c(110, 160, 210),
`2022` = c(120, 170, 220)
)
# pivot_longer: wide -> long
long <- wide %>%
pivot_longer(
cols = -country, # all columns except country
names_to = "year",
values_to = "gdp"
)
# A tibble: 9 x 3
# pivot_wider: long -> wide
wide2 <- long %>%
pivot_wider(names_from = year, values_from = gdp)
# multiple value columns
df <- tibble(id = 1:2, a_1 = c(1,2), a_2 = c(3,4), b_1 = c(5,6), b_2 = c(7,8))
df %>%
pivot_longer(
-id,
names_to = c(".value", "time"), # .value keeps a, b as columns
names_sep = "_"
)
# id time a b
# 1 1 1 5
# 1 2 3 7Separate, Unite & Extract
separate는 구분자로 열을 여러 열로 분할; unite는 여러 열을 하나로 결합. extract는 정규식 캡처 그룹으로 더 유연한 분할. separate_rows는 구분된 문자열을 여러 행으로 폭발 — 셀이 리스트(예: 태그, 범주)를 포함할 때 필수. convert = TRUE 옵션은 타입 자동 변환(숫자, 날짜). 이 함수들은 지저분한 실제 데이터 정리: 전체 이름 분할, 날짜 파싱, 구분된 필드 정규화. pivot_*와 결합하여 거의 모든 재구조화 작업 처리.
df <- tibble(
name = c("John_Smith", "Jane_Doe", "Bob_Jones"),
date_range = c("2020-01-01_2020-12-31", "2021-01-01_2021-12-31", "2022-01-01_2022-12-31")
)
# separate one column into many
df %>% separate(name, into = c("first", "last"), sep = "_")
# separate with conversion
df %>% separate(name, into = c("first","last"), sep = "_", convert = TRUE)
# extract with regex groups
df %>%
extract(date_range,
into = c("start", "end"),
regex = "(.+)_(.+)")
# unite multiple columns into one
df2 <- tibble(first = c("John","Jane"), last = c("Smith","Doe"))
df2 %>% unite("full_name", first:last, sep = " ")
# separate_rows: split delimited values into rows
df3 <- tibble(id = 1:2, tags = c("a,b,c", "x,y"))
df3 %>% separate_rows(tags, sep = ",")
# id tags
# 1 a
# 1 b
# 1 c
# 2 x
# 2 y결측값 처리
실제 데이터에는 결측값이 도처에 있습니다. drop_na는 NA가 있는 행 제거; replace_na는 상수로 채움. fill은 마지막 관찰을 전진(LOCF) — 시계열에서 흔함. coalesce는 열에 걸쳐 첫 번째 비-NA 선택(겹치는 소스 병합에 유용). na_if는 센티넬 값(-99나 'N/A'처럼)을 적절한 NA로 변환. 대체 전에 항상 값이 왜 결측인지 조사; MCAR(완전히 무작위 결측), MAR, MNAR는 다른 함의. 정교한 대체의 경우 mice 또는 Amelia 패키지 사용.
df <- tibble(
x = c(1, 2, NA, 4, 5),
y = c(NA, 2, 3, NA, 5),
z = c("a", NA, "c", "d", NA)
)
# drop rows with any NA
df %>% drop_na()
df %>% drop_na(x) # only column x
# replace NA with a value
df %>% replace_na(list(x = 0, y = -1, z = "unknown"))
# fill NA with previous/next value
df %>% fill(x, y, .direction = "down") # carry forward
df %>% fill(x, y, .direction = "up") # carry backward
# coalesce: first non-missing value
df %>% mutate(x_filled = coalesce(x, y, 0))
# na_if: replace specific value with NA
df %>% mutate(z = na_if(z, "a"))
# detect missing values
is.na(df$x)
sum(is.na(df)) # total NAs
df %>% map_df(~ sum(is.na(.))) # NAs per column
# imputation (simple)
df %>% mutate(x = if_else(is.na(x), mean(x, na.rm = TRUE), x))네스팅과 리스트 열
리스트 열은 행당 여러 값(또는 전체 tibble, 모델, 플롯) 저장 — 분할-적용-결합 워크플로에 강력. nest()는 행을 중첩 tibble로 그룹화; map()은 각각에 함수 적용; unnest()는 결과를 다시 펼침. 이 패턴(nest → map → unnest)은 많은 for-루프를 대체하고 그룹별 분석을 하는 관용적 tidyverse 방식. broom::tidy/glance/augment는 모델 객체를 tibble로 변환, 중첩 친화적으로 만듦. 리스트 열은 또한 R에서 purrr 기반 함수형 프로그래밍의 기반.
# nest: group rows into list-columns
nested <- mtcars %>%
group_by(cyl) %>%
nest()
# cyl data
# 4 <tibble 11x10>
# 6 <tibble 7x10>
# 8 <tibble 14x10>
# access nested data
nested$data[[1]] # first group's tibble
# fit models to each group
models <- nested %>%
mutate(
model = map(data, ~ lm(mpg ~ wt, data = .x)),
glance = map(model, broom::glance),
tidy = map(model, broom::tidy)
)
# unnest results
models %>% unnest(glance)
models %>% unnest(tidy)
# unnest a list-column back to rows
df <- tibble(id = 1:2, vals = list(c(1,2,3), c(4,5)))
df %>% unnest(vals)
# id vals
# 1 1
# 1 2
# 1 3
# 2 4
# 2 5
# chop/unchop (similar but keeps other columns as lists)
df %>% chop(vals)
df %>% unchop(vals)렉탱글링과 JSON
렉탱글링은 중첩/계층적 데이터(JSON, API 응답)를 tidy tibble로 변환. hoist()는 리스트 열에서 특정 요소 추출; unnest_wider()는 리스트를 열로 펼침; unnest_longer()는 각 요소를 행으로 확장. 깊이 중첩된 구조의 경우 purrr의 map 함수와 tibble 구성 결합. jsonlite::fromJSON with simplifyDataFrame = TRUE는 단순 JSON 자동 평탄화. 이 워크플로는 REST API, NoSQL 데이터베이스, 구성 파일 작업에 필수 — 현대 데이터 엔지니어의 일상.
library(jsonlite)
library(tidyr)
# parse JSON
json <- '[
{"name":"Alice","age":30,"skills":["R","Python"]},
{"name":"Bob","age":25,"skills":["SQL"]}
]'
parsed <- fromJSON(json, simplifyDataFrame = FALSE)
# convert list to tibble
tibble(person = parsed) %>%
hoist(person,
name = "name",
age = "age",
skills = "skills"
)
# name age skills
# Alice 30 <chr [2]>
# Bob 25 <chr [1]>
# unnest longer for nested lists
tibble(person = parsed) %>%
unnest_wider(person) # spread list to columns
tibble(skills = list(c("R","Python"), c("SQL"))) %>%
unnest_longer(skills)
# deeply nested: use purrr + tibble
flatten_df <- function(lst) {
tibble(
name = lst$name,
age = lst$age,
n_skills = length(lst$skills)
)
}
map_dfr(parsed, flatten_df)
# rectangularize arrays
jsonlite::fromJSON(json, simplifyDataFrame = TRUE) # auto-flattenpurrr 함수형 프로그래밍
map 계열과 타입 안전 변형
purrr의 map 계열은 lapply/sapply를 일관되고 타입 안전한 변형으로 대체. map_dbl/chr/int/lgl은 타입화된 벡터 반환(불일치 시 오류) — 조용히 강제 변환하는 sapply보다 훨씬 안전. map2와 pmap은 여러 벡터를 병렬로 반복. imap은 값과 인덱스 모두 제공. walk는 반환 값이 필요 없는 부작용(출력, 파일 쓰기)용. ~ .x 단축키는 익명 함수 생성; .x는 첫 번째 인자, .y는 두 번째. 타입 불안정성을 피하기 위해 프로덕션 코드에서 sapply보다 map_* 항상 선호.
library(purrr)
# map: apply function to each element, return list
map(1:3, ~ .x ^ 2) # list(1, 4, 9)
# type-specific variants (safer, faster)
map_dbl(1:3, ~ .x ^ 2) # numeric vector: 1 4 9
map_chr(c(1,2,3), ~ paste("n=", .x))
map_int(c(1.5, 2.7), floor)
map_lgl(c(1, NA, 3), ~ !is.na(.x))
# map over two vectors in parallel
map2_dbl(c(1,2,3), c(10,20,30), ~ .x + .y) # 11 22 33
# map over multiple vectors (pmap)
pmap_dbl(list(a = 1:3, b = 4:6, c = 7:9), sum) # 12 15 18
# map over indices (imap)
imap_chr(c("a","b","c"), ~ paste0(.y, ":", .x))
# "1:a" "2:b" "3:c"
# walk: side effects (no return value)
walk(c("file1.csv","file2.csv"), ~ print(read.csv(.x)))
# map over columns of a data frame
map_dbl(mtcars, mean) # mean of each column
map(mtcars, class) # class of each column익명 함수와 공식 문법
purrr는 함수 지정을 위한 여러 방법 제공: 단순 경우의 공식 단축키(~ .x + 1), 복잡한 본문의 전체 function(x) 문법, 재사용을 위한 이름 있는 함수. map 함수는 또한 이름/위치로 추출하기 위해 문자열/숫자 허용 — 래퍼 함수 불필요. pluck()은 .default 대체와 함께 깊이 중첩된 구조를 안전하게 탐색; chuck()는 누락된 경로에서 오류를 발생시키는 엄격한 버전. 이것은 purrr를 JSON, API 응답, base R의 [[ 추출이 다루기 어려워지는 기타 계층적 데이터 작업에 이상적으로 만듭니다.
# formula syntax (~ creates a function)
map(1:3, ~ .x + 10) # .x is the argument
map2(1:3, 10:12, ~ .x + .y) # .x, .y for two args
pmap(list(1:3, 4:6, 7:9), ~ ..1 + ..2 + ..3) # ..1, ..2 for many
# full function syntax (for complex bodies)
map(1:3, function(x) {
if (x > 2) return(x * 10)
x
})
# named function
square <- function(x) x^2
map(1:3, square)
# extract by name/position (no function needed)
map(list(a = list(x = 1), b = list(x = 2)), "x")
map(list(list(1,2), list(3,4)), 1) # first element of each
# pluck: safely extract deeply nested
deep <- list(a = list(b = list(c = 42)))
pluck(deep, "a", "b", "c") # 42
pluck(deep, "a", "x", "y", .default = NA) # NA (no error)
# chuck: same but errors if missing
# chuck(deep, "a", "x") # errorReduce, Accumulate와 술어 함수
reduce()는 요소를 쌍으로 결합(왼쪽 접기) — 많은 데이터 프레임 병합이나 곱 계산에 완벽. accumulate()는 중간 결과 유지, 실행 총계에 유용. keep/discard는 술자로 요소 필터(dplyr::filter처럼 but for vectors/lists). some/every는 어떤/모든 요소가 조건을 만족하는지 검사. detect는 첫 번째 일치 요소 찾기. 이 고차 함수는 많은 루프를 간결하고 선언적인 코드로 대체. negate()는 술어 함수 반전 — 조건 구성에 편리. 함께 purrr를 완전한 함수형 프로그래밍 툴킷으로 만듭니다.
# reduce: combine elements pairwise
reduce(c(1,2,3,4), `+`) # 10 (sum)
reduce(c(1,2,3,4), `*`) # 24 (product)
reduce(list(df1, df2, df3), full_join, by = "id") # merge many
# accumulate: keep intermediate results
accumulate(c(1,2,3,4), `+`) # 1 3 6 10 (running sum)
accumulate(c(2,3,4), `*`) # 2 6 24 (running product)
# keep/discard: filter by predicate
keep(1:10, ~ .x %% 2 == 0) # 2 4 6 8 10
discard(1:10, ~ .x %% 2 == 0) # 1 3 5 7 9
keep(mtcars, is.numeric) # keep numeric columns
# some/every/detect: test predicates
some(1:10, ~ .x > 5) # TRUE
every(1:10, ~ is.numeric(.x)) # TRUE
detect(1:10, ~ .x > 5) # 6 (first match)
detect_index(1:10, ~ .x > 5) # 6 (position)
# head_while/tail_while
head_while(1:10, ~ .x < 5) # 1 2 3 4
tail_while(10:1, ~ .x > 5) # 10 9 8 7 6
# negate a predicate
negate(is.na)(5) # TRUE
keep(c(1, NA, 3), negate(is.na)) # 1 3Safely, Possibly와 오류 처리
safely()는 함수를 감싸 항상 'result'와 'error'를 가진 리스트 반환 — 절대 throw하지 않음. 하나의 실패가 전체 실행을 멈추지 않아야 하는 배치 작업에 필수. possibly()는 오류 시 기본값 반환(오류 세부 정보가 필요 없을 때 더 깔끔). quietly()는 경고와 메시지 캡처. transpose()는 {result, error} 쌍의 리스트를 별도 리스트로 변환 — 성공과 실패 분리에 편리. 개별적으로 실패할 수 있는 많은 항목(API 호출, 파일 읽기, 모델 피팅) 처리 시 사용.
# safely: capture errors without stopping
safe_log <- safely(log)
safe_log(10) # list(result = 2.3, error = NULL)
safe_log(-1) # list(result = NULL, error = <error>)
# process many, capturing failures
results <- map(c(10, -1, 0, "x"), safely(log))
successes <- map(results, "result") %>% discard(is.null)
failures <- map(results, "error") %>% discard(is.null)
# possibly: return default on error
safe_log2 <- possibly(log, otherwise = NA_real_)
map_dbl(c(10, -1, 0, "x"), safe_log2) # 2.3 NA NA NA
# quietly: capture warnings/messages
quiet_log <- quietly(log)
quiet_log(10) # list(result, warnings, messages)
# transpose: restructure list-of-lists
transposed <- transpose(results)
transposed$result # all results
transposed$error # all errors
# rate-limited / retried operations
# library(purrr)
# safely_slow <- slowly(safely(f), rate = rate_backoff())
# walk + safely for batch file processing
walk(files, ~ safely(read.csv)(.x))벡터화와 병렬 purrr
modify()는 map()처럼 but 입력 타입 보존 — 데이터 프레임 열을 제자리에서 변환에 완벽. modify_if와 modify_at은 특정 열 대상. list_modify/list_merge는 리스트를 비파괴적으로 업데이트. 병렬의 경우 furrr는 future 백엔드를 사용하는 future_map(map의 드롭인 대체) 제공 — plan() 변경으로 순차에서 병렬로 전환. .progress = TRUE 옵션은 진행 막대 표시, 오래 실행되는 map에 귀중. 이 도구들은 purrr를 대화형 탐색과 프로덕션 파이프라인 모두에 적합하게 만듭니다.
# vectorized map (faster for simple operations)
# map_vec returns a vector, auto-detecting type
map_vec(1:3, ~ .x * 2) # numeric vector
map_vec(c("a","b"), ~ toupper(.x)) # character vector
# modify: like map but preserves type
modify(mtcars, ~ .x * 2) # still a data frame
modify_if(mtcars, is.numeric, ~ .x * 2)
modify_at(mtcars, c("mpg","cyl"), ~ .x * 2)
# list_modify / list_merge
l1 <- list(a = 1, b = 2)
list_modify(l1, b = 20, c = 30) # a=1, b=20, c=30
list_merge(l1, list(b = 20, c = 30))
# parallel mapping with future + furrr
# library(furrr)
# plan(multisession) # parallel backend
# future_map(1:100, slow_function, .options = furrr_options(seed = TRUE))
# progress bar
# walk(1:100, ~ Sys.sleep(0.1), .progress = TRUE)
# conditional execution in map
map(1:5, ~ if (.x > 3) .x * 10 else .x)
# 1 2 3 40 50stringr & lubridate
stringr 패턴 매칭과 추출
stringr은 ICU 정규식 엔진을 감싸는 일관되고 파이프 친화적인 API 제공. 모든 함수는 str_로 시작하여 자동 완성 용이. str_detect/which/subset은 패턴으로 필터. str_extract/match는 일치 추출(match는 그룹 캡처). str_replace/remove는 텍스트 수정. str_split은 문자열을 조각으로 분할. 기저 정규식 문법은 표준(PCRE 유사), \\w, \\d, \\s 같은 도우미. 고정 문자열(정규식 아님)의 경우 str_detect(text, fixed('a.b'))로 문자 그대로 매치. stringr은 base R의 grep/sub 계열보다 훨씬 더 일관됨.
library(stringr)
text <- c("apple pie", "banana bread", "cherry tart")
# detect pattern
str_detect(text, "pie") # TRUE FALSE FALSE
str_which(text, "pie") # 1
str_count(text, "a") # 1 3 1
# subset strings
str_subset(text, "pie") # "apple pie"
str_extract(text, "[aeiou]") # first vowel
str_extract_all(text, "[aeiou]") # list of all vowels
# match groups
str_match(text, "(\\w+) (\\w+)") # matrix with groups
str_match_all(text, "(\\w+)")
# locate pattern positions
str_locate(text, "a") # start/end matrix
str_locate_all(text, "a")
# replace
str_replace(text, "a", "A") # first match
str_replace_all(text, "a", "A") # all matches
str_remove(text, "a") # str_replace(text, "a", "")
# split
str_split("a,b,c", ",") # list of vectors
str_split_fixed("a,b,c", ",", 3) # matrix문자열 조작과 변환
stringr은 일관된 인터페이스로 모든 일반 문자열 연산을 다룹니다. 대소문자 변환(str_to_upper/lower/title/sentence)은 로케일 존중. str_trim은 공백 제거; str_squish는 내부 공백도 축소. str_pad는 문자열을 고정 너비로 정렬(표 형식화에 유용). str_sub는 R의 1-기반 인덱싱으로 부분문자열 추출 또는 교체(음수 인덱스는 끝에서부터). str_c는 paste0의 파이프 친화적 동등물. 이 함수들은 base R의 분산된 문자열 함수에 비해 문자열 조작을 예측 가능하고 읽기 쉽게 만듭니다.
library(stringr)
# case conversion
str_to_upper("hello") # "HELLO"
str_to_lower("WORLD") # "world"
str_to_title("the wind in the willows") # "The Wind In The Willows"
str_to_sentence("hello world") # "Hello world"
# trimming and padding
str_trim(" hello ") # "hello" (both sides)
str_trim(" hello ", side = "left")
str_squish(" hello world ") # "hello world" (collapse spaces)
str_pad("5", width = 3, pad = "0") # "005"
str_pad("5", width = 3, side = "left", pad = "0") # "005"
# subsetting
str_sub("hello", 1, 3) # "hel"
str_sub("hello", -3, -1) # "llo" (negative from end)
str_sub("hello", 2) # "ello" (to end)
str_sub("hello", 1, 1) <- "H" # assignment: "Hello"
# length
str_length("hello") # 5
str_length(c("a","bb","ccc")) # 1 2 3
# combine and duplicate
str_c("a", "b", "c", sep = "-") # "a-b-c"
str_c(c("x","y"), collapse = ",") # "x,y"
str_dup("ab", 3) # "ababab"
# truncate
str_trunc("Hello World", 8) # "Hello..."정규 표현식 심층
stringr은 표준 문법으로 ICU 정규식 엔진 사용. ^와 $는 시작/끝 앵커. 문자 클래스 [a-z]는 범위 매치; \w, \d, \s는 단축키. 수량자 {n,m}, ?, +, *는 반복 제어. 괄호는 캡처 그룹 생성; |는 대안. 룩어헤드(?=)와 룩비하인드(?<=)는 소비 없이 주장. 이름 있는 그룹(?<name>...)은 추출을 자체 문서화. 정규식 메타문자를 포함한 리터럴 문자열 매칭 시 항상 fixed() 사용 — 더 빠름. 복잡한 파싱의 경우 정규식을 읽기 쉽게 구축하는 rebus 패키지 고려.
library(stringr)
# anchors
str_detect(c("cat","scat","catch"), "^cat") # starts with: T F T
str_detect(c("cat","scat","catch"), "cat$") # ends with: T F F
# character classes
str_extract_all("a1 b2 c3", "[a-z]") # letters
str_extract_all("a1 b2 c3", "[0-9]") # digits
str_extract_all("a1 b2 c3", "[^0-9]") # non-digits
str_extract_all("a1 b2 c3", "\\w") # word chars
str_extract_all("a1 b2 c3", "\\s") # whitespace
# quantifiers
str_detect("aaa", "a{2,3}") # 2-3 a's
str_detect("color", "colou?r") # u optional
str_detect("abbbbc", "ab+c") # one or more
str_detect("ac", "ab*c") # zero or more
# groups and alternation
str_match("2024-01-15", "(\\d{4})-(\\d{2})-(\\d{2})")
str_detect("cat", "cat|dog|bird") # alternation
# lookahead/lookbehind
str_extract("abc123", "(?<=abc)\\d+") # 123 (lookbehind)
str_extract("abc123", "\\d+(?=end)") # nothing (no 'end')
# named capture (stringr 1.5+)
str_match("John 30", "(?<name>[A-Za-z]+) (?<age>\\d+)")
# use fixed() for literal matching
str_detect("a.b.c", fixed(".")) # TRUE (no regex)lubridate 날짜와 시간 파싱
lubridate의 파싱 함수(ymd, mdy, dmy)는 구분자와 형식을 자동 감지 — base R의 strptime보다 훨씬 관대. 함수 이름은 순서 표시: ymd = 년-월-일. make_date/assemble은 구성 요소에서 구축. today()와 now()는 현재 날짜/시간 반환. 구성 요소 함수(year, month, day, wday, yday)는 값을 가져오고 설정; wday(label=TRUE)는 요일 이름 반환. update()는 여러 구성 요소를 한 번에 수정. 조용한 UTC 가정을 피하기 위해 datetime에 항상 tz(시간대) 명시적 지정.
library(lubridate)
# parse dates (auto-detect format)
ymd("2024-01-15") # 2024-01-15
ymd("2024/01/15")
ymd("24/1/15")
mdy("01-15-2024") # month-day-year
dmy("15/01/2024") # day-month-year
ymd_hms("2024-01-15 14:30:00")
ymd_hm("2024-01-15 14:30")
hms("14:30:45")
# from components
make_date(2024, 1, 15)
make_datetime(2024, 1, 15, 14, 30, 0, tz = "UTC")
# current date/time
today() # 2024-01-15
now() # 2024-01-15 14:30:00 UTC
# extract components
d <- ymd("2024-01-15")
year(d) # 2024
month(d) # 1
month(d, label = TRUE) # "Jan"
day(d) # 15
wday(d, label = TRUE) # "Mon"
yday(d) # 15 (day of year)
quarter(d) # 1
semester(d) # 1
# set components
year(d) <- 2025
month(d) <- 6
d <- update(d, year = 2025, month = 6, day = 1)시간대, 기간과 간격
시간대는 datetime 처리의 가장 까다로운 부분. with_tz는 같은 순간을 다른 존에 표시; force_tz는 시계를 변경하지 않고 존 변경(잘못 라벨링된 데이터 수정에 유용). Durations(dseconds, dhours)는 정확한 초 — 물리에 좋음. Periods(minutes, hours, days)는 달력 인식: 1월 31일에 months(1) 추가는 2월 28일, days(1)는 DST 전환 처리. Intervals(start %--% end)는 고정 끝점으로 범위 나타냄. 인간 규모 산술(스케줄링)에는 periods, 경과 시간 측정에는 durations 사용.
library(lubridate)
# timezones
t <- ymd_hms("2024-01-15 14:30:00", tz = "UTC")
with_tz(t, "America/New_York") # 09:30 EST
with_tz(t, "Asia/Shanghai") # 22:30 CST
force_tz(t, "America/New_York") # keeps clock, changes zone
# list timezones
OlsonNames()[1:5]
# durations (exact seconds)
d <- dseconds(60) + dminutes(2) # 180 seconds
as.numeric(d, "seconds")
dhours(1) + ddays(1)
# periods (calendar-aware, variable length)
p <- minutes(5) + hours(2)
ymd("2024-01-15") + p # 2024-01-15 02:05:00
ymd("2024-03-10") + days(1) # handles DST
months(1) # variable length!
# intervals (start to end)
int <- interval(ymd("2024-01-01"), ymd("2024-02-01"))
int_length(int) # seconds
int %within% int2 # test overlap
as.period(int) # "1M 0S"
# arithmetic
ymd("2024-01-15") %--% ymd("2024-02-15") # interval
ymd("2024-01-15") + weeks(2) # period
ymd("2024-01-15") + dweeks(2) # duration (exact)
# rounding dates
floor_date(t, "hour") # round down
ceiling_date(t, "day")
round_date(t, "hour")R Markdown과 보고
R Markdown 기본과 청크
R Markdown은 산문(Markdown), 코드(R/Python/SQL), 출력(표, 플롯)을 재현 가능한 보고서로 결합. YAML 헤더는 메타데이터와 출력 형식 설정. 삼중 백틱으로 구분된 코드 청크는 knit 시 실행; 청크 옵션은 동작 제어(echo=FALSE는 코드 숨김, include=FALSE는 실행하지만 아무것도 표시 안 함, fig.width는 플롯 크기 설정). 단일 백틱의 인라인 코드는 텍스트에 값 삽입. kable()은 표 형식화; 더 화려한 표는 kableExtra 또는 gt 사용. knit 버튼은 HTML/PDF/Word로 렌더링.
---
title: "Analysis Report"
author: "Data Team"
date: "`r format(Sys.Date(), '%B %d, %Y')`"
output: html_document
---
## Introduction
This is **Markdown** with embedded R.
Inline code: the mean is `r mean(mtcars$mpg)`.
```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = FALSE, warning = FALSE, message = FALSE)
library(dplyr)
library(ggplot2)
```
```{r load-data}
df <- mtcars
head(df)
```
```{r plot, fig.width=8, fig.height=6, fig.cap="MPG by weight"}
ggplot(df, aes(wt, mpg)) + geom_point() + theme_minimal()
```
```{r table}
library(knitr)
kable(summary(df), caption = "Summary statistics")
```출력 형식과 매개변수
하나의 R Markdown 파일은 같은 소스에서 여러 출력 형식(HTML, PDF, Word, 슬라이드)을 생성 가능 — output 아래에 나열. HTML 특정 옵션(toc_float, code_folding, theme)은 대화형 문서 생성. 매개변수(params)는 다른 입력으로 같은 보고서 렌더링 — 배치 보고에 필수(지역, 고객, 또는 기간당 하나). rmarkdown::render()로 프로그래밍 방식 렌더링하여 cron 작업이나 Shiny 앱에서 보고서 생성 자동화. params 객체는 데이터 필터링을 위해 청크 내부에서 사용 가능.
---
title: "Quarterly Report"
output:
html_document:
toc: true
toc_float: true
code_folding: hide
theme: flatly
df_print: paged
pdf_document:
toc: true
number_sections: true
word_document: default
params:
quarter: "Q1"
region: "North"
---
## Report for `r params$quarter` - `r params$region`
```{r}
data <- filter(all_data, quarter == params$quarter, region == params$region)
```
# Render with parameters:
# rmarkdown::render("report.Rmd", params = list(quarter = "Q2"))
# parameterized batch rendering
quarters <- c("Q1","Q2","Q3","Q4")
for (q in quarters) {
rmarkdown::render("report.Rmd",
params = list(quarter = q),
output_file = paste0("report_", q, ".html")
)
}kable, gt & DT로 표
kable + kableExtra는 스타일링, 그룹화, 조건부 형식화로 출판 품질 표 생성. gt는 더 표현적인 문법을 가진 현대적 대안(표를 위한 ggplot처럼). DT는 검색, 정렬, 페이지 매김을 가진 대화형 HTML 표 생성 — 탐색적 보고서에 완벽. reactable은 더 많은 대화형 제공. 출력 기반 선택: PDF/Word는 kable/gt, HTML은 DT/reactable. 항상 숫자를 일관되게 형식화(fmt_number, formatRound)하고 맥락을 위해 캡션 추가. 좋은 표는 결과 전달에 좋은 플롯만큼 중요.
library(knitr); library(kableExtra)
# basic kable
kable(head(mtcars), caption = "First 6 rows")
# styled kable
kable(head(mtcars), "html") %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"),
full_width = FALSE) %>%
row_spec(0, bold = TRUE, background = "lightblue") %>%
column_spec(1, bold = TRUE)
# gt package (modern, flexible)
library(gt)
mtcars %>%
head() %>%
gt() %>%
tab_header(title = "Car Specifications", subtitle = "Top 6") %>%
fmt_number(columns = mpg, decimals = 1) %>%
data_color(columns = mpg, colors = scales::col_numeric("Reds", NULL))
# DT: interactive tables
library(DT)
datatable(mtcars,
filter = "top",
options = list(pageLength = 10),
caption = "Searchable table"
) %>%
formatRound(columns = c("mpg","disp"), digits = 2)
# reactable for even more interactivity
# library(reactable)Quarto와 고급 기능
Quarto는 R Markdown의 후속, 하나의 문서에서 R, Python, Julia, Observable 지원. 교차 참조(@fig-label, @tbl-label)는 그림과 표를 자동 번호. 청크 옵션의 #| 문법은 기존 knitr opts보다 깔끔. 코드-접기는 대화형 HTML을 위해 접을 수 있는 코드 블록 생성. Quarto의 다중 언어 지원은 R과 Python을 모두 사용하는 팀에 이상적. 기존 .Rmd 파일 변환 가능; 문법은 비슷하지만 더 일관됨. Quarto는 또한 같은 소스에서 발표(revealjs), 웹사이트, 책 생성.
---
title: "Modern Report"
format:
html:
toc: true
code-fold: true
pdf:
documentclass: article
execute:
echo: false
warning: false
filters:
- lightbox
---
## Cross-references
See Figure @fig-scatter and Table @tbl-summary.
```{r}
#| label: fig-scatter
#| fig-cap: "MPG vs Weight"
ggplot(mtcars, aes(wt, mpg)) + geom_point()
```
```{r}
#| label: tbl-summary
#| tbl-cap: "Summary by cylinder"
mtcars %>%
group_by(cyl) %>%
summarize(mean_mpg = mean(mpg)) %>%
gt()
```
## Multiple languages
```{python}
import pandas as pd
df = pd.DataFrame({'a': [1,2,3]})
print(df)
```
```{sql}
SELECT * FROM mtcars WHERE mpg > 20
```
# Quarto supports Python, Julia, Observable, and more
# in the same document, sharing data via ojs.자동화된 보고와 Cron
자동화된 보고는 일회성 분석을 반복되는 산출물로 변환. rmarkdown::render()는 프로그래밍 방식으로 보고서 생성; 사용자 정의를 위해 params와 결합. blastula 또는 emayili로 결과 이메일. 매일/주간 실행을 위해 cronR(Linux/Mac) 또는 taskscheduleR(Windows)로 스케줄. 배치 생성의 경우 purrr::walk로 매개변수 반복. 비용이 많이 드는 계산의 반복 속도를 위해 청크 캐싱 활성화(cache=TRUE) — 변경된 청크만 재실행. 이 파이프라인은 R에서 비즈니스 인텔리전스와 자동화된 데이터 제품의 근간.
# render a report programmatically
rmarkdown::render("daily_report.Rmd",
output_dir = "reports",
output_file = paste0("report_", Sys.Date(), ".html"),
params = list(date = Sys.Date() - 1),
quiet = TRUE
)
# email the report
# library(blastula)
# render_email("email_template.Rmd") %>%
# smtp_send(
# to = "[email protected]",
# from = "[email protected]",
# subject = paste("Daily Report -", Sys.Date())
# )
# schedule with cron (Linux/Mac) or Task Scheduler (Windows)
# crontab -e:
# 0 8 * * * cd /path/to/project && Rscript -e 'rmarkdown::render("daily.Rmd")'
# or use the cronR package
# library(cronR)
# cmd <- cron_rscript("render_report.R")
# cron_add(cmd, frequency = "daily", at = "08:00")
# batch render multiple reports
purrr::walk(regions, function(r) {
rmarkdown::render("regional.Rmd",
params = list(region = r),
output_file = paste0("report_", r, ".html")
)
})
# version control: cache expensive computations
# knitr::opts_chunk$set(cache = TRUE)Shiny 웹 앱
앱 구조: UI와 Server
모든 Shiny 앱은 두 부분: ui(HTML 레이아웃)과 server(R 로직). UI는 fluidPage와 레이아웃 함수(sidebarLayout, tabsetPanel, navbarPage) 사용. 입력(sliderInput, selectInput 등)은 사용자 데이터 수집; 출력(plotOutput, textOutput)은 결과 표시. server 함수는 render* 함수로 연결. 반응형 표현식(reactive({...}))은 계산을 캐시하고 입력이 변경될 때만 재실행. app.R로 저장하고 runApp()으로 실행 또는 shinyapps.io / RStudio Connect / Shiny Server에 호스트.
library(shiny)
ui <- fluidPage(
titlePanel("My First Shiny App"),
sidebarLayout(
sidebarPanel(
sliderInput("n", "Number of points:", min = 1, max = 100, value = 50),
selectInput("color", "Color:", choices = c("red","blue","green"))
),
mainPanel(
plotOutput("scatter"),
verbatimTextOutput("summary")
)
)
)
server <- function(input, output, session) {
data <- reactive({
data.frame(x = rnorm(input$n), y = rnorm(input$n))
})
output$scatter <- renderPlot({
plot(data()$x, data()$y, col = input$color, pch = 19,
xlab = "X", ylab = "Y", main = paste("n =", input$n))
})
output$summary <- renderPrint({
summary(data())
})
}
shinyApp(ui, server)
# save as app.R in a folder, then runApp("folder")
# or run with shiny::runApp()반응형 프로그래밍
반응성은 Shiny의 핵심 개념. reactive()는 의존성이 변경될 때만 재실행하는 지연, 캐시된 표현식 생성. observe()는 부작용(입력 업데이트, 로깅)을 위해 즉시 실행. observeEvent/eventReactive는 특정 이벤트(버튼 클릭)에서 트리거. reactiveVal과 reactiveValues는 가변 상태 보유. isolate()는 의존성 생성 없이 값 읽기. 핵심 통찰: 출력은 반응형 의존성이 변경될 때 자동으로 재렌더링. 반응성 오해는 Shiny 버그의 #1 소스 — 의존성 그래프 디버깅을 위해 reactiveLogViewer() 사용.
server <- function(input, output, session) {
# reactive expression: lazy, cached
filtered <- reactive({
mtcars %>% filter(cyl == input$cyl)
})
# observe: eager, for side effects
observe({
updateSelectInput(session, "model",
choices = unique(filtered()$model))
})
# observeEvent: triggered by specific input
observeEvent(input$reset, {
updateSliderInput(session, "cyl", value = 4)
})
# eventReactive: lazy, triggered by event
result <- eventReactive(input$go, {
run_analysis(input$param)
})
# reactiveVal: mutable value
counter <- reactiveVal(0)
observeEvent(input$add, counter(counter() + 1))
# reactiveValues: multiple mutable values
rv <- reactiveValues(data = NULL, status = "ready")
observeEvent(input$load, {
rv$data <- read.csv(input$file$datapath)
rv$status <- "loaded"
})
# isolate: read without dependency
output$plot <- renderPlot({
plot(isolate(rv$data))
})
}동적 UI와 모듈
동적 UI(renderUI + uiOutput)는 데이터나 사용자 선택에 기반한 컨트롤 생성. insertUI/removeUI는 전체 페이지 재렌더링 없이 요소 추가/제거. 모듈(NS + moduleServer)은 재사용을 위해 UI+server 로직 캡슐화 — 반복 구성 요소가 있는 복잡한 앱에 필수. 각 모듈 인스턴스는 고유한 네임스페이스(ns)를 받아 입력 ID가 충돌하지 않음. 모듈은 유지보수 가능한 Shiny 앱 구축의 핵심: 앱을 작고 테스트 가능한 모듈(차트 모듈, 필터 모듈, 데이터 업로드 모듈)로 분할하고 결합.
# dynamic UI
ui <- fluidPage(
uiOutput("dynamic_controls"),
plotOutput("plot")
)
server <- function(input, output, session) {
output$dynamic_controls <- renderUI({
cols <- names(input$data)
selectInput("xvar", "X variable:", choices = cols)
})
# insert/remove UI
observeEvent(input$add, {
insertUI("#placeholder", "beforeEnd",
sliderInput(paste0("s", input$add), "Slider", 0, 100, 50))
})
# modules: reusable UI+server
histogramUI <- function(id) {
ns <- NS(id)
tagList(
selectInput(ns("var"), "Variable:", names(mtcars)),
plotOutput(ns("hist"))
)
}
histogramServer <- function(id) {
moduleServer(id, function(input, output, session) {
output$hist <- renderPlot(hist(mtcars[[input$var]]))
})
}
# use module
ui <- fluidPage(histogramUI("hist1"), histogramUI("hist2"))
server <- function(input, output, session) {
histogramServer("hist1")
histogramServer("hist2")
}
}입력, 출력과 렌더링
Shiny는 많은 입력 타입(file, date, slider, selectize, checkbox)과 출력 타입(plot, table, text, image, UI) 지원. DT::renderDataTable은 검색/정렬이 있는 대화형 표 생성. downloadHandler는 사용자가 데이터 내보내기 가능. .data[[]]는 ggplot에서 동적 열 선택 가능. 큰 데이터의 경우 DT의 서버측 처리 또는 대화형 플롯을 위해 plotly 사용. renderImage는 사전 생성된 파일 표시(renderPlot보다 복잡한 시각화에 더 빠름). 입력과 반응형 표현식 결합으로 정교하고 반응하는 대시보드 구축.
# file upload
fileInput("data", "Upload CSV:", accept = ".csv")
# in server: input$data$datapath (temp file path)
# date range
dateRangeInput("dates", "Period:", start = Sys.Date() - 30, end = Sys.Date())
# tabset with conditional panels
tabsetPanel(
tabPanel("Plot", plotOutput("p")),
tabPanel("Table", DT::dataTableOutput("t")),
tabPanel("Summary", verbatimTextOutput("s"))
)
# render DataTable (interactive)
output$t <- DT::renderDataTable({
datatable(filtered(), filter = "top", options = list(pageLength = 25))
})
# render UI from ggplot
output$p <- renderPlot({
ggplot(filtered(), aes(.data[[input$x]], .data[[input$y]])) +
geom_point() + theme_minimal()
})
# download handlers
output$download <- downloadHandler(
filename = function() paste0("data_", Sys.Date(), ".csv"),
content = function(file) write.csv(filtered(), file)
)
# render image (pre-generated)
output$img <- renderImage({
list(src = "plot.png", contentType = "image/png", width = 400)
}, deleteFile = FALSE)성능, 배포와 스케일링
Shiny 성능: 과도한 재계산을 피하기 위해 빠른 입력(검색 상자)을 debounce/throttle. 비동기 연산을 위해 future + promises 사용(이벤트 루프 차단 안 함). bindCache는 키로 렌더 결과 캐시 — 많은 사용자가 접근하는 비용이 많이 드는 플롯에 큰 이득. shinyapps.io(관리 클라우드), RStudio Connect(상업용), 또는 Docker 뒤의 Shiny Server(오픈 소스)에 배포. 높은 트래픽의 경우 여러 워커 프로세스 실행하고 로드 밸런스. 사용자 행동 이해와 오류 잡기 위해 shinylogs로 사용 모니터링. 병목 찾기 위해 profvis::profvis()로 느린 앱 프로파일링.
# performance: debounce/throttle rapid inputs
input_debounced <- debounce(reactive({ input$text }), 500)
# async with future
library(future)
plan(multisession)
result <- reactive({
future_promise({ expensive_computation(input$params) })
})
# caching expensive computations
# in UI: add resourcePath or use bindCache
output$plot <- renderPlot({ ... }) %>% bindCache(input$dataset)
# deploy to shinyapps.io
# library(rsconnect)
# deployApp("myapp/")
# run multiple Shiny apps behind a load balancer
# (shinyapps.io / RStudio Connect handle this automatically)
# dockerize for self-hosting
# Dockerfile:
# FROM rocker/shiny
# COPY app.R /srv/shiny-server/
# EXPOSE 3838
# shiny server config (/etc/shiny-server/shiny-server.conf)
# run_as shiny;
# server { listen 3838; location / { site_dir /srv/shiny-server; } }
# monitor with shinylogs or shiny.telemetry통계 검정과 추론
가설 검정 프레임워크
가설 검정은 관찰된 데이터가 귀무 가설과 일치하는지 평가. t-검정은 평균 비교(모수적, 정규성 가정); Wilcoxon은 비모수 대안. p-값만이 아닌 효과 크기와 신뢰 구간 항상 보고 — p는 표본 크기에 의존하는 반면 CI는 실질적 유의성 표시. 해석 전 가정 확인: 정규성(Shapiro-Wilk), 등분산(Levene). 검정력 분석(pwr 패키지)은 데이터 수집 전 필요 표본 크기 결정. 0.05 임계값은 관례적, 마법적이지 않음 — 효과 크기와 맥락 고려.
# one-sample t-test: is mean different from hypothesized value?
t.test(mtcars$mpg, mu = 20)
# t = 0.085, df = 31, p-value = 0.933
# 95% CI: [17.92, 22.26]
# mean of x: 20.09
# two-sample t-test
t.test(mpg ~ cyl, data = subset(mtcars, cyl %in% c(4, 6)))
# paired t-test
t.test(pre, post, paired = TRUE)
# Wilcoxon (non-parametric alternative)
wilcox.test(mpg ~ cyl, data = subset(mtcars, cyl %in% c(4,6)))
# interpret results:
# p < 0.05: reject null hypothesis (significant)
# p > 0.05: fail to reject (not enough evidence)
# CI shows plausible range of effect
# check assumptions: normality (shapiro.test), equal variance
# power analysis
library(pwr)
pwr.t.test(d = 0.5, sig.level = 0.05, power = 0.8)
# n = 64 per group needed to detect medium effectANOVA와 다중 비교
ANOVA는 3+ 그룹 간 평균 차이 검사. F-검정은 어떤 차이가 존재하는지 알려줌; 사후 검정(Tukey HSD, 보정과 함께 pairwise.t.test)은 어느 그룹이 다른지 식별. 항상 가정 확인(정규성, 등분산성)하고 위반 시 비모수 대안(Kruskal-Wallis) 사용. 반복 측정 또는 계층적 데이터의 경우, 대상 내 상관을 적절히 처리하는 혼합 효과 모델(lme4::lmer) 사용. 다중 비교 보정(Bonferroni, FDR)은 많은 검정 실행 시 위양성 방지. afex 패키지는 반복 측정 ANOVA 단순화.
# one-way ANOVA: compare means across 3+ groups
fit <- aov(mpg ~ factor(cyl), data = mtcars)
summary(fit)
# Df Sum Sq Mean Sq F value Pr(>F)
# factor(cyl) 2 824.8 412.4 39.7 4.98e-09
# check assumptions
plot(fit) # residuals vs fitted, QQ
shapiro.test(resid(fit)) # normality of residuals
library(car); leveneTest(fit) # equal variance
# post-hoc tests (which groups differ?)
TukeyHSD(fit) # Tukey HSD
pairwise.t.test(mtcars$mpg, mtcars$cyl, p.adjust = "bonferroni")
# two-way ANOVA with interaction
fit2 <- aov(mpg ~ cyl * am, data = mtcars)
summary(fit2)
# Kruskal-Wallis (non-parametric ANOVA)
kruskal.test(mpg ~ factor(cyl), data = mtcars)
# repeated measures ANOVA
# library(afex)
# aov_ez(id = "subject", dv = "score", data = df, within = "condition")
# mixed-effects models (lme4)
library(lme4)
lmer(score ~ treatment + (1|subject), data = df)상관과 회귀 진단
상관은 선형 연관 측정(-1에서 1); cor.test는 추론 추가. 선형 회귀(lm)는 y = β0 + β1*x + ε 피팅. 네 가지 진단 플롯은 가정 위반 드러냄: 비선형성, 비정규 잔차, 이분산성, 영향점. VIF > 5-10은 다중공선성 표시(예측 변수가 너무 상관). Cook's 거리는 영향 관찰 식별. 신뢰 구간(평균 응답) vs 예측 구간(새 관찰)을 적절히 사용. 모델 비교는 anova(중첩) 또는 AIC/BIC(비중첩, 낮을수록 좋음)로. p-값을 신뢰하기 전에 항상 시각화.
# correlation
cor(mtcars$mpg, mtcars$wt) # -0.867
cor.test(mtcars$mpg, mtcars$wt) # with p-value
cor(mtcars[, c("mpg","wt","hp","disp")]) # correlation matrix
library(corrplot); corrplot(cor(mtcars[,1:4]))# visualize
# linear regression
fit <- lm(mpg ~ wt + hp, data = mtcars)
summary(fit)
confint(fit) # CIs for coefficients
# diagnostics
par(mfrow = c(2,2)); plot(fit)
# 1. Residuals vs Fitted: check linearity
# 2. Normal Q-Q: check normality
# 3. Scale-Location: check homoscedasticity
# 4. Residuals vs Leverage: check influential points
# check for multicollinearity
library(car); vif(fit) # variance inflation factor
# influential observations
cooks.distance(fit) |> plot()
influence.measures(fit)
# predictions with intervals
predict(fit, newdata, interval = "confidence") # CI for mean
predict(fit, newdata, interval = "prediction") # PI for new obs
# compare nested models
anova(fit1, fit2) # F-test
AIC(fit1, fit2); BIC(fit1, fit2) # information criteria범주형 데이터: 카이제곱과 Fisher
카이제곱은 범주형 변수 간 독립성 검사; Fisher의 정확 검정은 작은 표본에 더 정확(기대 빈도 < 5). 카이제곱 결과 신뢰 전 기대 빈도 확인. 적합도 검정은 관찰값을 이론적 비율과 비교. McNemar는 대응 명목 데이터(전/후) 검사. 효과 크기(Cramer's V, phi)는 p-값을 넘어 연관 강도 정량화. 모자이크 플롯은 분할표를 직관적으로 시각화. 순서 데이터의 경우 Spearman 상관 또는 추세 검정 고려. vcd(범주형 데이터 시각화) 패키지는 포괄적 도구 제공.
# contingency table
tbl <- table(mtcars$cyl, mtcars$am)
# 0 1
# 4 3 8
# 6 4 3
# 8 12 2
# chi-square test of independence
chisq.test(tbl)
# X-squared = 8.74, df = 2, p-value = 0.0126
# expected counts (should be > 5 for valid chi-square)
chisq.test(tbl)$expected
# Fisher's exact test (small samples)
fisher.test(tbl)
# goodness of fit (one variable vs expected proportions)
chisq.test(c(10, 20, 30), p = c(1/3, 1/3, 1/3))
# McNemar's test (paired nominal data)
mcnemar.test(table(pre, post))
# Cochran-Mantel-Haenszel (stratified)
mantelhaen.test(tbl3d)
# visualize
library(ggplot2); library(ggmosaic)
ggplot(as.data.frame(tbl)) +
geom_mosaic(aes(weight = Freq, x = product(Var1), fill = Var2))
# effect size
library(vcd); assocstats(tbl) # Cramer's V, phibrms로 베이지안 추론
베이지안 추론(brms를 통해, Stan을 감싸는)은 점 추정 대신 완전한 사후 분포 제공. 도메인 지식을 인코딩하기 위해 사전 지정; 약하게 정보적 사전(normal(0, 10))은 편향 없이 정규화. 사후 요약은 신뢰 구간 제공(빈도주의 CI와 달리 직접 확률 진술). pp_check는 관찰 데이터와 시뮬레이션 데이터 비교로 모델 적합 검증. LOO-CV와 WAIC는 교차 검증된 예측 정확도로 모델 비교. 계층 모델은 그룹화된 데이터를 자연스럽게 처리. 베이지안 방법은 작은 표본, 복잡한 모델, 불확실성 정량화가 필요할 때 빛남.
library(brms)
# Bayesian linear regression
fit <- brm(
mpg ~ wt + hp,
data = mtcars,
family = gaussian(),
prior = c(
prior(normal(0, 10), class = "b"), # weakly informative
prior(cauchy(0, 2), class = "sigma")
),
chains = 4, cores = 4, iter = 2000
)
# summary
summary(fit)
plot(fit) # posterior distributions
pp_check(fit) # posterior predictive check
# extract posterior samples
posterior <- as_draws_df(fit)
mean(posterior$b_wt > 0) # P(coefficient > 0)
# predictions
posterior_predict(fit, newdata) |> as.data.frame() -> preds
# model comparison
fit_null <- brm(mpg ~ 1, data = mtcars, ...)
loo(fit, fit_null) # leave-one-out CV
waic(fit, fit_null)
# hierarchical model
fit_h <- brm(
score ~ treatment + (1 + treatment|subject),
data = df, family = gaussian()
)
# Stan code behind the model
stancode(fit)caret으로 머신러닝
데이터 분할과 전처리
적절한 데이터 분할과 전처리는 ML 성공의 80%. createDataPartition은 층화 표집 수행(클래스 균형 보존). trainControl은 재표본 구성(CV, 부트스트랩, 반복 CV). preProcess는 표준화, 변환, PCA, 대체 처리 — 항상 훈련 데이터에만 피팅하고 누출을 피하기 위해 테스트에 적용. nzv는 정보 없는 열 제거. 시계열의 경우 무작위 CV 대신 createTimeSlices 사용. caret의 통합 인터페이스는 같은 전처리가 모든 모델 타입에 작동함을 의미.
library(caret)
# split data
set.seed(42)
idx <- createDataPartition(iris$Species, p = 0.8, list = FALSE)
train <- iris[idx, ]; test <- iris[-idx, ]
# stratified split for classification
# createDataPartition preserves class proportions
# k-fold cross-validation
ctrl <- trainControl(
method = "cv", number = 10,
savePredictions = "final",
classProbs = TRUE, # for AUC
summaryFunction = multiClassSummary
)
# preprocessing pipeline
preproc <- preProcess(train[, -5],
method = c("center","scale","YeoJohnson","nzv"))
train_processed <- predict(preproc, train[, -5])
test_processed <- predict(preproc, test[, -5])
# common preprocessing methods:
# center, scale: standardize
# BoxCox, YeoJohnson: transform to normality
# pca: principal components
# nzv: remove near-zero variance
# knnImpute, bagImpute: impute missing values
# dummy variables for categorical
dummies <- dummyVars(Species ~ ., data = train)
predict(dummies, train)모델 훈련과 튜닝
caret의 train() 함수는 200+ 모델에 통합 인터페이스 제공 — method 문자열만 변경. tuneLength는 자동으로 튜닝 그리드 생성; tuneGrid는 완전한 제어 제공. resamples()은 재표본 성능으로 여러 모델 비교(단일 테스트 셋 평가보다 더 정직). 공정한 비교를 위해 모델 전체에 같은 trControl 항상 사용. resamples의 dotplot은 성능 겹침 표시 — CI가 겹치면 모델이 유의하게 다르지 않음. 최선의 한 표준 오차 내 가장 단순한 모델 선택('one-SE 규칙').
library(caret)
# train a random forest
ctrl <- trainControl(method = "cv", number = 5)
rf_fit <- train(
Species ~ ., data = train,
method = "rf",
trControl = ctrl,
tuneGrid = expand.grid(mtry = 1:4),
tuneLength = 5 # auto-tune grid
)
print(rf_fit) # shows accuracy by tuning param
plot(rf_fit) # tuning curve
# try multiple models
models <- c("rf","gbm","svmRadial","knn","rpart")
fits <- lapply(models, function(m) {
train(Species ~ ., data = train, method = m, trControl = ctrl)
})
names(fits) <- models
# compare models
resamps <- resamples(fits)
summary(resamps)
dotplot(resamps, metric = "Accuracy")
# custom tuning grid
grid <- expand.grid(
mtry = c(2, 4, 8),
splitrule = c("gini","extratrees"),
min.node.size = c(1, 5, 10)
)
fit <- train(Species ~ ., data = train, method = "ranger",
trControl = ctrl, tuneGrid = grid)분류 지표와 혼동 행렬
정확도만으로는 불균형 데이터에 오해의 소지가 있습니다. confusionMatrix는 클래스별 민감도(재현율), 특이도, 정밀도, F1 제공. 이진 문제의 경우 ROC-AUC는 판별력 측정; 양성 클래스가 희귀할 때 PR 곡선이 더 좋음. 다중 클래스의 경우 매크로/마이크로 평균 지표 또는 log-loss 사용. 항상 보류된 테스트 셋(또는 편향 없는 추정을 위해 중첩 CV)에서 평가. caret에서 올바른 지표 최적화를 위해 trainControl의 summaryFunction 설정(예: 확률적 예측을 위해 mnLogLoss). 점 추정만이 아닌 성능에 신뢰 구간 보고.
# predictions
pred <- predict(rf_fit, test)
prob <- predict(rf_fit, test, type = "prob")
# confusion matrix
cm <- confusionMatrix(pred, test$Species)
print(cm)
# Reference
# Prediction setosa versicolor virginica
# setosa 10 0 0
# versicolor 0 10 1
# virginica 0 0 9
# Overall Accuracy: 0.9667
# byClass: Sensitivity, Specificity, etc.
# two-class metrics
cm$byClass[, c("Sensitivity","Specificity","Precision","Recall","F1")]
# ROC and AUC
library(pROC)
roc_curve <- roc(test$Species == "versicolor", prob$versicolor)
auc(roc_curve)
plot(roc_curve)
# multi-class AUC
library(pRoc)
multiclass.roc(test$Species, prob)
# precision-recall curve (better for imbalanced data)
library(PRROC)
pr <- pr.curve(scores.class0 = prob$versicolor,
weights.class0 = test$Species == "versicolor",
curve = TRUE)
plot(pr)
# custom metrics in trainControl
twoClassSummary # built-in for 2-class
mnLogLoss # multi-class log loss특성 선택과 해석
특성 선택은 모델 성능과 해석 가능성 향상. RFE(재귀적 특성 제거)는 모델을 감싸고 가장 중요하지 않은 특성을 반복적으로 제거. varImp는 caret 훈련 모델(랜덤 포레스트, gbm 등)에서 중요도 추출. 필터 방법(findCorrelation, findLinearCombos)은 훈련 전 중복 특성 제거. 블랙박스 해석의 경우 SHAP 값(fastshap, shapviz)이 예측을 특성에 귀속. 선택 편향을 피하기 위해 교차 검증 내에서 항상 특성 선택 수행. 더 적은 특성을 가진 단순한 모델이 종종 더 잘 일반화.
library(caret)
# recursive feature elimination (RFE)
ctrl <- rfeControl(functions = rfFuncs, method = "cv", number = 10)
rfe_fit <- rfe(train[,-5], train$Species,
sizes = c(1:4), rfeControl = ctrl)
print(rfe_fit) # optimal subset
predictors(rfe_fit) # selected features
plot(rfe_fit)
# variable importance from trained model
varImp(rf_fit) # caret extracts importance
plot(varImp(rf_fit))
# filter methods (before training)
# remove highly correlated features
cor_matrix <- cor(train[,-5])
high_cor <- findCorrelation(cor_matrix, cutoff = 0.9)
train_filtered <- train[,-high_cor]
# remove linear dependencies
findLinearCombos(train[,-5])
# genetic algorithm / simulated annealing selection
# gafsFit <- gafs(x, y, iters = 20)
# safsFit <- safs(x, y, iters = 20)
# SHAP values (model-agnostic interpretation)
# library(fastshap)
# explain(rf_fit, X = test, nsim = 100)앙상블과 스태킹
앙상블은 어떤 단일 모델보다 더 나은 성능을 위해 여러 모델 결합. caretEnsemble은 동일한 재표본으로 모델 훈련(공정한 스태킹에 필요). caretStack은 기본 예측 위에 메타 모델 훈련 — 메타 모델은 각 기본 모델을 언제 신뢰할지 학습. 단순 평균은 비슷한 정확도 모델에 놀랍게 잘 작동. 가중 앙상블은 더 나은 모델 강조. 배깅(treebag)은 부트스트랩 모델 평균으로 분산 감소. 기본 모델의 다양성이 개별 정확도보다 더 중요 — 다른 오류를 만드는 모델 결합.
library(caretEnsemble)
# train multiple models with same resampling
ctrl <- trainControl(method = "cv", number = 5,
savePredictions = "final",
classProbs = TRUE)
models <- caretList(
Species ~ ., data = train,
trControl = ctrl,
methodList = c("rf","gbm","svmRadial","knn")
)
# simple ensemble (average predictions)
ens <- caretEnsemble(models)
summary(ens)
plot(ens)
# stack: train a meta-model on base predictions
stack <- caretStack(models, method = "glm",
metric = "Accuracy", trControl = ctrl)
print(stack)
# predict with ensemble
pred_ens <- predict(ens, test)
pred_stack <- predict(stack, test)
# weighted ensemble (custom weights)
weights <- c(0.4, 0.3, 0.2, 0.1)
probs <- lapply(models, function(m) predict(m, test, type = "prob"))
final_prob <- Reduce('+', Map(function(p, w) p * w, probs, weights))
# bagging (bootstrap aggregating)
bag_fit <- train(Species ~ ., data = train, method = "treebag",
trControl = ctrl)Apply 계열과 성능
apply, lapply, sapply, vapply
apply 계열은 base R의 함수형 프로그래밍 툴킷. apply는 배열에 작동(행은 margin 1, 열은 2 사용)하지만 내장 rowSums/colSums가 더 빠름. lapply는 항상 리스트 반환; sapply는 벡터로 단순화 시도(편리하지만 타입 불안정). vapply는 안전한 버전 — 출력 템플릿 지정, 조용히 강제 변환 대신 불일치 시 오류. 프로덕션 코드에서 vapply 사용, 대화형에서 sapply. 시뮬레이션에 replicate 편리. mapply(또는 Map)는 여러 인자를 병렬로 반복. 현대 코드의 경우 일관성을 위해 purrr의 map 계열 선호.
# apply: over array margins (rows or columns)
m <- matrix(1:12, 3, 4)
apply(m, 1, sum) # row sums: 22 26 30
apply(m, 2, mean) # column means
apply(m, c(1,2), sqrt) # element-wise (silly but valid)
# built-in row/col functions are faster
rowSums(m); rowMeans(m)
colSums(m); colMeans(m)
# lapply: list in, list out
lapply(list(a=1:3, b=4:6), mean) # list(a=2, b=5)
lapply(mtcars, class) # class of each column
# sapply: list in, vector out (simplifies)
sapply(mtcars, mean) # named numeric vector
sapply(mtcars, is.numeric) # logical vector
# vapply: type-safe sapply (specify output template)
vapply(mtcars, mean, numeric(1)) # always numeric(1)
vapply(mtcars, class, character(1)) # always character(1)
# replicate: repeat expression n times
replicate(5, rnorm(3)) # 3x5 matrix
replicate(100, mean(rexp(30))) # 100 sample means
# mapply: multivariate map
mapply(rep, 1:3, 3:1) # list(1,1,1, 2,2, 3)
mapply(function(a,b) a+b, 1:3, 4:6) # 5 7 9벡터화와 속도
벡터화는 #1 R 성능 최적화. R의 산술, 비교, 수학 함수는 최적화된 C 코드를 통해 전체 벡터에 작동 — R의 루프는 인터프리터되어 느림. ifelse는 벡터화되지만 여전히 오버헤드; 직접 논리 인덱싱(x * (x > 5))이 가장 빠름. 데이터 프레임에 apply 피함(행렬로 강제 변환); 대신 벡터화된 열 연산 사용. 결과 벡터 항상 미리 할당 — c()로 키우는 것은 O(n²). 진정한 핫 루프의 경우 Rcpp로 C++ 인라인 작성. 개선 검증을 위해 microbenchmark로 마이크로벤치마크; system.time은 너무 조재함.
# BAD: element-wise loop
x <- 1:1e6
y <- numeric(length(x))
for (i in seq_along(x)) y[i] <- x[i]^2
# GOOD: vectorized
y <- x^2 # ~100x faster
# ifelse vs vectorized
# BAD
y <- numeric(length(x))
for (i in seq_along(x)) {
y[i] <- if (x[i] > 5) x[i] else 0
}
# GOOD
y <- ifelse(x > 5, x, 0)
# BEST (fastest)
y <- x * (x > 5)
# row-wise operations (avoid if possible)
df <- data.frame(a = 1:1000, b = 1001:2000)
# BAD
df$sum <- apply(df, 1, sum)
# GOOD
df$sum <- df$a + df$b
# preallocate!
n <- 1000
result <- numeric(n) # not result <- c()
for (i in 1:n) result[i] <- i^2
# use Rcpp for hot loops
# library(Rcpp)
# cppFunction('double sumc(NumericVector x) { return sum(x); }')
# benchmark
library(microbenchmark)
microbenchmark(
loop = {y <- numeric(length(x)); for(i in seq_along(x)) y[i] <- x[i]^2},
vectorized = x^2,
times = 10
)메모리와 data.table
data.table은 큰 데이터(1M+ 행)에 data.frame/dplyr보다 극적으로 빠르고 메모리 효율적. dt[i, j, by] 문법은 필터링, 선택, 그룹화를 하나의 표현으로 결합. 참조 의미론(:=)은 복사 없이 제자리 수정 — 메모리에 결정적. setkey는 이진 검색 조회와 빠른 병합을 가능하게 하는 인덱스 생성. fread/fwrite는 read.csv/write.csv보다 5-10배 빠름. 메모리에 맞는 데이터의 경우 data.table은 심지어 Spark보다도 빠름. 단점은 더 가파른 학습 곡선과 dplyr에 비해 덜 읽기 쉬운 문법.
# data.table: faster, memory-efficient alternative to data.frame
library(data.table)
dt <- data.table(mtcars)
# syntax: dt[i, j, by]
dt[mpg > 20, .(mean_hp = mean(hp)), by = .(cyl, gear)]
# cyl gear mean_hp
# 1: 4 4 76.0
# 2: 4 5 102.0
# reference semantics (modify in place, no copy)
dt[, mpg_dbl := mpg * 2] # add column in place
dt[1:5, mpg := 0] # modify subset in place
dt[, c("a","b") := .(mpg*2, hp/10)] # multiple columns
# setkey for fast lookups and joins
setkey(dt, cyl)
dt[.(4)] # all rows where cyl == 4 (binary search)
dt[.(4), mean(mpg)] # fast aggregation
# joins
dt1 <- data.table(id = 1:3, x = letters[1:3])
dt2 <- data.table(id = 2:4, y = LETTERS[2:4])
setkey(dt1, id); setkey(dt2, id)
dt1[dt2] # right join
dt2[dt1] # left join
merge(dt1, dt2, by = "id") # inner join
# fread/fwrite: fast I/O
big <- fread("huge.csv") # ~10x faster than read.csv
fwrite(big, "out.csv")병렬 컴퓨팅
R은 기본적으로 단일 스레드이지만, 병렬화는 간단. parallel 패키지(내장)는 mclapply(fork, Linux/Mac 전용)와 parLapply(클러스터, 모든 플랫폼) 제공. foreach + doParallel은 인기 있는 대안. future 생태계(furrr와 함께)는 현대적이고 통합 — plan() 변경으로 백엔드 전환. caret의 경우 allowParallel = TRUE 설정하고 백엔드 등록. 워커에 필요한 변수와 패키지 항상 내보내기. 병렬화는 오버헤드가 있음 — 각 작업이 상당할 때만 도움(>100ms). 가속 검증을 위해 벤치마크; Amdahl의 법칙이 이득 제한.
# parallel package (built-in)
library(parallel)
ncores <- detectCores() - 1
# parallel lapply
cl <- makeCluster(ncores)
results <- parLapply(cl, 1:100, function(i) slow_function(i))
stopCluster(cl)
# foreach with doParallel
library(foreach); library(doParallel)
registerDoParallel(ncores)
results <- foreach(i = 1:100, .combine = "c") %dopar% {
slow_function(i)
}
stopImplicitCluster()
# future ecosystem (modern, flexible)
library(future); library(furrr)
plan(multisession, workers = ncores) # or multicore (Linux/Mac)
results <- future_map(1:100, slow_function)
# parallel caret
library(caret)
ctrl <- trainControl(method = "cv", number = 10, allowParallel = TRUE)
fit <- train(y ~ ., data = train, method = "rf", trControl = ctrl)
# benchmark
library(microbenchmark)
microbenchmark(
serial = lapply(1:100, slow_function),
parallel = parLapply(makeCluster(4), 1:100, slow_function),
times = 5
)
# export variables to workers
clusterExport(cl, c("my_data", "my_function"))
clusterEvalQ(cl, library(dplyr))프로파일링과 최적화 워크플로
최적화 전에 프로파일 — 병목에 대한 직관은 보통 틀림. profvis는 줄당 시간과 호출당 시간을 보여주는 대화형 플레임 그래프 제공. Rprof는 base R 동등물. Rprofmem은 할당 추적. object.size는 메모리 측정; gc()는 가비지 수집 강제하고 사용량 보고. 최적화 계층: (1) 벡터화, (2) 미리 할당, (3) data.table으로 전환, (4) 환원 불가능한 루프를 위해 Rcpp, (5) 병렬화. memoise는 함수 결과 캐시 — 같은 인자로 반복 호출되는 비용이 많이 드는 순수 함수에 좋음. 개선 확인을 위해 항상 전후 측정.
# profile to find bottlenecks
library(profvis)
profvis({
result <- my_analysis(big_data)
})
# opens interactive flame graph
# Rprof (base R)
Rprof("profile.out")
my_analysis(big_data)
Rprof(NULL)
summaryRprof("profile.out")
# memory profiling
Rprofmem("mem.out")
my_analysis(big_data)
Rprofmem(NULL)
# object sizes
object.size(my_data) # bytes
format(object.size(my_data), "MB")
# find memory hogs
sort(sapply(ls(), function(x) object.size(get(x))))
# garbage collection
gc() # force collection, show memory
gcinfo(TRUE) # report on auto GC
# common optimizations:
# 1. Vectorize (avoid loops)
# 2. Preallocate
# 3. Use data.table instead of data.frame
# 4. Use Rcpp for hot loops
# 5. Avoid growing objects (c(), rbind())
# 6. Use appropriate data types (integer vs double)
# 7. Cache expensive computations (memoise)
library(memoise)
slow_cached <- memoise(slow_function)dplyr 심층
핵심 동사
dplyr의 5개 핵심 동사: filter(조건으로 행), select(열), mutate(새 열), arrange(정렬), summarize(집계). %>로 연결. group_by + summarize는 집계를 위한 주력. na.rm = TRUE는 필수 — 그렇지 않으면 데이터의 어떤 NA가 요약을 NA로 만듦.
library(dplyr)
starwars %>%
filter(species == "Human", height > 170) %>%
select(name, height, mass, homeworld) %>%
mutate(bmi = mass / (height/100)^2) %>%
arrange(desc(height)) %>%
slice_head(n = 5)
# group_by + summarize
starwars %>%
group_by(species) %>%
summarize(
n = n(),
avg_height = mean(height, na.rm = TRUE),
avg_mass = mean(mass, na.rm = TRUE)
) %>%
arrange(desc(n)) %>%
filter(n >= 2)조인
변이 조인은 열 결합; 필터 조인은 행 서브셋. left_join은 가장 흔함 — x의 모든 행 유지, 비일치는 NA로 채움. 항상 우측 테이블의 중복 키 확인(행 곱셈 유발). semi_join/anti_join은 다른 테이블의 열을 가져오지 않고 다른 테이블 기반 필터링에 좋음.
library(dplyr)
# mutating joins (add columns from y to x)
left_join(x, y, by = "id") # all rows in x
right_join(x, y, by = "id") # all rows in y
inner_join(x, y, by = "id") # only matching rows
full_join(x, y, by = "id") # all rows from both
# different column names
left_join(x, y, by = c("id" = "user_id"))
# multiple keys
left_join(x, y, by = c("first", "last"))
# filtering joins (filter x, no columns added)
semi_join(x, y, by = "id") # rows in x that match y
anti_join(x, y, by = "id") # rows in x that DON'T match y
# check for duplicates
x %>% count(id) %>% filter(n > 1)윈도우 함수
윈도우 함수는 현재 행과 관련된 행에 걸쳐 값 계산. lag/lead는 이전/다음 행 접근 — 시계열에 필수. cumsum/cummean은 누적 집계. slice_max/slice_min은 그룹당 top-n의 단축키. 그룹 내에서 계산하기 위해 항상 먼저 group_by.
library(dplyr)
# row numbering and ranking
df %>% group_by(group) %>%
mutate(
row_num = row_number(),
rank = rank(value),
dense_rank = dense_rank(value),
min_rank = min_rank(value)
)
# offsets
df %>% group_by(group) %>%
mutate(
prev = lag(value),
next = lead(value, 2),
diff = value - lag(value)
)
# cumulative
df %>% group_by(group) %>%
mutate(
cum_sum = cumsum(value),
cum_mean = cummean(value),
cum_max = cummax(value),
cum_min = cummin(value)
)
# top n per group
df %>% group_by(group) %>%
slice_max(value, n = 3) # top 3
df %>% group_by(group) %>%
slice_min(value, n = 2)across와 다중 열
across(dplyr 1.0+)는 기존 _at, _if, _all 접미사 대체. where(is.numeric)으로 술자로 열 선택. .x 대명사는 람다(~) 내부의 현재 열 참조. rename_with는 함수로 열 이름 변경. across는 다중 열에 작동하는 현대적이고 일관된 방법.
library(dplyr)
# apply function to multiple columns
starwars %>%
mutate(across(where(is.numeric), ~ replace_na(.x, 0)))
# summarize multiple columns
starwars %>%
summarize(across(where(is.numeric), mean, na.rm = TRUE))
# rename multiple columns
starwars %>%
rename_with(tolower, everything())
# transform specific columns
df %>%
mutate(across(c(height, mass), ~ .x * 0.453592)) # lb to kg
# multiple functions
df %>%
summarize(across(value, list(mean = mean, sd = sd, n = ~ n()), na.rm = TRUE))
# conditional transformation
df %>%
mutate(across(where(is.character), tolower))Summarize 패턴
summarize는 그룹을 단일 값으로 축소. n()는 행 계산; n_distinct()는 고유 값 계산. 통계 함수에 항상 na.rm = TRUE 전달 또는 NA 전파. across + list로 한 번에 여러 통계 계산. count()는 group_by + summarize(n = n()) + ungroup의 단축키.
library(dplyr)
# basic
df %>%
group_by(category) %>%
summarize(
count = n(),
distinct = n_distinct(id),
total = sum(value, na.rm = TRUE),
avg = mean(value, na.rm = TRUE),
median = median(value, na.rm = TRUE),
sd = sd(value, na.rm = TRUE),
min = min(value, na.rm = TRUE),
max = max(value, na.rm = TRUE),
first = first(value),
last = last(value),
q25 = quantile(value, 0.25, na.rm = TRUE)
)
# multiple summary stats at once
df %>%
group_by(category) %>%
summarize(
across(value, list(mean = mean, sd = sd, n = ~ n()), na.rm = TRUE)
)
# count and proportions
df %>%
count(category) %>%
mutate(pct = n / sum(n))ggplot2 고급
레이어와 미학
ggplot는 +로 연결된 레이어로 플롯을 구축. aes()는 데이터 열을 시각적 속성에 매핑. geom_*는 기하를 정의; scale_*는 축/색상 제어; labs는 모든 것에 라벨; theme_*는 비데이터 요소 스타일. facet_wrap은 하나의 변수로 분할; facet_grid는 두 변수의 2D 그리드를 만듭니다.
library(ggplot2)
ggplot(mpg, aes(displ, hwy, color = class)) +
geom_point(size = 2, alpha = 0.7) +
geom_smooth(method = "lm", se = TRUE) +
scale_color_brewer(palette = "Set2") +
labs(
title = "Fuel efficiency by engine size",
subtitle = "Source: EPA mpg dataset",
x = "Engine displacement (L)",
y = "Highway MPG",
color = "Vehicle class"
) +
theme_minimal(base_size = 12) +
theme(legend.position = "bottom")
# facets
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_wrap(~ class, ncol = 4)
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_grid(drv ~ cyl)스케일과 좌표
scale_* 함수는 데이터를 시각적 속성에 매핑하는 방법 제어. scale_x_log10은 로그 변환; scale_color_viridis_c는 지각적으로 균일한 컬러맵 제공. coord_cartesian은 데이터 삭제 없이 확대(xlim과 다름). coord_flip은 축 교환. coord_polar는 막대를 파이 슬라이스로 변환. scale_x_date는 날짜 축 형식화.
ggplot(mpg, aes(displ, hwy, color = cty)) +
geom_point() +
scale_x_log10() +
scale_y_continuous(limits = c(10, 50), breaks = seq(10, 50, 5)) +
scale_color_viridis_c(option = "plasma") +
coord_cartesian(xlim = c(1, 7)) # zoom without removing data
# coord flip
ggplot(mpg, aes(class, hwy)) +
geom_boxplot() +
coord_flip()
# coord polar (pie chart from bar)
ggplot(mtcars, aes(factor(1), fill = factor(cyl))) +
geom_bar(width = 1) +
coord_polar(theta = "y")
# date axis
ggplot(economics, aes(date, unemploy)) +
geom_line() +
scale_x_date(date_labels = "%Y", date_breaks = "5 years")테마와 사용자 정의
theme()는 모든 비데이터 요소 제어. element_text/rect/line/blank는 빌딩 블록. 일반 조정: x축 라벨 회전(angle, hjust), 굵은 제목, 보조 그리드 숨기기(panel.grid.minor = element_blank()). ggsave는 PNG/PDF/SVG로 내보냄 — 래스터 형식의 경우 인치와 dpi로 크기 지정.
library(ggplot2)
p <- ggplot(mpg, aes(class, hwy)) + geom_boxplot()
# built-in themes
p + theme_minimal()
p + theme_classic()
p + theme_bw()
# custom theme
p + theme(
panel.background = element_rect(fill = "white"),
panel.grid.major = element_line(color = "gray90"),
panel.grid.minor = element_blank(),
axis.text.x = element_text(angle = 45, hjust = 1, size = 10),
axis.title = element_text(face = "bold"),
plot.title = element_text(size = 16, hjust = 0.5),
plot.subtitle = element_text(color = "gray40"),
legend.position = "right",
legend.key = element_blank(),
strip.background = element_rect(fill = "lightblue"),
strip.text = element_text(face = "bold")
)
# save
ggsave("plot.png", width = 8, height = 6, dpi = 300)
ggsave("plot.pdf", device = cairo_pdf)통계와 평활
stat_summary는 사용자 정의 요약 계산(mean, median, mean_se, mean_cl_normal). geom_smooth는 추세선 추가 — 선형은 method='lm', 국소 회귀는 'loess', 일반화 가법은 'gam'. geom_density/geom_density_2d는 분포 표시. geom_violin은 박스플롯 옆에 전체 분포 형태 표시.
library(ggplot2)
# stat_summary for custom summaries
ggplot(mtcars, aes(factor(cyl), mpg)) +
stat_summary(fun = "mean", geom = "point", size = 3) +
stat_summary(fun.data = "mean_se", geom = "errorbar")
# smooth
ggplot(mtcars, aes(wt, mpg)) +
geom_point() +
geom_smooth(method = "lm", formula = y ~ x, se = TRUE) +
geom_smooth(method = "loess", se = FALSE, color = "red")
# density
ggplot(iris, aes(Sepal.Length, fill = Species)) +
geom_density(alpha = 0.5)
# 2d density
ggplot(diamonds, aes(carat, price)) +
geom_density_2d() +
scale_x_log10() + scale_y_log10()
# histograms with binning
ggplot(diamonds, aes(price)) +
geom_histogram(bins = 50, fill = "steelblue") +
scale_x_log10()
# boxplot and violin
ggplot(iris, aes(Species, Sepal.Length)) +
geom_boxplot() +
geom_violin(alpha = 0.3, fill = "orange")확장과 패치워크
patchwork는 + (나란히), / (쌓임), plot_layout으로 여러 플롯 결합. plot_annotation은 전체 제목과 태그(A, B, C...) 추가. ggplot2 확장 생태계는 거대: 라벨은 ggrepel, 능선 플롯은 ggridges, 애니메이션은 gganimate, 대화형은 ggiraph, 지도는 geom_sf.
library(ggplot2)
library(patchwork)
p1 <- ggplot(mtcars, aes(wt, mpg)) + geom_point()
p2 <- ggplot(mtcars, aes(factor(cyl))) + geom_bar()
p3 <- ggplot(mtcars, aes(mpg)) + geom_histogram(bins = 10)
p4 <- ggplot(mtcars, aes(factor(cyl), mpg)) + geom_boxplot()
# combine plots
p1 + p2 # side by side
p1 / p2 # stacked
(p1 + p2) / p3 # grouped
p1 + p2 + p3 + plot_layout(nrow = 2, byrow = FALSE)
# annotations
p1 + p2 + plot_annotation(
title = "MT cars analysis",
tag_levels = "A"
)
# other extensions:
# ggrepel: non-overlapping labels
# ggridges: joy plots
# gganimate: animated plots
# ggiraph: interactive
# ggplot2::geom_sf: maps
library(ggrepel)
ggplot(mtcars, aes(wt, mpg, label = rownames(mtcars))) +
geom_point() +
geom_text_repel()tidyr 데이터 랭글링
Pivot longer와 wider
pivot_longer/pivot_wider(기존 gather/spread 대체)는 데이터 재구조화. 긴 형식은 ggplot과 dplyr 집계에 최선; 넓은 형식은 인간이 읽기에 더 좋음. .value와 names_pattern은 열 이름 구조에 기반하여 여러 열로 분할 가능. cols, names_to, values_to를 항상 명시적으로 지정.
library(tidyr)
# wide to long
# id q1 q2 q3
# A 10 20 30
wide_data %>%
pivot_longer(
cols = starts_with("q"),
names_to = "quarter",
values_to = "score"
)
# id quarter score
# A q1 10
# A q2 20
# A q3 30
# long to wide
long_data %>%
pivot_wider(
names_from = quarter,
values_from = score
)
# multiple value columns
df %>%
pivot_longer(
cols = c(starts_with("q"), starts_with("r")),
names_to = c(".value", "quarter"),
names_pattern = "([a-z])([0-9])"
)Separate와 unite
separate는 하나의 열을 여러 개로 분할; unite는 여러 개를 하나로 결합. separate_rows는 여러 행으로 분할(태그 리스트에 유용). extract는 정규식 캡처 그룹 사용. 모두 sep 인자 사용(기본값은 영숫자가 아닌 것). convert = TRUE로 separate에서 타입 자동 변환.
library(tidyr)
# split a column
df <- tibble(x = c("a_1", "b_2", "c_3"))
df %>% separate(x, c("letter", "number"), sep = "_")
# letter number
# a 1
# b 2
# split into rows
df %>% separate_rows(x, sep = "_")
# x
# a
# 1
# b
# 2
# unite columns
df %>%
separate(x, c("letter", "number")) %>%
unite("combined", letter, number, sep = "-")
# combined
# a-1
# b-2
# extract with regex
df %>% extract(x, c("letter", "number"), "([a-z])_([0-9])")결측값
replace_na는 NA를 상수로 채움; fill은 값을 전파(시계열에 좋음); drop_na는 불완전한 행 제거; coalesce는 열에 걸쳐 첫 번째 비-NA 선택. complete는 지정된 열의 모든 조합으로 확장(카티션 곱처럼) — 요약에서 누락된 그룹이 나타나도록 보장하는 데 유용.
library(tidyr)
# replace NA
df %>% replace_na(list(value = 0, name = "unknown"))
# fill NA with previous/next value
df %>% fill(value, .direction = "down") # forward fill
df %>% fill(value, .direction = "up") # backward fill
df %>% fill(value, .direction = "downup") # down then up
# drop rows with NA
df %>% drop_na()
df %>% drop_na(value) # only specific columns
# detect NA
df %>% filter(!is.na(value))
sum(is.na(df$value))
df %>% mutate_all(~ sum(is.na(.))) # NA count per column
# coalesce: first non-NA
df %>% mutate(value = coalesce(value, fallback, 0))
# complete: expand combinations
df %>% complete(group, year) # all group-year combos, NA filled
df %>% complete(group, year, fill = list(value = 0))네스팅과 리스트 열
nest는 그룹화된 행을 리스트 열로 포장 — purrr과 분할-적용-결합의 기반. unnest는 역방향. unnest_wider는 리스트 요소를 열로 펼침; unnest_longer는 행으로 펼침. 리스트 열은 tibble 내에 임의의 객체(모델, 데이터 프레임, 벡터)를 보유 가능.
library(tidyr)
library(dplyr)
# nest: pack rows into list columns
nested <- mtcars %>%
nest(data = -cyl)
# cyl data
# 4 <tibble 11x10>
# 6 <tibble 7x10>
# 8 <tibble 14x10>
# operate on each group
nested %>%
mutate(model = map(data, ~ lm(mpg ~ wt, data = .x)))
# unnest
df %>%
unnest(data)
# unnest specific column
df %>%
unnest_wider(data) # list-col of named lists -> cols
df %>%
unnest_longer(data) # list-col of vectors -> rows
# chop (similar to nest)
df %>% chop(value)
# pack (opposite of unnest)
df %>% pack(x = c(a, b))Tibble과 데이터 읽기
Tibble은 개선된 데이터 프레임: 문자열을 팩터로 변환 안 함, 행 이름 뭉개기 없음, 더 나은 출력. read_csv는 read.csv보다 훨씬 빠르고 tibble 반환. 놀람을 피하기 위해 col_types 지정(예: ID가 숫자로 읽힘). na 인자로 여러 문자열을 NA로 처리. 표 형식 데이터에는 항상 base보다 readr 사용.
library(tibble)
library(readr)
# create tibble
tibble(
x = 1:5,
y = c("a", "b", "c", "d", "e"),
z = runif(5)
)
# tribble: row-wise construction
tribble(
~name, ~age, ~score,
"Alice", 30, 90,
"Bob", 25, 80
)
# read csv
df <- read_csv("data.csv", col_names = TRUE, col_types = "cdd")
df <- read_csv("data.csv", na = c("", "NA", "N/A"))
df <- read_tsv("data.tsv")
df <- read_delim("data.txt", delim = "|")
# write
write_csv(df, "out.csv")
write_tsv(df, "out.tsv")
# read from clipboard (Excel)
df <- read_clipboard()
# column types
# c = character, d = double, i = integer, l = logical, f = factor, D = date통계 검정
t-검정
t-검정은 평균 비교. 단일 표본(값 대비), 두 표본(그룹 간), 대응(대상 내). 기본값은 Welch's(불등분산) — 보통 원하는 것. 항상 가정 확인: 정규성(Shapiro)과 등분산(var.test). p-값만이 아닌 효과 크기(Cohen's d) 보고.
# one-sample t-test
t.test(x, mu = 5)
# H0: mean of x equals 5
# two-sample t-test
t.test(x, y)
t.test(x, y, var.equal = TRUE) # Student's (assume equal var)
t.test(x, y, alternative = "greater")
# paired t-test
t.test(before, after, paired = TRUE)
# output interpretation
result <- t.test(x, y)
result$p.value # < 0.05 -> reject H0
result$conf.int # 95% CI of difference
result$statistic # t value
# check assumptions
shapiro.test(x) # normality
var.test(x, y) # equal variances
# effect size (effsize package)
library(effsize)
cohen.d(x, y)ANOVA
ANOVA는 3+ 그룹 간 차이 검사. 상호작용이 있는 요인 설계에는 * 사용. 유의한 ANOVA 후 항상 사후 검정(TukeyHSD)으로 어느 쌍이 다른지 찾기. 분산의 동질성 확인(Levene). 비정규 데이터의 경우 Kruskal-Wallis 사용. 반복 측정의 경우 lme4의 lmer 사용.
# one-way ANOVA
result <- aov(yield ~ fertilizer, data = df)
summary(result)
# two-way ANOVA with interaction
result <- aov(yield ~ fertilizer + density + fertilizer:density, data = df)
result <- aov(yield ~ fertilizer * density, data = df) # shorthand
# Tukey post-hoc
TukeyHSD(result)
# check assumptions
plot(result) # diagnostic plots
library(car)
leveneTest(yield ~ fertilizer, data = df) # equal variances
# Kruskal-Wallis (non-parametric alternative)
kruskal.test(yield ~ fertilizer, data = df)
# repeated measures
result <- aov(score ~ time + Error(subject/time), data = df)
# mixed effects (lme4)
library(lme4)
model <- lmer(score ~ time + (1 | subject), data = df)
anova(model)카이제곱과 범주형
카이제곱은 두 범주형 변수가 독립인지 검사. 각 셀의 기대 빈도는 >= 5여야 함; 그렇지 않으면 Fisher의 정확 검정 사용. McNemar는 대응 이진 데이터(전/후)용. Pearson 잔차는 어느 셀이 기대에서 가장 벗어나는지 표시. assocstats는 효과 크기 Cramer's V 제공.
# chi-square test of independence
tbl <- table(df$gender, df$vote)
chisq.test(tbl)
# goodness of fit
chisq.test(table(df$color), p = c(0.25, 0.25, 0.25, 0.25))
# Fisher's exact (small samples)
fisher.test(tbl)
# McNemar (paired binary)
mcnemar.test(tbl)
# expected frequencies
ct <- chisq.test(tbl)
ct$expected
ct$observed
ct$residuals # Pearson residuals
# association measures
library(vcd)
assocstats(tbl)
# visualize
library(ggplot2)
ggplot(df, aes(gender, fill = vote)) +
geom_bar(position = "fill")상관
Pearson은 선형 상관 측정; Spearman/Kendall은 단조(순위 기반) 측정 — 이상치와 비선형에 강건. cor.test는 p-값과 CI 제공. corrplot는 행렬 시각화; ggpairs는 산점도와 상관 표시. 전체 행 삭제 없이 결측 데이터 처리를 위해 use='pairwise.complete.obs' 사용.
# Pearson (linear, normal)
cor(x, y, method = "pearson")
cor.test(x, y, method = "pearson")
# Spearman (rank, non-parametric)
cor(x, y, method = "spearman")
# Kendall (rank, smaller samples)
cor(x, y, method = "kendall")
# correlation matrix
cor(mtcars[, c("mpg", "wt", "hp", "disp")])
# with p-values
library(psych)
corr.test(mtcars[, 1:6])
# visualize
library(corrplot)
M <- cor(mtcars)
corrplot(M, method = "circle", type = "lower", order = "hclust")
# GGally for scatterplot matrix
library(GGally)
ggpairs(mtcars[, c("mpg", "wt", "hp", "cyl")])
# handle missing
cor(df, use = "complete.obs") # listwise deletion
cor(df, use = "pairwise.complete.obs")비모수 검정
비모수 검정은 정규성 가정 안 함 — 표본이 작거나, 데이터가 치우쳐 있거나, 순서 데이터가 있을 때 사용. Mann-Whitney/Wilcoxon은 t-검정의 순위 기반 대응물. 순열 검정과 부트스트랩은 컴퓨터 집약적이지만 최소 가정. 항상 p-값과 함께 효과 크기 보고.
# Mann-Whitney U (alternative to two-sample t)
wilcox.test(x, y)
wilcox.test(x, y, paired = FALSE)
# Wilcoxon signed-rank (alternative to paired t)
wilcox.test(before, after, paired = TRUE)
# Kruskal-Wallis (alternative to one-way ANOVA)
kruskal.test(y ~ group, data = df)
# Friedman (alternative to repeated measures ANOVA)
friedman.test(y ~ group | subject, data = df)
# permutation test
library(coin)
oneway_test(y ~ group, data = df, distribution = approximate(nresample = 9999))
# bootstrap CI
library(boot)
boot_mean <- function(data, idx) mean(data[idx])
b <- boot(x, boot_mean, R = 10000)
boot.ci(b, type = "perc")
# sign test
library(BSDA)
SIGN.test(x, md = 5) # median different from 5?회귀 분석
선형 회귀
lm은 선형 모델 피팅. summary는 계수, 표준 오차, t-값, p-값, R², F-통계량 표시. 항상 진단 확인: 선형성/등분산성을 위한 잔차 플롯, 다중공선성을 위한 VIF(>5는 우려). I()는 공식에서 산술 보호; poly(x, 2)는 직교 다항식 제공.
# simple linear
model <- lm(mpg ~ wt, data = mtcars)
summary(model)
coef(model)
confint(model)
fitted(model)
residuals(model)
predict(model, newdata = new_df, interval = "confidence")
# multiple regression
model <- lm(mpg ~ wt + hp + cyl, data = mtcars)
# interactions
model <- lm(mpg ~ wt * hp, data = mtcars)
model <- lm(mpg ~ wt + hp + wt:hp, data = mtcars)
# transformations
model <- lm(log(mpg) ~ wt, data = mtcars)
model <- lm(mpg ~ poly(wt, 2), data = mtcars) # quadratic
model <- lm(mpg ~ I(wt^2) + wt, data = mtcars)
# categorical predictors
model <- lm(mpg ~ factor(cyl), data = mtcars)
# diagnostics
plot(model) # 4 diagnostic plots
library(car)
vif(model) # variance inflation factor일반화 선형 모델
glm은 lm을 비정규 응답으로 확장. 로지스틱(이진 결과)은 family=binomial; 카운트는 family=poisson. 로지스틱의 경우 exp(coef)는 승산 비 제공. 중첩 모델은 anova(..., test='Chisq')로 비교. 과산포 카운트(분산 > 평균)의 경우 Poisson 대신 glm.nb(음이항) 사용.
# logistic regression (binary)
model <- glm(am ~ wt + hp, data = mtcars, family = binomial)
summary(model)
# predicted probabilities
predict(model, type = "response")
# odds ratios
exp(coef(model))
# Poisson regression (counts)
model <- glm(count ~ group, data = df, family = poisson)
# negative binomial (overdispersed counts)
library(MASS)
model <- glm.nb(count ~ group, data = df)
# compare models
model1 <- glm(y ~ x1, family = binomial, data = df)
model2 <- glm(y ~ x1 + x2, family = binomial, data = df)
anova(model1, model2, test = "Chisq")
# goodness of fit
1 - pchisq(model$deviance, model$df.residual)
# McFadden pseudo R²
1 - model$deviance / model$null.deviance모델 선택
단계별 선택은 쉽지만 편향 — 전체 부분집합(regsubsets) 또는 정규화(glmnet) 선호. AIC/BIC는 적합과 복잡성 균형(낮을수록 좋음). 교차 검증은 정직한 표본 외 추정 제공. alpha=0인 glmnet은 능선, alpha=1은 라쏘(계수를 0으로 만들 수 있음 — 특성 선택).
# stepwise selection
full <- lm(mpg ~ ., data = mtcars)
step(full, direction = "both")
step(full, direction = "backward")
step(full, direction = "forward", scope = list(lower = ~1, upper = full))
# AIC and BIC
AIC(model1, model2)
BIC(model1, model2)
# all subsets
library(leaps)
leaps <- regsubsets(mpg ~ ., data = mtcars, nvmax = 10)
plot(leaps, scale = "adjr2")
plot(leaps, scale = "Cp")
# cross-validation
library(caret)
train(mpg ~ ., data = mtcars, method = "lm",
trControl = trainControl(method = "cv", number = 10))
# regularized (glmnet)
library(glmnet)
X <- model.matrix(mpg ~ ., mtcars)[, -1]
y <- mtcars$mpg
cv_model <- cv.glmnet(X, y, alpha = 0) # ridge
cv_model <- cv.glmnet(X, y, alpha = 1) # lasso혼합 효과 모델
혼합 효과 모델은 상관 데이터(반복 측정, 군집 표본) 처리. (1 | subject)는 대상당 무작위 절편; (time | subject)는 무작위 기울기 추가. 중첩 그룹은 / 사용. 교차 그룹은 + 사용. 연속형은 lmer, 비연속형은 glmer. p-값을 위해 lmerTest 사용(lme4는 기본적으로 계산 안 함).
library(lme4)
library(lmerTest)
# random intercept
model <- lmer(score ~ treatment + (1 | subject), data = df)
# random intercept and slope
model <- lmer(score ~ time + (time | subject), data = df)
# nested random effects
model <- lmer(score ~ 1 + (1 | school/class), data = df)
# crossed random effects
model <- lmer(score ~ 1 + (1 | subject) + (1 | item), data = df)
# generalized mixed model
glmer(outcome ~ treatment + (1 | subject), data = df, family = binomial)
# summary and CIs
summary(model)
confint(model, method = "Wald")
confint(model, method = "boot")
# anova
anova(model)
# random effects
ranef(model)
VarCorr(model)
# predict
predict(model, newdata = df, allow.new.levels = TRUE)진단과 예측
항상 진단 확인: 잔차 플롯은 비선형성과 이분산성 드러냄; Cook's 거리(>4/n)는 영향점 표시; hatvalues(>2p/n)는 고레버리지점 표시. predict with interval='confidence'는 평균용, 'prediction'은 개별 값용(더 넓음). ggeffects는 시각화를 위한 한계 효과 계산.
model <- lm(mpg ~ wt + hp, data = mtcars)
# diagnostic plots
par(mfrow = c(2, 2))
plot(model)
par(mfrow = c(1, 1))
# residuals vs leverage
plot(model, which = 5)
# influence measures
influence.measures(model)
hatvalues(model) # leverage
cooks.distance(model) # Cook's distance
dfbeta(model) # change in coef per obs
# identify outliers
which(cooks.distance(model) > 4 / nrow(mtcars))
# predictions
new_data <- data.frame(wt = c(2, 3, 4), hp = c(100, 150, 200))
predict(model, newdata = new_data, interval = "confidence")
predict(model, newdata = new_data, interval = "prediction")
# marginal effects
library(ggeffects)
ggeffect(model, terms = "wt")
plot(ggeffect(model, terms = c("wt", "hp")))
# R squared variants
library(rsq)
rsq(model, type = "v") # variance-based
rsq(model, type = "kl") # Kullback-LeiblerR Markdown과 보고
R Markdown 기본
R Markdown은 산문(Markdown), 코드(R 청크), 출력(표/플롯)을 재현 가능한 보고서로 결합. YAML 헤더는 메타데이터와 출력 형식 설정. 청크 옵션: echo=FALSE는 코드 숨김, include=FALSE는 실행하지만 모두 숨김, fig.cap은 캡션 추가. 백틱의 인라인 r 코드는 산문에 값 삽입. Knit(Ctrl+Shift+K)은 렌더링.
---
title: "Quarterly Report"
author: "Data Team"
date: "2024-12-31"
output: html_document
---
## Section
Inline code: the mean is `r mean(x)`.
```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = FALSE, warning = FALSE, message = FALSE)
library(dplyr)
library(ggplot2)
data <- read_csv("data.csv")
```
```{r plot, fig.cap="Sales over time"}
ggplot(data, aes(date, sales)) + geom_line()
```
```{r table}
library(knitr)
kable(head(data), caption = "First 6 rows")
```청크 옵션
청크 옵션은 코드 실행과 출력 제어. cache=TRUE는 재-knit 속도 향상 but 변경을 숨길 수 있음 — 캐시 무효화를 위해 dependson 설정. R Markdown은 knitr 엔진을 통해 많은 언어 지원: python, bash, sql, javascript 등. Python의 경우 reticulate 패키지로 R과 Python 간 객체 공유.
```{r chunk-name, options}
# code here
```
# Common options:
# echo=FALSE hide code, show output
# eval=FALSE show code, don't run
# include=FALSE run, hide code and output
# results="hide" show code, hide text output
# warning=FALSE hide warnings
# message=FALSE hide messages
# fig.width=6 figure width (inches)
# fig.height=4 figure height
# fig.cap="..." figure caption
# fig.path="figs/" where to save figures
# cache=TRUE cache results (faster re-knits)
# dependson="..." cache dependencies
# dev="svg" output device
# global options
knitr::opts_chunk$set(
echo = TRUE,
fig.width = 6,
fig.height = 4,
fig.path = "figures/",
cache = TRUE
)
# language engines
```{python}
# Python code
```
```{bash}
# Shell commands
```
```{sql, connection=db}
# SQL queries
```출력 형식
html_document는 가장 유연(대화형, code_folding, 페이징 표). pdf_document는 LaTeX 필요(tinytex::install_tinytex()로 TinyTeX 설치). word_document는 스타일링을 위해 참조 docx를 사용하는 Word 파일 생성. 발표의 경우 ioslides(내장) 또는 revealjs(더 세련됨) 사용. 여러 출력을 함께 지정 가능.
---
output:
html_document:
toc: true
toc_float: true
toc_depth: 3
number_sections: true
theme: flatly
highlight: tango
code_folding: hide
df_print: paged
---
---
output:
pdf_document:
toc: true
number_sections: true
fig_caption: true
latex_engine: xelatex
---
---
output:
word_document:
toc: true
reference_docx: template.docx
---
---
output:
ioslides_presentation:
widescreen: true
---
---
output:
revealjs::revealjs_presentation:
theme: solarized
transition: slide
---
# Multiple formats
---
output:
html_document: default
pdf_document: default
---kable과 gt로 표
kable + kableExtra는 그룹화, 조건부 형식화, 스타일링으로 출판 품질 표 생성. gt는 더 그래픽의-문법 스타일의 최신 대안. DT는 정렬, 필터링, 페이지 매김이 있는 대화형 HTML 표 생성 — HTML 보고서에 좋음. 출력 형식 기반 선택(kable은 모두에서 작동; DT는 HTML만).
library(knitr)
library(kableExtra)
library(gt)
# basic kable
kable(head(mtcars))
# styled kable
kable(head(mtcars), format = "html", caption = "Top cars") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE) %>%
add_header_above(c(" " = 1, "Specs" = 3, "Performance" = 7))
# conditional formatting
kable(df) %>%
cell_spec(value, color = ifelse(value > 0, "green", "red")) %>%
row_spec(1, bold = TRUE, background = "yellow")
# gt (modern alternative)
mtcars %>%
head() %>%
gt() %>%
tab_header(title = "MT Cars", subtitle = "First 6 rows") %>%
cols_label(mpg = "MPG", cyl = "Cylinders") %>%
tab_options(table.width = pct(80))
# DT for interactive tables
library(DT)
datatable(mtcars, filter = "top", options = list(pageLength = 5))매개변수와 자동화
params는 보고서를 재사용 가능하게 — YAML에서 정의, params$<name>으로 접근. rmarkdown::render()로 사용자 정의 params로 렌더링. 매개변수 값 반복으로 여러 보고서 생성(지역당, 분기당 등). 이것은 자동화된 보고 파이프라인의 기반. 주기적 보고를 위해 cron/예약된 R과 결합.
---
title: "Regional Sales Report"
output: html_document
params:
region: "West"
year: 2024
data_file: "sales.csv"
---
## Report for `r params$region` in `r params$year`
```{r}
data <- read_csv(params$data_file) %>%
filter(region == params$region, year == params$year)
```
# Render from R
rmarkdown::render("report.Rmd",
params = list(region = "East", year = 2024),
output_file = "east_2024.html")
# Render from command line
# Rscript -e 'rmarkdown::render("report.Rmd", params = list(region = "East"))'
# Loop over parameters
for (r in c("West", "East", "North")) {
rmarkdown::render("report.Rmd",
params = list(region = r),
output_file = paste0(r, "_report.html"))
}purrr 함수형 프로그래밍
map 계열
map은 tidyverse의 lapply — 항상 리스트 반환. map_dbl/chr/int/lgl은 타입화된 벡터 반환(map보다 안전). map2와 pmap은 여러 인자를 병렬로 반복. walk는 부작용(출력, 저장)용. .x 대명사는 현재 요소 참조; pmap에서는 ..1, ..2 등 사용. map_dfr은 데이터 프레임을 행으로 결합.
library(purrr)
# map: list output
map(1:5, ~ .x^2) # list(1, 4, 9, 16, 25)
map_dbl(1:5, ~ .x^2) # numeric vector
map_chr(1:5, ~ paste0("n", .x)) # character vector
map_int(1:5, ~ .x * 2L) # integer vector
map_lgl(1:5, ~ .x > 3) # logical vector
map_dfr(1:3, ~ data.frame(id = .x, val = .x^2)) # row-bound df
map_dfc(1:3, ~ data.frame(x = .x)) # col-bound df
# multiple arguments
map2(1:3, 4:6, ~ .x + .y)
pmap(list(1:3, 4:6, 7:9), ~ ..1 + ..2 + ..3)
# walk (for side effects)
walk(1:5, ~ print(.x))
walk(files, ~ process(.x))
walk2(plots, filenames, ~ ggsave(.y, .x))
# in pipelines
mtcars %>%
split(.$cyl) %>%
map(~ lm(mpg ~ wt, data = .x)) %>%
map(summary) %>%
map_dbl(~ .x$r.squared)Safely와 quietly
safely는 함수를 감싸 throw 대신 (result, error) 반환 — 하나의 실패가 모든 것을 멈추지 않아야 하는 배치 처리에 필수. possibly는 기본값 반환. quietly는 메시지/경고 캡처. transpose는 쌍의 리스트를 리스트의 쌍으로 뒤집음. insistently는 백오프로 재시도 — 불안정한 API에 좋음.
library(purrr)
# safely: capture errors without stopping
safe_log <- safely(log)
safe_log(10) # list(result = 2.3, error = NULL)
safe_log("a") # list(result = NULL, error = <error>)
# process many, keep going on error
results <- map(values, safely(my_function))
successes <- map(results, "result") %>% compact()
errors <- map(results, "error") %>% compact()
# quietly: capture messages/warnings
quiet_log <- quietly(log)
quiet_log(10) # list(result, output, warnings, messages)
# possibly: return default on error
safe_log <- possibly(log, otherwise = NA_real_)
map(values, safe_log) # never errors
# transpose for cleaner output
results <- map(values, safely(fun)) %>% transpose()
results$result # all results
results$error # all errors
# insistently: retry on failure
slow_fn <- insistently(api_call, rate = rate_backoff())Reduce와 accumulate
reduce는 요소를 쌍으로 결합(foldl처럼); accumulate는 중간값 유지. 많은 데이터 프레임 조인이나 누적 계산 구축에 유용. detect/find는 첫 번째 일치; keep/discard는 필터. every/some은 술어 검사. 이것들은 간결하고 조합 가능한 연산으로 루프 대체.
library(purrr)
# reduce: combine pairwise
reduce(c(1, 2, 3, 4), ) # 10
reduce(c(1, 2, 3, 4), ~ .x * .y) # 24
reduce(list(df1, df2, df3), full_join, by = "id")
# accumulate: keep intermediate results
accumulate(c(1, 2, 3, 4), ) # 1 3 6 10
accumulate(c(2, 3, 4), ~ .x * .y) # 2 6 24
# right reduce
reduce(c(1, 2, 3, 4), , .dir = "backward")
# with init
reduce(c(1, 2, 3), ~ c(.x, .y), .init = numeric(0))
# detect
detect(1:10, ~ .x > 5) # 6 (first match)
detect_index(1:10, ~ .x > 5) # 6 (index)
detect(1:10, ~ .x > 5, .right = TRUE) # 10 (last match)
# keep/discard
keep(1:10, ~ .x %% 2 == 0) # 2 4 6 8 10
discard(1:10, ~ .x %% 2 == 0) # 1 3 5 7 9
every(1:10, ~ .x > 0) # TRUE
some(1:10, ~ .x > 5) # TRUE부분 적용과 합성
partial은 인자를 미리 채움 — 일반 함수에서 특수 함수 생성에 유용. compose는 함수 체인(오른쪽에서 왼쪽). negate는 술어 반전. lift는 함수를 인자 리스트를 받도록 변환. imap은 인덱스를 두 번째 인자로 하는 map2. 이 도구들은 함수 합성을 깔끔하고 읽기 쉽게 만듭니다.
library(purrr)
# partial: pre-fill arguments
add_one <- partial(, 1)
add_one(5) # 6
round2 <- partial(round, digits = 2)
round2(3.14159) # 3.14
# compose: chain functions
clean_string <- compose(
str_trim,
str_to_lower,
~ str_replace_all(.x, "[^a-z]", " ")
)
clean_string(" Hello, World! ") # "hello world "
# %>% (magrittr pipe) vs compose
# pipe: data %>% f %>% g %>% h
# compose: g %>% f creates a new function
# negate
is_missing <- negate(is.na)
is_missing(5) # TRUE
# lift (vectorize)
sum2 <- lift() # takes a list of 2 args
sum2(list(1, 2)) # 3
# walk with index
imap(letters[1:3], ~ paste0(.y, ": ", .x))
# list("1: a", "2: b", "3: c")리스트 열과 중첩 데이터
nest + map + unnest 패턴은 tidyverse의 분할-적용-결합. 리스트 열은 모든 객체(모델, 예측, 서브 데이터) 보유. 데이터 프레임의 map은 열 반복. pluck은 중첩 요소 안전 추출. modify_if는 술어에 일치하는 요소 변경, 원래 구조 보존.
library(purrr)
library(dplyr)
library(tidyr)
# nest data, fit models, predict
mtcars %>%
group_by(cyl) %>%
nest() %>%
mutate(
model = map(data, ~ lm(mpg ~ wt, data = .x)),
rsq = map_dbl(model, ~ summary(.x)$r.squared),
pred = map2(model, data, predict)
)
# unnest predictions
mtcars %>%
group_by(cyl) %>%
nest() %>%
mutate(model = map(data, ~ lm(mpg ~ wt, .x))) %>%
mutate(pred = map2(model, data, predict)) %>%
select(cyl, data, pred) %>%
unnest(c(data, pred))
# apply function to each column
mtcars %>% map_dbl(mean)
mtcars %>% map_dbl(sd)
# apply to columns of specific type
iris %>%
map_if(is.numeric, mean) %>%
map_dbl(round, 2)
# pluck
list(a = list(b = list(c = 42))) %>% pluck("a", "b", "c")
# 42
# modify (returns same type)
modify_if(iris, is.numeric, ~ .x * 2)
modify_depth(nested_list, 2, ~ .x + 1)Shiny 앱
기본 앱 구조
Shiny 앱은 ui(레이아웃)와 server(로직)로 구성. 입력은 input$<id>에서; 출력은 render* 함수를 통해 output$<id>로. fluidPage는 기본 레이아웃; sidebarLayout은 사이드바(컨트롤)와 메인(출력)으로 분할. app.R로 자체 폴더에 저장; 폴더 이름이 앱 이름이 됨.
library(shiny)
ui <- fluidPage(
titlePanel("Hello Shiny"),
sidebarLayout(
sidebarPanel(
sliderInput("n", "Number of points", 1, 100, 50)
),
mainPanel(
plotOutput("scatter")
)
)
)
server <- function(input, output, session) {
output$scatter <- renderPlot({
plot(1:input$n, rnorm(input$n))
})
}
shinyApp(ui, server)
# Save as app.R in a folder, run with:
# shiny::runApp("path/to/folder")
# Or click "Run App" in RStudio입력과 반응성
Shiny는 많은 입력 컨트롤이 있습니다. observeEvent는 입력이 변경될 때 코드 실행; eventReactive는 이벤트에서 반응값 생성. reactive()는 입력이 변경될 때까지 결과 캐시. reactiveVal/reactiveValues는 가변 상태 보유. 명시적 트리거를 위해 actionButton + observeEvent 사용(모든 키입력에 반응하지 마세요).
library(shiny)
ui <- fluidPage(
numericInput("n", "N", value = 10, min = 1, max = 100),
textInput("label", "Label", value = "Data"),
selectInput("color", "Color", choices = c("red", "blue", "green")),
dateInput("date", "Date"),
dateRangeInput("range", "Range"),
checkboxInput("show", "Show?", value = TRUE),
checkboxGroupInput("vars", "Variables", choices = names(mtcars)),
radioButtons("dist", "Distribution",
choices = c("Normal", "Uniform", "Exponential")),
fileInput("file", "Upload CSV", accept = ".csv"),
actionButton("go", "Go!")
)
server <- function(input, output, session) {
# event-triggered
observeEvent(input$go, {
showNotification(paste("Clicked", input$go))
})
# reactive expression (cached)
data <- reactive({
rnorm(input$n)
})
# reactive value
val <- reactiveVal(0)
observeEvent(input$go, val(val() + 1))
}출력과 렌더링
각 출력 타입은 매칭되는 render* 함수: 플롯은 renderPlot, 표는 renderTable, 콘솔 출력은 renderPrint, 문자열은 renderText, 동적 UI는 renderUI. 출력은 반응형 — 입력 의존성이 변경될 때 재실행. DT::dataTableOutput은 대화형 표의 표준.
library(shiny)
library(ggplot2)
library(DT)
ui <- fluidPage(
plotOutput("plot", click = "plot_click"),
tableOutput("table"),
DT::dataTableOutput("dt"),
verbatimTextOutput("summary"),
textOutput("text"),
uiOutput("dynamic")
)
server <- function(input, output, session) {
output$plot <- renderPlot({
ggplot(mtcars, aes(wt, mpg)) + geom_point()
})
output$table <- renderTable({
head(mtcars)
})
output$dt <- DT::renderDataTable({
datatable(mtcars, filter = "top")
})
output$summary <- renderPrint({
summary(mtcars)
})
output$text <- renderText({
paste("You clicked:", input$plot_click$x)
})
output$dynamic <- renderUI({
if (input$n > 5) strong("Big!") else em("Small")
})
}
# observe vs reactive
# observe: side effects (output$ assignments, updates)
# reactive: returns a value, used by other reactives반응형 프로그래밍
reactive()는 주력 — 캐시되고 지연(읽을 때만 재계산). observe()는 즉시(의존성 변경 시 즉시 실행) — 부작용용. eventReactive는 이벤트 대기. reactiveValues는 여러 가변 값 보유(작은 반응 객체처럼). isolate는 의존성 생성 없이 값 읽기. debounce는 빠른 입력 변경 스로틀.
library(shiny)
server <- function(input, output, session) {
# reactive: cached, lazy
filtered <- reactive({
mtcars %>% filter(cyl == input$cyl)
})
# observe: eager, side effects
observe({
updateSelectInput(session, "model",
choices = unique(filtered()$model))
})
# eventReactive: triggered by event
result <- eventReactive(input$go, {
run_analysis(input$params)
})
# reactiveVal: single mutable value
counter <- reactiveVal(0)
observeEvent(input$add, counter(counter() + 1))
# reactiveValues: multiple values
rv <- reactiveValues(data = NULL, status = "idle")
observeEvent(input$load, {
rv$data <- read.csv(input$file$datapath)
rv$status <- "loaded"
})
# debounce (throttle rapid changes)
search <- reactive({ input$search }) %>% debounce(500)
# isolate (use value without dependency)
output$plot <- renderPlot({
plot(isolate(rv$data)) # not reactive on rv$data
})
}배포
shinyapps.io는 가장 쉬운 호스팅(무료 티어 사용 가능). 자체 호스팅의 경우 Linux에 Shiny Server 설치 또는 Docker 사용(rocker/shiny 이미지). 성능: 플롯 캐시, 긴 작업에 비동기(future/promises) 사용, render 함수에서 파일 재읽기 피함. 디버깅을 위해 반응 그래프 시각화하는 reactlog(Ctrl+F3) 활성화.