base-R
name: base-r description: Provides base R programming guidance covering data structures, data wrangling, statistical modeling, visualization, and I/O, using only packages included in a standard R…
prompts.chatv10 stars · 0 forks · 0 makes≈17K tokens
Base R Programming Skill
A comprehensive reference for base R programming — covering data structures, control flow, functions, I/O, statistical computing, and plotting.
Quick Reference
Data Structures
# Vectors (atomic)
x <- c(1, 2, 3) # numeric
y <- c("a", "b", "c") # character
z <- c(TRUE, FALSE, TRUE) # logical
# Factor
f <- factor(c("low", "med", "high"), levels = c("low", "med", "high"), ordered = TRUE)
# Matrix
m <- matrix(1:6, nrow = 2, ncol = 3)
m[1, ] # first row
m[, 2] # second column
# List
lst <- list(name = "ali", scores = c(90, 85), passed = TRUE)
lst$name # access by name
lst[[2]] # access by position
# Data frame
df <- data.frame(
id = 1:3,
name = c("a", "b", "c"),
value = c(10.5, 20.3, 30.1),
stringsAsFactors = FALSE
)
df[df$value > 15, ] # filter rows
df$new_col <- df$value * 2 # add column
Subsetting
# Vectors
x[1:3] # by position
x[c(TRUE, FALSE)] # by logical
x[x > 5] # by condition
x[-1] # exclude first
# Data frames
df[1:5, ] # first 5 rows
df[, c("name", "value")] # select columns
df[df$value > 10, "name"] # filter + select
subset(df, value > 10, select = c(name, value))
# which() for index positions
idx <- which(df$value == max(df$value))
Control Flow
# if/else
if (x > 0) {
"positive"
} else if (x == 0) {
"zero"
} else {
"negative"
}
# ifelse (vectorized)
ifelse(x > 0, "pos", "neg")
# for loop
for (i in seq_along(x)) {
cat(i, x[i], "\n")
}
# while
while (condition) {
# body
if (stop_cond) break
}
# switch
switch(type,
"a" = do_a(),
"b" = do_b(),
stop("Unknown type")
)
Functions
# Define
my_func <- function(x, y = 1, ...) {
result <- x + y
return(result) # or just: result
}
# Anonymous functions
sapply(1:5, function(x) x^2)
# R 4.1+ shorthand:
sapply(1:5, \(x) x^2)
# Useful: do.call for calling with a list of args
do.call(paste, list("a", "b", sep = "-"))
Apply Family
# sapply — simplify result to vector/matrix
sapply(lst, length)
# lapply — always returns list
lapply(lst, function(x) x[1])
# vapply — like sapply but with type safety
vapply(lst, length, integer(1))
# apply — over matrix margins (1=rows, 2=cols)
apply(m, 2, sum)
# tapply — apply by groups
tapply(df$value, df$group, mean)
# mapply — multivariate
mapply(function(x, y) x + y, 1:3, 4:6)
# aggregate — like tapply for data frames
aggregate(value ~ group, data = df, FUN = mean)
String Operations
paste("a", "b", sep = "-") # "a-b"
paste0("x", 1:3) # "x1" "x2" "x3"
sprintf("%.2f%%", 3.14159) # "3.14%"
nchar("hello") # 5
substr("hello", 1, 3) # "hel"
gsub("old", "new", text) # replace all
grep("pattern", x) # indices of matches
grepl("pattern", x) # logical vector
strsplit("a,b,c", ",") # list("a","b","c")
trimws(" hi ") # "hi"
tolower("ABC") # "abc"
Data I/O
# CSV
df <- read.csv("data.csv", stringsAsFactors = FALSE)
write.csv(df, "output.csv", row.names = FALSE)
# Tab-delimited
df <- read.delim("data.tsv")
# General
df <- read.table("data.txt", header = TRUE, sep = "\t")
# RDS (single R object, preserves types)
saveRDS(obj, "data.rds")
obj <- readRDS("data.rds")
# RData (multiple objects)
save(df1, df2, file = "data.RData")
load("data.RData")
# Connections
con <- file("big.csv", "r")
chunk <- readLines(con, n = 100)
close(con)
Base Plotting
# Scatter
plot(x, y, main = "Title", xlab = "X", ylab = "Y",
pch = 19, col = "steelblue", cex = 1.2)
# Line
plot(x, y, type = "l", lwd = 2, col = "red")
lines(x, y2, col = "blue", lty = 2) # add line
# Bar
barplot(table(df$category), main = "Counts",
col = "lightblue", las = 2)
# Histogram
hist(x, breaks = 30, col = "grey80",
main = "Distribution", xlab = "Value")
# Box plot
boxplot(value ~ group, data = df,
col = "lightyellow", main = "By Group")
# Multiple plots
par(mfrow = c(2, 2)) # 2x2 grid
# ... four plots ...
par(mfrow = c(1, 1)) # reset
# Save to file
png("plot.png", width = 800, height = 600)
plot(x, y)
dev.off()
# Add elements
legend("topright", legend = c("A", "B"),
col = c("red", "blue"), lty = 1)
abline(h = 0, lty = 2, col = "grey")
text(x, y, labels = names, pos = 3, cex = 0.8)
Statistics
# Descriptive
mean(x); median(x); sd(x); var(x)
quantile(x, probs = c(0.25, 0.5, 0.75))
summary(df)
cor(x, y)
table(df$category) # frequency table
# Linear model
fit <- lm(y ~ x1 + x2, data = df)
summary(fit)
coef(fit)
predict(fit, newdata = new_df)
confint(fit)
# t-test
t.test(x, y) # two-sample
t.test(x, mu = 0) # one-sample
t.test(before, after, paired = TRUE)
# Chi-square
chisq.test(table(df$a, df$b))
# ANOVA
fit <- aov(value ~ group, data = df)
summary(fit)
TukeyHSD(fit)
# Correlation test
cor.test(x, y, method = "pearson")
Data Manipulation
# Merge (join)
merged <- merge(df1, df2, by = "id") # inner
merged <- merge(df1, df2, by = "id", all = TRUE) # full outer
merged <- merge(df1, df2, by = "id", all.x = TRUE) # left
# Reshape
wide <- reshape(long, direction = "wide",
idvar = "id", timevar = "time", v.names = "value")
long <- reshape(wide, direction = "long",
varying = list(c("v1", "v2")), v.names = "value")
# Sort
df[order(df$value), ] # ascending
df[order(-df$value), ] # descending
df[order(df$group, -df$value), ] # multi-column
# Remove duplicates
df[!duplicated(df), ]
df[!duplicated(df$id), ]
# Stack / combine
rbind(df1, df2) # stack rows (same columns)
cbind(df1, df2) # bind columns (same rows)
# Transform columns
df$log_val <- log(df$value)
df$category <- cut(df$value, breaks = c(0, 10, 20, Inf),
labels = c("low", "med", "high"))
Environment & Debugging
ls() # list objects
rm(x) # remove object
rm(list = ls()) # clear all
str(obj) # structure
class(obj) # class
typeof(obj) # internal type
is.na(x) # check NA
complete.cases(df) # rows without NA
traceback() # after error
debug(my_func) # step through
browser() # breakpoint in code
system.time(expr) # timing
Sys.time() # current time
Reference Files
For deeper coverage, read the reference files in references/:
Function Gotchas & Quick Reference (condensed from R 4.5.3 Reference Manual)
Non-obvious behaviors, surprising defaults, and tricky interactions — only what Claude doesn't already know:
- data-wrangling.md — Read when: subsetting returns wrong type, apply on data frame gives unexpected coercion, merge/split/cbind behaves oddly, factor levels persist after filtering, table/duplicated edge cases.
- modeling.md — Read when: formula syntax is confusing (
I(),*vs:,/), aov gives wrong SS type, glm silently fits OLS, nls won't converge, predict returns wrong scale, optim/optimize needs tuning. - statistics.md — Read when: hypothesis test gives surprising result, need to choose correct p.adjust method, clustering parameters seem wrong, distribution function naming is confusing (
d/p/q/rprefixes). - visualization.md — Read when: par settings reset unexpectedly, layout/mfrow interaction is confusing, axis labels are clipped, colors don't look right, need specialty plots (contour, persp, mosaic, pairs).
- io-and-text.md — Read when: read.table silently drops data or misparses columns, regex behaves differently than expected, sprintf formatting is tricky, write.table output has unwanted row names.
- dates-and-system.md — Read when: Date/POSIXct conversion gives wrong day, time zones cause off-by-one, difftime units are unexpected, need to find/list/test files programmatically.
- misc-utilities.md — Read when: do.call behaves differently than direct call, need Reduce/Filter/Map, tryCatch handler doesn't fire, all.equal returns string not logical, time series functions need setup.
Tips for Writing Good R Code
- Use
vapply()oversapply()in production code — it enforces return types - Prefer
seq_along(x)over1:length(x)— the latter breaks whenxis empty - Use
stringsAsFactors = FALSEinread.csv()/data.frame()(default changed in R 4.0) - Vectorize operations instead of writing loops when possible
- Use
stop(),warning(),message()for error handling — notprint() <<-assigns to parent environment — use sparingly and intentionallywith(df, expr)avoids repeatingdf$everywhereSys.setenv()and.Renvironfor environment variables FILE:references/misc-utilities.md
Miscellaneous Utilities — Quick Reference
Non-obvious behaviors, gotchas, and tricky defaults for R functions. Only what Claude doesn't already know.
do.call
do.call(fun, args_list)—argsmust be a list, even for a single argument.quote = TRUEprevents evaluation of arguments before the call — needed when passing expressions/symbols.- Behavior of
substituteinsidedo.calldiffers from direct calls. Semantics are not fully defined for this case. - Useful pattern:
do.call(rbind, list_of_dfs)to combine a list of data frames.
Reduce / Filter / Map / Find / Position
R's functional programming helpers from base — genuinely non-obvious.
Reduce(f, x)applies binary functionfcumulatively:Reduce("+", 1:4)=((1+2)+3)+4. Direction matters for non-commutative ops.Reduce(f, x, accumulate = TRUE)returns all intermediate results — equivalent to Python'sitertools.accumulate.Reduce(f, x, right = TRUE)folds from the right:f(x1, f(x2, f(x3, x4))).Reducewithinitadds a starting value:Reduce(f, x, init = v)=f(f(f(v, x1), x2), x3).Filter(f, x)keeps elements wheref(elem)isTRUE. Unlikex[sapply(x, f)], handlesNULL/empty correctly.Map(f, ...)is a simple wrapper formapply(f, ..., SIMPLIFY = FALSE)— always returns a list.Find(f, x)returns the first element wheref(elem)isTRUE.Find(f, x, right = TRUE)for last.Position(f, x)returns the index of the first match (likeFindbut returns position, not value).
lengths
lengths(x)returns the length of each element of a list. Equivalent tosapply(x, length)but faster (implemented in C).- Works on any list-like object. Returns integer vector.
conditions (tryCatch / withCallingHandlers)
tryCatchunwinds the call stack — handler runs in the calling environment, not where the error occurred. Cannot resume execution.withCallingHandlersdoes NOT unwind — handler runs where the condition was signaled. Can inspect/log then let the condition propagate.tryCatch(expr, error = function(e) e)returns the error condition object.tryCatch(expr, warning = function(w) {...})catches the first warning and exits. UsewithCallingHandlers+invokeRestart("muffleWarning")to suppress warnings but continue.tryCatchfinallyclause always runs (like Java try/finally).globalCallingHandlers()registers handlers that persist for the session (useful for logging).- Custom conditions:
stop(errorCondition("msg", class = "myError"))then catch withtryCatch(..., myError = function(e) ...).
all.equal
- Tests near equality with tolerance (default
1.5e-8, i.e.,sqrt(.Machine$double.eps)). - Returns
TRUEor a character string describing the difference — NOTFALSE. UseisTRUE(all.equal(x, y))in conditionals. toleranceargument controls numeric tolerance.scalefor absolute vs relative comparison.- Checks attributes, names, dimensions — more thorough than
==.
combn
combn(n, m)orcombn(x, m): generates all combinations ofmitems fromx.- Returns a matrix with
mrows; each column is one combination. FUNargument applies a function to each combination:combn(5, 3, sum)returns sums of all 3-element subsets.simplify = FALSEreturns a list instead of a matrix.
modifyList
modifyList(x, val)replaces elements of listxwith those invalby name.- Setting a value to
NULLremoves that element from the list. - Does add new names not in
x— it usesx[names(val)] <- valinternally, so any name invalgets added or replaced.
relist
- Inverse of
unlist: given a flat vector and a skeleton list, reconstructs the nested structure. relist(flesh, skeleton)—fleshis the flat data,skeletonprovides the shape.- Works with factors, matrices, and nested lists.
txtProgressBar
txtProgressBar(min, max, style = 3)— style 3 shows percentage + bar (most useful).- Update with
setTxtProgressBar(pb, value). Close withclose(pb). - Style 1: rotating
|/-\, style 2: simple progress. Only style 3 shows percentage.
object.size
- Returns an estimate of memory used by an object. Not always exact for shared references.
format(object.size(x), units = "MB")for human-readable output.- Does not count the size of environments or external pointers.
installed.packages / update.packages
installed.packages()can be slow (scans all packages). Usefind.package()orrequireNamespace()to check for a specific package.update.packages(ask = FALSE)updates all packages without prompting.lib.locspecifies which library to check/update.
vignette / demo
vignette()lists all vignettes;vignette("name", package = "pkg")opens a specific one.demo()lists all demos;demo("topic")runs one interactively.browseVignettes()opens vignette browser in HTML.
Time series: acf / arima / ts / stl / decompose
ts(data, start, frequency):frequencyis observations per unit time (12 for monthly, 4 for quarterly).acfdefaulttype = "correlation". Usetype = "partial"for PACF.plot = FALSEto suppress auto-plotting.arima(x, order = c(p,d,q))for ARIMA models.seasonal = list(order = c(P,D,Q), period = S)for seasonal component.arimahandlesNAvalues in the time series (via Kalman filter).stlrequiress.window(seasonal window) — must be specified, no default.s.window = "periodic"assumes fixed seasonality.decompose: simpler thanstl, uses moving averages.type = "additive"or"multiplicative".stlresult components:$time.seriesmatrix with columnsseasonal,trend,remainder. FILE:references/data-wrangling.md
Data Wrangling — Quick Reference
Non-obvious behaviors, gotchas, and tricky defaults for R functions. Only what Claude doesn't already know.
Extract / Extract.data.frame
Indexing pitfalls in base R.
m[j = 2, i = 1]ism[2, 1]notm[1, 2]— argument names are ignored in[, positional matching only. Never name index args.- Factor indexing:
x[f]uses integer codes of factorf, not its character labels. Usex[as.character(f)]for label-based indexing. x[[]]with no index is always an error.x$namedoes partial matching by default;x[["name"]]does not (exact by default).- Assigning
NULLviax[[i]] <- NULLorx$name <- NULLdeletes that list element. - Data frame
[with single column:df[, 1]returns a vector (drop=TRUE default for columns), butdf[1, ]returns a data frame (drop=FALSE for rows). Usedrop = FALSEexplicitly. - Matrix indexing a data frame (
df[cbind(i,j)]) coerces to matrix first — avoid.
subset
Use interactively only; unsafe for programming.
subsetargument uses non-standard evaluation — column names are resolved in the data frame, which can silently pick up wrong variables in programmatic use. Use[with explicit logic in functions.NAs in the logical condition are treated asFALSE(rows silently dropped).- Factors may retain unused levels after subsetting; call
droplevels().
match / %in%
%in%never returns NA — this makes it safe forif()conditions unlike==.match()returns position of first match only; duplicates intableare ignored.- Factors, raw vectors, and lists are all converted to character before matching.
NaNmatchesNaNbut notNA;NAmatchesNAonly.
apply
- On a data frame,
applycoerces to matrix viaas.matrixfirst — mixed types become character. - Return value orientation is transposed: if FUN returns length-n vector, result has dim
c(n, dim(X)[MARGIN]). Row results become columns. - Factor results are coerced to character in the output array.
...args cannot share names withX,MARGIN, orFUN(partial matching risk).
lapply / sapply / vapply
sapplycan return a vector, matrix, or list unpredictably — usevapplyin non-interactive code with explicitFUN.VALUEtemplate.- Calling primitives directly in
lapplycan cause dispatch issues; wrap infunction(x) is.numeric(x)rather than bareis.numeric. sapplywithsimplify = "array"can produce higher-rank arrays (not just matrices).
tapply
- Returns an array (not a data frame). Class info on return values is discarded (e.g., Date objects become numeric).
...args to FUN are not divided into cells — they apply globally, so FUN should not expect additional args with same length as X.default = NAfills empty cells; setdefault = 0for sum-like operations. Before R 3.4.0 this was hard-coded toNA.- Use
array2DF()to convert result to a data frame.
mapply
- Argument name is
SIMPLIFY(all caps) notsimplify— inconsistent withsapply. MoreArgsmust be a list of args not vectorized over.- Recycles shorter args to common length; zero-length arg gives zero-length result.
merge
- Default
byisintersect(names(x), names(y))— can silently merge on unintended columns if data frames share column names. by = 0orby = "row.names"merges on row names, adding a "Row.names" column.by = NULL(or bothby.x/by.ylength 0) produces Cartesian product.- Result is sorted on
bycolumns by default (sort = TRUE). For unsorted output usesort = FALSE. - Duplicate key matches produce all combinations (one row per match pair).
split
- If
fis a list of factors, interaction is used; levels containing"."can cause unexpected splits unlesssepis changed. drop = FALSE(default) retains empty factor levels as empty list elements.- Supports formula syntax:
split(df, ~ Month).
cbind / rbind
cbindon data frames callsdata.frame(...), notcbind.matrix. Mixing matrices and data frames can give unexpected results.rbindon data frames matches columns by name, not position. Missing columns getNA.cbind(NULL)returnsNULL(not a matrix). For consistency,rbind(NULL)also returnsNULL.
table
- By default excludes NA (
useNA = "no"). UseuseNA = "ifany"orexclude = NULLto count NAs. - Setting
excludenon-empty and non-default impliesuseNA = "ifany". - Result is always an array (even 1D), class "table". Convert to data frame with
as.data.frame(tbl). - Two kinds of NA (factor-level NA vs actual NA) are treated differently depending on
useNA/exclude.
duplicated / unique
duplicatedmarks the second and later occurrences as TRUE, not the first. UsefromLast = TRUEto reverse.- For data frames, operates on whole rows. For lists, compares recursively.
uniquekeeps the first occurrence of each value.
data.frame (gotchas)
stringsAsFactors = FALSEis the default since R 4.0.0 (was TRUE before).- Atomic vectors recycle to match longest column, but only if exact multiple. Protect with
I()to prevent conversion. - Duplicate column names allowed only with
check.names = FALSE, but many operations will de-dup them silently. - Matrix arguments are expanded to multiple columns unless protected by
I().
factor (gotchas)
as.numeric(f)returns integer codes, not original values. Useas.numeric(levels(f))[f]oras.numeric(as.character(f)).- Only
==and!=work between factors; factors must have identical level sets. Ordered factors support<,>. c()on factors unions level sets (since R 4.1.0), but earlier versions converted to integer.- Levels are sorted by default, but sort order is locale-dependent at creation time.
aggregate
- Formula interface (
aggregate(y ~ x, data, FUN)) dropsNAgroups by default. - The data frame method requires
byas a list (not a vector). - Returns columns named after the grouping variables, with result column keeping the original name.
- If FUN returns multiple values, result column is a matrix column inside the data frame.
complete.cases
- Returns a logical vector: TRUE for rows with no NAs across all columns/arguments.
- Works on multiple arguments (e.g.,
complete.cases(x, y)checks both).
order
- Returns a permutation vector of indices, not the sorted values. Use
x[order(x)]to sort. - Default is ascending; use
-xfor descending numeric, ordecreasing = TRUE. - For character sorting, depends on locale. Use
method = "radix"for locale-independent fast sorting. sort.int()withmethod = "radix"is much faster for large integer/character vectors. FILE:references/dates-and-system.md
Dates and System — Quick Reference
Non-obvious behaviors, gotchas, and tricky defaults for R functions. Only what Claude doesn't already know.
Dates (Date class)
Dateobjects are stored as integer days since 1970-01-01. Arithmetic works in days.Sys.Date()returns current date as Date object.seq.Date(from, to, by = "month")— "month" increments can produce varying-length intervals. Adding 1 month to Jan 31 gives Mar 3 (not Feb 28).diff(dates)returns adifftimeobject in days.format(date, "%Y")for year,"%m"for month,"%d"for day,"%A"for weekday name (locale-dependent).- Years before 1CE may not be handled correctly.
length(date_vector) <- npads withNAs if extended.
DateTimeClasses (POSIXct / POSIXlt)
POSIXct: seconds since 1970-01-01 UTC (compact, a numeric vector).POSIXlt: list with components$sec,$min,$hour,$mday,$mon(0-11!),$year(since 1900!),$wday(0-6, Sunday=0),$yday(0-365).- Converting between POSIXct and Date:
as.Date(posixct_obj)usestz = "UTC"by default — may give different date than intended if original was in another timezone. Sys.time()returns POSIXct in current timezone.strptimereturns POSIXlt;as.POSIXct(strptime(...))to get POSIXct.difftimearithmetic: subtracting POSIXct objects gives difftime. Units auto-selected ("secs", "mins", "hours", "days", "weeks").
difftime
difftime(time1, time2, units = "auto")— auto-selects smallest sensible unit.- Explicit units:
"secs","mins","hours","days","weeks". No "months" or "years" (variable length). as.numeric(diff, units = "hours")to extract numeric value in specific units.units(diff_obj) <- "hours"changes the unit in place.
system.time / proc.time
system.time(expr)returnsuser,system, andelapsedtime.gcFirst = TRUE(default): runs garbage collection before timing for more consistent results.proc.time()returns cumulative time since R started — take differences for intervals.elapsed(wall clock) can be less thanuser(multi-threaded BLAS) or more (I/O waits).
Sys.sleep
Sys.sleep(seconds)— allows fractional seconds. Actual sleep may be longer (OS scheduling).- The process yields to the OS during sleep (does not busy-wait).
options (key options)
Selected non-obvious options:
options(scipen = n): positive biases toward fixed notation, negative toward scientific. Default 0. Applies toprint/format/catbut notsprintf.options(digits = n): significant digits for printing (1-22, default 7). Suggestion only.options(digits.secs = n): max decimal digits for seconds in time formatting (0-6, default 0).options(warn = n): -1 = ignore warnings, 0 = collect (default), 1 = immediate, 2 = convert to errors.options(error = recover): drop into debugger on error.options(error = NULL)resets to default.options(OutDec = ","): change decimal separator in output (affectsformat,print, NOTsprintf).options(stringsAsFactors = FALSE): global default fordata.frame(moot since R 4.0.0 where it's already FALSE).options(expressions = 5000): max nested evaluations. Increase for deep recursion.options(max.print = 99999): controls truncation inprintoutput.options(na.action = "na.omit"): default NA handling in model functions.options(contrasts = c("contr.treatment", "contr.poly")): default contrasts for unordered/ordered factors.
file.path / basename / dirname
file.path("a", "b", "c.txt")→"a/b/c.txt"(platform-appropriate separator).basename("/a/b/c.txt")→"c.txt".dirname("/a/b/c.txt")→"/a/b".file.pathdoes NOT normalize paths (no..resolution); usenormalizePath()for that.
list.files
list.files(pattern = "*.csv")—patternis a regex, not a glob! Useglob2rx("*.csv")or"\\.csv$".full.names = FALSE(default) returns basenames only. Usefull.names = TRUEfor complete paths.recursive = TRUEto search subdirectories.all.files = TRUEto include hidden files (starting with.).
file.info
- Returns data frame with
size,isdir,mode,mtime,ctime,atime,uid,gid. mtime: modification time (POSIXct). Useful forfile.info(f)$mtime.- On some filesystems,
ctimeis status-change time, not creation time.
file_test
file_test("-f", path): TRUE if regular file exists.file_test("-d", path): TRUE if directory exists.file_test("-nt", f1, f2): TRUE if f1 is newer than f2.- More reliable than
file.exists()for distinguishing files from directories. FILE:references/io-and-text.md
I/O and Text Processing — Quick Reference
Non-obvious behaviors, gotchas, and tricky defaults for R functions. Only what Claude doesn't already know.
read.table (gotchas)
sep = ""(default) means any whitespace (spaces, tabs, newlines) — not a literal empty string.comment.char = "#"by default — lines with#are truncated. Usecomment.char = ""to disable (also faster).headerauto-detection: set to TRUE if first row has one fewer field than subsequent rows (the missing field is assumed to be row names).colClasses = "NULL"skips that column entirely — very useful for speed.read.csvdefaults differ fromread.table:header = TRUE,sep = ",",fill = TRUE,comment.char = "".- For large files: specifying
colClassesandnrowsdramatically reduces memory usage.read.tableis slow for wide data frames (hundreds of columns); usescanordata.table::freadfor matrices. stringsAsFactors = FALSEsince R 4.0.0 (was TRUE before).
write.table (gotchas)
row.names = TRUEby default — produces an unnamed first column that confuses re-reading. Userow.names = FALSEorcol.names = NAfor Excel-compatible CSV.write.csvfixessep = ",",dec = ".", and usesqmethod = "double"— cannot override these via....quote = TRUE(default) quotes character/factor columns. Numeric columns are never quoted.- Matrix-like columns in data frames expand to multiple columns silently.
- Slow for data frames with many columns (hundreds+); each column processed separately by class.
read.fwf
- Reads fixed-width format files.
widthsis a vector of field widths. - Negative widths skip that many characters (useful for ignoring fields).
buffersizecontrols how many lines are read at a time; increase for large files.- Uses
read.tableinternally after splitting fields.
count.fields
- Counts fields per line in a file — useful for diagnosing read errors.
sepandquotearguments match those ofread.table.
grep / grepl / sub / gsub (gotchas)
- Three regex modes: POSIX extended (default),
perl = TRUE,fixed = TRUE. They behave differently for edge cases. - Name arguments explicitly — unnamed args after
x/patternare matched positionally toignore.case,perl, etc. Common source of silent bugs. subreplaces first match only;gsubreplaces all matches.- Backreferences:
"\\1"in replacement (double backslash in R strings). Withperl = TRUE:"\\U\\1"for uppercase conversion. grep(value = TRUE)returns matching elements;grep(value = FALSE)(default) returns indices.greplreturns logical vector — preferred for filtering.regexprreturns first match position + length (as attributes);gregexprreturns all matches as a list.regexecreturns match + capture group positions;gregexecdoes this for all matches.- Character classes like
[:alpha:]must be inside[[:alpha:]](double brackets) in POSIX mode.
strsplit
- Returns a list (one element per input string), even for a single string.
split = ""orsplit = character(0)splits into individual characters.- Match at beginning of string: first element of result is
"". Match at end: no trailing"". fixed = TRUEis faster and avoids regex interpretation.- Common mistake: unnamed arguments silently match
fixed,perl, etc.
substr / substring
substr(x, start, stop): extracts/replaces substring. 1-indexed, inclusive on both ends.substring(x, first, last): same butlastdefaults to1000000L(effectively "to end"). Vectorized overfirst/last.- Assignment form:
substr(x, 1, 3) <- "abc"replaces in place (must be same length replacement).
trimws
which = "both"(default),"left", or"right".whitespace = "[ \\t\\r\\n]"— customizable regex for what counts as whitespace.
nchar
type = "bytes"counts bytes;type = "chars"(default) counts characters;type = "width"counts display width.nchar(NA)returnsNA(not 2).nchar(factor)works on the level labels.keepNA = TRUE(default since R 3.3.0); set toFALSEto count"NA"as 2 characters.
format / formatC
format(x, digits, nsmall):nsmallforces minimum decimal places.big.mark = ","adds thousands separator.formatC(x, format = "f", digits = 2): C-style formatting.format = "e"for scientific,"g"for general.formatreturns character vector; always right-justified by default (justify = "right").
type.convert
- Converts character vectors to appropriate types (logical, integer, double, complex, character).
as.is = TRUE(recommended): keeps characters as character, not factor.- Applied column-wise on data frames.
tryLogical = TRUE(R 4.3+) converts "TRUE"/"FALSE" columns.
Rscript
commandArgs(trailingOnly = TRUE)gets script arguments (excluding R/Rscript flags).#!line on Unix:/usr/bin/env Rscriptor full path.--vanillaor--no-init-fileto skip.Rprofileloading.- Exit code:
quit(status = 1)for error exit.
capture.output
- Captures output from
cat,print, or any expression that writes to stdout. file = NULL(default) returns character vector.file = "out.txt"writes directly to file.type = "message"captures stderr instead.
URLencode / URLdecode
URLencode(url, reserved = FALSE)by default does NOT encode reserved chars (/,?,&, etc.).- Set
reserved = TRUEto encode a URL component (query parameter value).
glob2rx
- Converts shell glob patterns to regex:
glob2rx("*.csv")→"^.*\\.csv$". - Useful with
list.files(pattern = glob2rx("data_*.RDS")). FILE:references/modeling.md
Modeling — Quick Reference
Non-obvious behaviors, gotchas, and tricky defaults for R functions. Only what Claude doesn't already know.
formula
Symbolic model specification gotchas.
I()is required to use arithmetic operators literally:y ~ x + I(x^2). WithoutI(),^means interaction crossing.*= main effects + interaction:a*bexpands toa + b + a:b.(a+b+c)^2= all main effects + all 2-way interactions (not squaring).-removes terms:(a+b+c)^2 - a:bdrops only thea:binteraction./means nesting:a/b=a + b %in% a=a + a:b..in formula means "all other columns in data" (interms.formulacontext) or "previous contents" (inupdate.formula).- Formula objects carry an environment used for variable lookup;
as.formula("y ~ x")usesparent.frame().
terms / model.matrix
model.matrixcreates the design matrix including dummy coding. Default contrasts:contr.treatmentfor unordered factors,contr.polyfor ordered.termsobject attributes:order(interaction order per term),intercept,factorsmatrix.- Column names from
model.matrixcan be surprising: e.g.,factorLevelNameconcatenation.
glm
- Default
family = gaussian(link = "identity")—glm()with nofamilysilently fits OLS (same aslm, but slower and with deviance-based output). - Common families:
binomial(link = "logit"),poisson(link = "log"),Gamma(link = "inverse"),inverse.gaussian(). binomialaccepts response as: 0/1 vector, logical, factor (second level = success), or 2-column matrixcbind(success, failure).weightsinglmmeans prior weights (not frequency weights) — for frequency weights, use the cbind trick or offset.predict.glm(type = "response")for predicted probabilities; defaulttype = "link"returns log-odds (for logistic) or log-rate (for Poisson).anova(glm_obj, test = "Chisq")for deviance-based tests;"F"is invalid for non-Gaussian families.- Quasi-families (
quasibinomial,quasipoisson) allow overdispersion — no AIC is computed. - Convergence:
control = glm.control(maxit = 100)if default 25 iterations isn't enough.
aov
aovis a wrapper aroundlmthat stores extra info for balanced ANOVA. For unbalanced designs, Type I SS (sequential) are computed — order of terms matters.- For Type III SS, use
car::Anova()or set contrasts tocontr.sum/contr.helmert. - Error strata for repeated measures:
aov(y ~ A*B + Error(Subject/B)). summary.aovgives ANOVA table;summary.lm(aov_obj)gives regression-style summary.
nls
- Requires good starting values in
start = list(...)or convergence fails. - Self-starting models (
SSlogis,SSasymp, etc.) auto-compute starting values. - Algorithm
"port"allows bounds on parameters (lower/upper). - If data fits too exactly (no residual noise), convergence check fails — use
control = list(scaleOffset = 1)or jitter data. weightsargument for weighted NLS;na.actionfor missing value handling.
step / add1
stepdoes stepwise model selection by AIC (default). Usek = log(n)for BIC.- Direction:
direction = "both"(default),"forward", or"backward". add1/drop1evaluate single-term additions/deletions;stepcalls these iteratively.scopeargument defines the upper/lower model bounds for search.stepmodifies the model object in place — can be slow for large models with many candidate terms.
predict.lm / predict.glm
predict.lmwithinterval = "confidence"gives CI for mean response;interval = "prediction"gives PI for new observation (wider).newdatamust have columns matching the original formula variables — factors must have the same levels.predict.glmwithtype = "response"gives predictions on the response scale (e.g., probabilities for logistic);type = "link"(default) gives on the link scale.se.fit = TRUEreturns standard errors; forpredict.glmthese are on the link scale regardless oftype.predict.lmwithtype = "terms"returns the contribution of each term.
loess
spancontrols smoothness (default 0.75). Span < 1 uses that proportion of points; span > 1 uses all points with adjusted distance.- Maximum 4 predictors. Memory usage is roughly quadratic in n (1000 points ~ 10MB).
degree = 0(local constant) is allowed but poorly tested — use with caution.- Not identical to S's
loess; conditioning is not implemented. normalize = TRUE(default) standardizes predictors to common scale; setFALSEfor spatial coords.
lowess vs loess
lowessis the older function; returnslist(x, y)— cannot predict at new points.loessis the newer formula interface withpredictmethod.lowessparameter isf(span, default 2/3);loessparameter isspan(default 0.75).lowessiterdefault is 3 (robustifying iterations);loessdefaultfamily = "gaussian"(no robustness).
smooth.spline
- Default smoothing parameter selected by GCV (generalized cross-validation).
cv = TRUEuses ordinary leave-one-out CV instead — do not use with duplicate x values.sparandlambdacontrol smoothness;dfcan specify equivalent degrees of freedom.- Returns object with
predict,print,plotmethods. Thefitcomponent has knots and coefficients.
optim
- Minimizes by default. To maximize: set
control = list(fnscale = -1). - Default method is Nelder-Mead (no gradients, robust but slow). Poor for 1D — use
"Brent"oroptimize(). "L-BFGS-B"is the only method supporting box constraints (lower/upper). Bounds auto-select this method with a warning."SANN"(simulated annealing): convergence code is always 0 — it never "fails".maxit= total function evals (default 10000), no other stopping criterion.parscale: scale parameters so unit change in each produces comparable objective change. Critical for mixed-scale problems.hessian = TRUE: returns numerical Hessian of the unconstrained problem even if box constraints are active.fncan returnNA/Inf(except"L-BFGS-B"which requires finite values always). Initial value must be finite.
optimize / uniroot
optimize: 1D minimization on a bounded interval. Returnsminimumandobjective.uniroot: finds a root offin[lower, upper]. Requiresf(lower)andf(upper)to have opposite signs.unirootwithextendInt = "yes"can auto-extend the interval to find sign change — but can find spurious roots for functions that don't actually cross zero.nlm: Newton-type minimizer. Gradient/Hessian as attributes of the return value fromfn(unusual interface).
TukeyHSD
- Requires a fitted
aovobject (notlm). - Default
conf.level = 0.95. Returns adjusted p-values and confidence intervals for all pairwise comparisons. - Only meaningful for balanced or near-balanced designs; can be liberal for very unbalanced data.
anova (for lm)
anova(model): sequential (Type I) SS — order of terms matters.anova(model1, model2): F-test comparing nested models.- For Type II or III SS use
car::Anova(). FILE:references/statistics.md
Statistics — Quick Reference
Non-obvious behaviors, gotchas, and tricky defaults for R functions. Only what Claude doesn't already know.
chisq.test
correct = TRUE(default) applies Yates continuity correction for 2x2 tables only.simulate.p.value = TRUE: Monte Carlo withB = 2000replicates (min p ~ 0.0005). Simulation assumes fixed marginals (Fisher-style sampling, not the chi-sq assumption).- For goodness-of-fit: pass a vector, not a matrix.
pmust sum to 1 (or setrescale.p = TRUE). - Return object includes
$expected,$residuals(Pearson), and$stdres(standardized).
wilcox.test
exact = TRUEby default for small samples with no ties. With ties, normal approximation used.correct = TRUEapplies continuity correction to normal approximation.conf.int = TRUEcomputes Hodges-Lehmann estimator and confidence interval (not just the p-value).- Paired test:
paired = TRUEuses signed-rank test (Wilcoxon), not rank-sum (Mann-Whitney).
fisher.test
- For tables larger than 2x2, uses simulation (
simulate.p.value = TRUE) or network algorithm. workspacecontrols memory for the network algorithm; increase if you get errors on large tables.orargument tests a specific odds ratio (default 1) — only for 2x2 tables.
ks.test
- Two-sample test or one-sample against a reference distribution.
- Does not handle ties well — warns and uses asymptotic approximation.
- For composite hypotheses (parameters estimated from data), p-values are conservative (too large). Use
dgoforks.testwithexact = NULLfor discrete distributions.
p.adjust
- Methods:
"holm"(default),"BH"(Benjamini-Hochberg FDR),"bonferroni","BY","hochberg","hommel","fdr"(alias for BH),"none". nargument: total number of hypotheses (can be larger thanlength(p)if some p-values are excluded).- Handles
NAs: adjusted p-values areNAwhere input isNA.
pairwise.t.test / pairwise.wilcox.test
p.adjust.methoddefaults to"holm". Change to"BH"for FDR control.pool.sd = TRUE(default for t-test): uses pooled SD across all groups (assumes equal variances).- Returns a matrix of p-values, not test statistics.
shapiro.test
- Sample size must be between 3 and 5000.
- Tests normality; low p-value = evidence against normality.
kmeans
nstart > 1recommended (e.g.,nstart = 25): runs algorithm from multiple random starts, returns best.- Default
iter.max = 10— may be too low for convergence. Increase for large/complex data. - Default algorithm is "Hartigan-Wong" (generally best). Very close points may cause non-convergence (warning with
ifault = 4). - Cluster numbering is arbitrary; ordering may differ across platforms.
- Always returns k clusters when k is specified (except Lloyd-Forgy may return fewer).
hclust
method = "ward.D2"implements Ward's criterion correctly (using squared distances). The older"ward.D"did not square distances (retained for back-compatibility).- Input must be a
distobject. Useas.dist()to convert a symmetric matrix. hang = -1inplot()aligns all labels at the bottom.
dist
method = "euclidean"(default). Other options:"manhattan","maximum","canberra","binary","minkowski".- Returns a
distobject (lower triangle only). Useas.matrix()to get full matrix. "canberra": terms with zero numerator and denominator are omitted from the sum (not treated as 0/0).Infvalues: Euclidean distance involvingInfisInf. MultipleInfs in same obs giveNaNfor some methods.
prcomp vs princomp
prcompuses SVD (numerically superior);princompuseseigenon covariance (less stable, N-1 vs N scaling).scale. = TRUEinprcompstandardizes variables; important when variables have very different scales.princompstandard deviations differ fromprcompby factorsqrt((n-1)/n).- Both return
$rotation(loadings) and$x(scores); sign of components may differ between runs.
density
- Default bandwidth:
bw = "nrd0"(Silverman's rule of thumb). For multimodal data, consider"SJ"or"bcv". adjust: multiplicative factor on bandwidth.adjust = 0.5halves the bandwidth (less smooth).- Default kernel:
"gaussian". Range of density extends beyond data range (controlled bycut, default 3 bandwidths). n = 512: number of evaluation points. Increase for smoother plotting.from/to: explicitly bound the evaluation range.
quantile
- Nine
typeoptions (1-9). Defaulttype = 7(R default, linear interpolation). Type 1 = inverse of empirical CDF (SAS default). Types 4-9 are continuous; 1-3 are discontinuous. na.rm = FALSEby default — returns NA if any NAs present.names = TRUEby default, adding "0%", "25%", etc. as names.
Distributions (gotchas across all)
All distribution functions follow the d/p/q/r pattern. Common non-obvious points:
nargument inr*()functions: iflength(n) > 1, useslength(n)as the count, notnitself. Sornorm(c(1,2,3))generates 3 values, not 1+2+3.log = TRUE/log.p = TRUE: compute on log scale for numerical stability in tails.lower.tail = FALSEgives survival function P(X > x) directly (more accurate than 1 - pnorm() in tails).- Gamma: parameterized by
shapeandrate(= 1/scale). Defaultrate = 1. Specifying bothrateandscaleis an error. - Beta:
shape1(alpha),shape2(beta) — nomean/sdparameterization. - Poisson
dpois:xcan be non-integer (returns 0 with a warning for non-integer values iflog = FALSE). - Weibull:
shapeandscale(norate). R's parameterization:f(x) = (shape/scale)(x/scale)^(shape-1) exp(-(x/scale)^shape). - Lognormal:
meanlogandsdlogare mean/sd of the log, not of the distribution itself.
cor.test
- Default method:
"pearson". Also"kendall"and"spearman". - Returns
$estimate,$p.value,$conf.int(CI only for Pearson). - Formula interface:
cor.test(~ x + y, data = df)— note the~with no LHS.
ecdf
- Returns a function (step function). Call it on new values:
Fn <- ecdf(x); Fn(3.5). plot(ecdf(x))gives the empirical CDF plot.- The returned function is right-continuous with left limits (cadlag).
weighted.mean
- Handles
NAin weights: observation is dropped if weight isNA. - Weights do not need to sum to 1; they are normalized internally. FILE:references/visualization.md
Visualization — Quick Reference
Non-obvious behaviors, gotchas, and tricky defaults for R functions. Only what Claude doesn't already know.
par (gotchas)
par()settings are per-device. Opening a new device resets everything.- Setting
mfrow/mfcolresetscexto 1 andmexto 1. With 2x2 layout, basecexis multiplied by 0.83; with 3+ rows/columns, by 0.66. mai(inches),mar(lines),pin,plt,ptyall interact. Restoring all saved parameters after device resize can produce inconsistent results — last-alphabetically wins.bgset viapar()also setsnew = FALSE. Settingfgviapar()also setscol.xpd = NAclips to device region (allows drawing in outer margins);xpd = TRUEclips to figure region;xpd = FALSE(default) clips to plot region.mgp = c(3, 1, 0): controls title line (mgp[1]), label line (mgp[2]), axis line (mgp[3]). All inmexunits.las: 0 = parallel to axis, 1 = horizontal, 2 = perpendicular, 3 = vertical. Does not respond tosrt.tck = 1draws grid lines across the plot.tcl = -0.5(default) gives outward ticks.usrwith log scale: contains log10 of the coordinate limits, not the raw values.- Read-only parameters:
cin,cra,csi,cxy,din,page.
layout
layout(mat)wherematis a matrix of integers specifying figure arrangement.widths/heightsacceptlcm()for absolute sizes mixed with relative sizes.- More flexible than
mfrow/mfcolbut cannot be queried once set (unlikepar("mfrow")). layout.show(n)visualizes the layout for debugging.
axis / mtext
axis(side, at, labels):side1=bottom, 2=left, 3=top, 4=right.- Default gap between axis labels controlled by
par("mgp"). Labels can overlap if not managed. mtext:lineargument positions text in margin lines (0 = adjacent to plot, positive = outward).adjcontrols horizontal position (0-1).mtextwithouter = TRUEwrites in the outer margin (set bypar(oma = ...)).
curve
- First argument can be an expression in
xor a function:curve(sin, 0, 2*pi)orcurve(x^2 + 1, 0, 10). add = TRUEto overlay on existing plot. Defaultn = 101evaluation points.xname = "x"by default; change if your expression uses a different variable name.
pairs
panelfunction receives(x, y, ...)for each pair.lower.panel,upper.panel,diag.panelfor different regions.gapcontrols spacing between panels (default 1).- Formula interface:
pairs(~ var1 + var2 + var3, data = df).
coplot
- Conditioning plots:
coplot(y ~ x | a)orcoplot(y ~ x | a * b)for two conditioning variables. panelfunction can be customized;rows/columnscontrol layout.- Default panel draws points; use
panel = panel.smoothfor loess overlay.
matplot / matlines / matpoints
- Plots columns of one matrix against columns of another. Recycles
col,lty,pchacross columns. type = "l"by default (unlikeplotwhich defaults to"p").- Useful for plotting multiple time series or fitted curves simultaneously.
contour / filled.contour / image
contour(x, y, z):zmust be a matrix withdim = c(length(x), length(y)).filled.contourhas a non-standard layout — it creates its own plot region for the color key. Cannot usepar(mfrow)with it. Adding elements requires theplot.axesargument.image: plots z-values as colored rectangles. Default color scheme may be misleading; setcolexplicitly.- For
image,xandyspecify cell boundaries or midpoints depending on context.
persp
persp(x, y, z, theta, phi):theta= azimuthal angle,phi= colatitude.- Returns a transformation matrix (invisible) for projecting 3D to 2D — use
trans3d()to add points/lines to the perspective plot. shadeandcolcontrol surface shading.border = NAremoves grid lines.
segments / arrows / rect / polygon
- All take vectorized coordinates; recycle as needed.
arrows:code = 1(head at start),code = 2(head at end, default),code = 3(both).polygon: last point auto-connects to first. Fill withcol;bordercontrols outline.rect(xleft, ybottom, xright, ytop)— note argument order is not the same as other systems.
dev / dev.off / dev.copy
dev.new()opens a new device.dev.off()closes current device (and flushes output for file devices likepdf).dev.off()on the last open device reverts to null device.dev.copy(pdf, file = "plot.pdf")followed bydev.off()to save current plot.dev.list()returns all open devices;dev.cur()the active one.
- Must call
dev.off()to finalize the file. Without it, file may be empty/corrupt. onefile = TRUE(default): multiple pages in one PDF.onefile = FALSE: one file per page (uses%din filename for numbering).useDingbats = FALSErecommended to avoid issues with certain PDF viewers and pch symbols.- Default size: 7x7 inches.
familycontrols font family.
png / bitmap devices
rescontrols DPI (default 72). For publication:res = 300with appropriatewidth/heightin pixels or inches (withunits = "in").type = "cairo"(on systems with cairo) gives better antialiasing than default.bg = "transparent"for transparent background (PNG supports alpha).
colors / rgb / hcl / col2rgb
colors()returns all 657 named colors.col2rgb("color")returns RGB matrix.rgb(r, g, b, alpha, maxColorValue = 255)— notemaxColorValuedefault is 1, not 255.hcl(h, c, l): perceptually uniform color space. Preferred for color scales.adjustcolor(col, alpha.f = 0.5): easy way to add transparency.
colorRamp / colorRampPalette
colorRampreturns a function mapping [0,1] to RGB matrix.colorRampPalettereturns a function takingnand returningninterpolated colors.space = "Lab"gives more perceptually uniform interpolation than"rgb".
palette / recordPlot
palette()returns current palette (default 8 colors).palette("Set1")sets a built-in palette.- Integer colors in plots index into the palette (with wrapping). Index 0 = background color.
recordPlot()/replayPlot(): save and restore a complete plot — device-dependent and fragile across sessions. FILE:assets/analysis_template.R
============================================================
Analysis Template — Base R
Copy this file, rename it, and fill in your details.
============================================================
Author :
Date :
Data :
Purpose :
============================================================
── 0. Setup ─────────────────────────────────────────────────
Clear environment (optional — comment out if loading into existing session)
rm(list = ls())
Set working directory if needed
setwd("/path/to/your/project")
Reproducibility
set.seed(42)
Libraries — uncomment what you need
library(haven) # read .dta / .sav / .sas
library(readxl) # read Excel files
library(openxlsx) # write Excel files
library(foreign) # older Stata / SPSS formats
library(survey) # survey-weighted analysis
library(lmtest) # Breusch-Pagan, Durbin-Watson etc.
library(sandwich) # robust standard errors
library(car) # Type II/III ANOVA, VIF
── 1. Load Data ─────────────────────────────────────────────
df <- read.csv("your_data.csv", stringsAsFactors = FALSE)
df <- readRDS("your_data.rds")
df <- haven::read_dta("your_data.dta")
First look — always run these
dim(df) str(df) head(df, 10) summary(df)
── 2. Data Quality Check ────────────────────────────────────
Missing values
na_report <- data.frame( column = names(df), n_miss = colSums(is.na(df)), pct_miss = round(colMeans(is.na(df)) * 100, 1), row.names = NULL ) print(na_report[na_report$n_miss > 0, ])
Duplicates
n_dup <- sum(duplicated(df)) cat(sprintf("Duplicate rows: %d\n", n_dup))
Unique values for categorical columns
cat_cols <- names(df)[sapply(df, function(x) is.character(x) | is.factor(x))] for (col in cat_cols) { cat(sprintf("\n%s (%d unique):\n", col, length(unique(df[[col]])))) print(table(df[[col]], useNA = "ifany")) }
── 3. Clean & Transform ─────────────────────────────────────
Rename columns (example)
names(df)[names(df) == "old_name"] <- "new_name"
Convert types
df$group <- as.factor(df$group)
df$date <- as.Date(df$date, format = "%Y-%m-%d")
Recode values (example)
df$gender <- ifelse(df$gender == 1, "Male", "Female")
Create new variables (example)
df$log_income <- log(df$income + 1)
df$age_group <- cut(df$age,
breaks = c(0, 25, 45, 65, Inf),
labels = c("18-25", "26-45", "46-65", "65+"))
Filter rows (example)
df <- df[df$year >= 2010, ]
df <- df[complete.cases(df[, c("outcome", "predictor")]), ]
Drop unused factor levels
df <- droplevels(df)
── 4. Descriptive Statistics ────────────────────────────────
Numeric summary
num_cols <- names(df)[sapply(df, is.numeric)] round(sapply(df[num_cols], function(x) c( n = sum(!is.na(x)), mean = mean(x, na.rm = TRUE), sd = sd(x, na.rm = TRUE), median = median(x, na.rm = TRUE), min = min(x, na.rm = TRUE), max = max(x, na.rm = TRUE) )), 3)
Cross-tabulation
table(df$group, df$category, useNA = "ifany")
prop.table(table(df$group, df$category), margin = 1) # row proportions
── 5. Visualization (EDA) ───────────────────────────────────
par(mfrow = c(2, 2))
Histogram of main outcome
hist(df$outcome_var, main = "Distribution of Outcome", xlab = "Outcome", col = "steelblue", border = "white", breaks = 30)
Boxplot by group
boxplot(outcome_var ~ group_var, data = df, main = "Outcome by Group", col = "lightyellow", las = 2)
Scatter plot
plot(df$predictor, df$outcome_var, main = "Predictor vs Outcome", xlab = "Predictor", ylab = "Outcome", pch = 19, col = adjustcolor("steelblue", alpha.f = 0.5), cex = 0.8) abline(lm(outcome_var ~ predictor, data = df), col = "red", lwd = 2)
Correlation matrix (numeric columns only)
cor_mat <- cor(df[num_cols], use = "complete.obs") image(cor_mat, main = "Correlation Matrix", col = hcl.colors(20, "RdBu", rev = TRUE))
par(mfrow = c(1, 1))
── 6. Analysis ───────────────────────────────────────────────
·· 6a. Comparison of means ··
t.test(outcome_var ~ group_var, data = df)
·· 6b. Linear regression ··
fit <- lm(outcome_var ~ predictor1 + predictor2 + group_var, data = df) summary(fit) confint(fit)
Check VIF for multicollinearity (requires car)
car::vif(fit)
Robust standard errors (requires lmtest + sandwich)
lmtest::coeftest(fit, vcov = sandwich::vcovHC(fit, type = "HC3"))
·· 6c. ANOVA ··
fit_aov <- aov(outcome_var ~ group_var, data = df)
summary(fit_aov)
TukeyHSD(fit_aov)
·· 6d. Logistic regression (binary outcome) ··
fit_logit <- glm(binary_outcome ~ x1 + x2,
data = df,
family = binomial(link = "logit"))
summary(fit_logit)
exp(coef(fit_logit)) # odds ratios
exp(confint(fit_logit)) # OR confidence intervals
── 7. Model Diagnostics ─────────────────────────────────────
par(mfrow = c(2, 2)) plot(fit) par(mfrow = c(1, 1))
Residual normality
shapiro.test(residuals(fit))
Homoscedasticity (requires lmtest)
lmtest::bptest(fit)
── 8. Save Output ────────────────────────────────────────────
Cleaned data
write.csv(df, "data_clean.csv", row.names = FALSE)
saveRDS(df, "data_clean.rds")
Model results to text file
sink("results.txt")
cat("=== Linear Model ===\n")
print(summary(fit))
cat("\n=== Confidence Intervals ===\n")
print(confint(fit))
sink()
Plots to file
png("figure1_distributions.png", width = 1200, height = 900, res = 150)
par(mfrow = c(2, 2))
# ... your plots ...
par(mfrow = c(1, 1))
dev.off()
============================================================
END OF TEMPLATE
============================================================
FILE:scripts/check_data.R
check_data.R — Quick data quality report for any R data frame
Usage: source("check_data.R") then call check_data(df)
Or: source("check_data.R"); check_data(read.csv("yourfile.csv"))
check_data <- function(df, top_n_levels = 8) {
if (!is.data.frame(df)) stop("Input must be a data frame.")
n_row <- nrow(df) n_col <- ncol(df)
cat("══════════════════════════════════════════\n") cat(" DATA QUALITY REPORT\n") cat("══════════════════════════════════════════\n") cat(sprintf(" Rows: %d Columns: %d\n", n_row, n_col)) cat("══════════════════════════════════════════\n\n")
── 1. Column overview ──────────────────────
cat("── COLUMN OVERVIEW ────────────────────────\n")
for (col in names(df)) { x <- df[[col]] cls <- class(x)[1] n_na <- sum(is.na(x)) pct <- round(n_na / n_row * 100, 1) n_uniq <- length(unique(x[!is.na(x)]))
na_flag <- if (n_na == 0) "" else sprintf(" *** %d NAs (%.1f%%)", n_na, pct)
cat(sprintf(" %-20s %-12s %d unique%s\n",
col, cls, n_uniq, na_flag))
}
── 2. NA summary ────────────────────────────
cat("\n── NA SUMMARY ─────────────────────────────\n")
na_counts <- sapply(df, function(x) sum(is.na(x))) cols_with_na <- na_counts[na_counts > 0]
if (length(cols_with_na) == 0) { cat(" No missing values. \n") } else { cat(sprintf(" Columns with NAs: %d of %d\n\n", length(cols_with_na), n_col)) for (col in names(cols_with_na)) { bar_len <- round(cols_with_na[col] / n_row * 20) bar <- paste0(rep("█", bar_len), collapse = "") pct_na <- round(cols_with_na[col] / n_row * 100, 1) cat(sprintf(" %-20s [%-20s] %d (%.1f%%)\n", col, bar, cols_with_na[col], pct_na)) } }
── 3. Numeric columns ───────────────────────
num_cols <- names(df)[sapply(df, is.numeric)]
if (length(num_cols) > 0) { cat("\n── NUMERIC COLUMNS ────────────────────────\n") cat(sprintf(" %-20s %8s %8s %8s %8s %8s\n", "Column", "Min", "Mean", "Median", "Max", "SD")) cat(sprintf(" %-20s %8s %8s %8s %8s %8s\n", "──────", "───", "────", "──────", "───", "──"))
for (col in num_cols) {
x <- df[[col]][!is.na(df[[col]])]
if (length(x) == 0) next
cat(sprintf(" %-20s %8.3g %8.3g %8.3g %8.3g %8.3g\n",
col,
min(x), mean(x), median(x), max(x), sd(x)))
}
}
── 4. Factor / character columns ───────────
cat_cols <- names(df)[sapply(df, function(x) is.factor(x) | is.character(x))]
if (length(cat_cols) > 0) { cat("\n── CATEGORICAL COLUMNS ────────────────────\n")
for (col in cat_cols) {
x <- df[[col]]
tbl <- sort(table(x, useNA = "no"), decreasing = TRUE)
n_lv <- length(tbl)
cat(sprintf("\n %s (%d unique values)\n", col, n_lv))
show <- min(top_n_levels, n_lv)
for (i in seq_len(show)) {
lbl <- names(tbl)[i]
cnt <- tbl[i]
pct <- round(cnt / n_row * 100, 1)
cat(sprintf(" %-25s %5d (%.1f%%)\n", lbl, cnt, pct))
}
if (n_lv > top_n_levels) {
cat(sprintf(" ... and %d more levels\n", n_lv - top_n_levels))
}
}
}
── 5. Duplicate rows ────────────────────────
cat("\n── DUPLICATES ─────────────────────────────\n") n_dup <- sum(duplicated(df)) if (n_dup == 0) { cat(" No duplicate rows.\n") } else { cat(sprintf(" %d duplicate row(s) found (%.1f%% of data)\n", n_dup, n_dup / n_row * 100)) }
cat("\n══════════════════════════════════════════\n") cat(" END OF REPORT\n") cat("══════════════════════════════════════════\n")
Return invisibly for programmatic use
invisible(list( dims = c(rows = n_row, cols = n_col), na_counts = na_counts, n_dupes = n_dup )) } FILE:scripts/scaffold_analysis.R #!/usr/bin/env Rscript
scaffold_analysis.R — Generates a starter analysis script
Usage (from terminal):
Rscript scaffold_analysis.R myproject
Rscript scaffold_analysis.R myproject outcome_var group_var
Usage (from R console):
source("scaffold_analysis.R")
scaffold_analysis("myproject", outcome = "score", group = "treatment")
Output: myproject_analysis.R (ready to edit)
scaffold_analysis <- function(project_name, outcome = "outcome", group = "group", data_file = NULL) {
if (is.null(data_file)) data_file <- paste0(project_name, ".csv") out_file <- paste0(project_name, "_analysis.R")
template <- sprintf( '# ============================================================
Project : %s
Created : %s
============================================================
── 0. Libraries ─────────────────────────────────────────────
Add packages you need here
library(ggplot2)
library(haven) # for .dta files
library(openxlsx) # for Excel output
── 1. Load Data ─────────────────────────────────────────────
df <- read.csv("%s", stringsAsFactors = FALSE)
Quick check — always do this first
cat("Dimensions:", dim(df), "\n") str(df) head(df)
── 2. Explore / EDA ─────────────────────────────────────────
summary(df)
NA check
na_counts <- colSums(is.na(df)) na_counts[na_counts > 0]
Key variable distributions
hist(df$%s, main = "Distribution of %s", xlab = "%s")
if ("%s" %%in%% names(df)) { table(df$%s) barplot(table(df$%s), main = "Counts by %s", col = "steelblue", las = 2) }
── 3. Clean / Transform ──────────────────────────────────────
df <- df[complete.cases(df), ] # drop rows with any NA
df$%s <- as.factor(df$%s) # convert to factor
── 4. Analysis ───────────────────────────────────────────────
Descriptive stats by group
tapply(df$%s, df$%s, mean, na.rm = TRUE) tapply(df$%s, df$%s, sd, na.rm = TRUE)
t-test (two groups)
t.test(%s ~ %s, data = df)
Linear model
fit <- lm(%s ~ %s, data = df) summary(fit) confint(fit)
ANOVA (multiple groups)
fit_aov <- aov(%s ~ %s, data = df)
summary(fit_aov)
TukeyHSD(fit_aov)
── 5. Visualize Results ──────────────────────────────────────
par(mfrow = c(1, 2))
Boxplot by group
boxplot(%s ~ %s, data = df, main = "%s by %s", xlab = "%s", ylab = "%s", col = "lightyellow")
Model diagnostics
plot(fit, which = 1) # residuals vs fitted
par(mfrow = c(1, 1))
── 6. Save Output ────────────────────────────────────────────
Save cleaned data
write.csv(df, "%s_clean.csv", row.names = FALSE)
Save model summary to text
sink("%s_results.txt")
summary(fit)
sink()
Save plot to file
png("%s_boxplot.png", width = 800, height = 600, res = 150)
boxplot(%s ~ %s, data = df, col = "lightyellow")
dev.off()
', project_name, format(Sys.Date(), "%%Y-%%m-%%d"), data_file, # Section 2 — EDA outcome, outcome, outcome, group, group, group, group, # Section 3 group, group, # Section 4 outcome, group, outcome, group, outcome, group, outcome, group, outcome, group, outcome, group, # Section 5 outcome, group, outcome, group, group, outcome, # Section 6 project_name, project_name, project_name, outcome, group )
writeLines(template, out_file) cat(sprintf("Created: %s\n", out_file)) invisible(out_file) }
── Run from command line ─────────────────────────────────────
if (!interactive()) { args <- commandArgs(trailingOnly = TRUE)
if (length(args) == 0) { cat("Usage: Rscript scaffold_analysis.R <project_name> [outcome_var] [group_var]\n") cat("Example: Rscript scaffold_analysis.R myproject score treatment\n") quit(status = 1) }
project <- args[1] outcome <- if (length(args) >= 2) args[2] else "outcome" group <- if (length(args) >= 3) args[3] else "group"
scaffold_analysis(project, outcome = outcome, group = group) } FILE:README.md
base-r-skill
GitHub: https://github.com/iremaydas/base-r-skill
A Claude Code skill for base R programming.
The Story
I'm a political science PhD candidate who uses R regularly but would never call myself an R person. I needed a Claude Code skill for base R — something without tidyverse, without ggplot2, just plain R — and I couldn't find one anywhere.
So I made one myself. At 11pm. Asking Claude to help me build a skill for Claude.
If you're also someone who Googles how to drop NA rows in R every single time, this one's for you. 🫶
What's Inside
base-r/
├── SKILL.md # Main skill file
├── references/ # Gotchas & non-obvious behaviors
│ ├── data-wrangling.md # Subsetting traps, apply family, merge, factor quirks
│ ├── modeling.md # Formula syntax, lm/glm/aov/nls, optim
│ ├── statistics.md # Hypothesis tests, distributions, clustering
│ ├── visualization.md # par, layout, devices, colors
│ ├── io-and-text.md # read.table, grep, regex, format
│ ├── dates-and-system.md # Date/POSIXct traps, options(), file ops
│ └── misc-utilities.md # tryCatch, do.call, time series, utilities
├── scripts/
│ ├── check_data.R # Quick data quality report for any data frame
│ └── scaffold_analysis.R # Generates a starter analysis script
└── assets/
└── analysis_template.R # Copy-paste analysis template
The reference files were condensed from the official R 4.5.3 manual — 19,518 lines → 945 lines (95% reduction). Only the non-obvious stuff survived: gotchas, surprising defaults, tricky interactions. The things Claude already knows well got cut.
How to Use
Add this skill to your Claude Code setup by pointing to this repo. Then Claude will automatically load the relevant reference files when you're working on R tasks.
Works best for:
- Base R data manipulation (no tidyverse)
- Statistical modeling with
lm,glm,aov - Base graphics with
plot,par,barplot - Understanding why your R code is doing that weird thing
Not for: tidyverse, ggplot2, Shiny, or R package development.
The check_data.R Script
Probably the most useful standalone thing here. Source it and run check_data(df) on any data frame to get a formatted report of dimensions, NA counts, numeric summaries, and categorical breakdowns.
source("scripts/check_data.R")
check_data(your_df)
Built With Help From
- Claude (obviously)
- The official R manuals (all 19,518 lines of them)
- Mild frustration and several cups of coffee
Contributing
If you spot a missing gotcha, a wrong default, or something that should be in the references — PRs are very welcome. I'm learning too.
Made by @iremaydas — PhD candidate, occasional R user, full-time Googler of things I should probably know by now.