diff --git a/PROMPTS.md b/PROMPTS.md
index 3bb6a95f..98f3834c 100644
--- a/PROMPTS.md
+++ b/PROMPTS.md
@@ -110644,3 +110644,1894 @@ When you ask for help, the agent responds with:
+
+base-R
+
+## base-R
+
+Contributed by [@iremaydas](https://github.com/iremaydas)
+
+```md
+---
+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 installation
+---
+
+# 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
+
+```r
+# 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
+
+```r
+# 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
+
+```r
+# 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
+
+```r
+# 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
+
+```r
+# 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
+
+```r
+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
+
+```r
+# 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
+
+```r
+# 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
+
+```r
+# 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
+
+```r
+# 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
+
+```r
+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`/`r` prefixes).
+- **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()` over `sapply()` in production code — it enforces return types
+- Prefer `seq_along(x)` over `1:length(x)` — the latter breaks when `x` is empty
+- Use `stringsAsFactors = FALSE` in `read.csv()` / `data.frame()` (default changed in R 4.0)
+- Vectorize operations instead of writing loops when possible
+- Use `stop()`, `warning()`, `message()` for error handling — not `print()`
+- `<<-` assigns to parent environment — use sparingly and intentionally
+- `with(df, expr)` avoids repeating `df$` everywhere
+- `Sys.setenv()` and `.Renviron` for 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)` — `args` must be a **list**, even for a single argument.
+- `quote = TRUE` prevents evaluation of arguments before the call — needed when passing expressions/symbols.
+- Behavior of `substitute` inside `do.call` differs 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 function `f` cumulatively: `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's `itertools.accumulate`.
+- `Reduce(f, x, right = TRUE)` folds from the right: `f(x1, f(x2, f(x3, x4)))`.
+- `Reduce` with `init` adds a starting value: `Reduce(f, x, init = v)` = `f(f(f(v, x1), x2), x3)`.
+- `Filter(f, x)` keeps elements where `f(elem)` is `TRUE`. Unlike `x[sapply(x, f)]`, handles `NULL`/empty correctly.
+- `Map(f, ...)` is a simple wrapper for `mapply(f, ..., SIMPLIFY = FALSE)` — always returns a list.
+- `Find(f, x)` returns the **first** element where `f(elem)` is `TRUE`. `Find(f, x, right = TRUE)` for last.
+- `Position(f, x)` returns the **index** of the first match (like `Find` but returns position, not value).
+
+---
+
+## lengths
+
+- `lengths(x)` returns the length of **each element** of a list. Equivalent to `sapply(x, length)` but faster (implemented in C).
+- Works on any list-like object. Returns integer vector.
+
+---
+
+## conditions (tryCatch / withCallingHandlers)
+
+- `tryCatch` **unwinds** the call stack — handler runs in the calling environment, not where the error occurred. Cannot resume execution.
+- `withCallingHandlers` does 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. Use `withCallingHandlers` + `invokeRestart("muffleWarning")` to suppress warnings but continue.
+- `tryCatch` `finally` clause 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 with `tryCatch(..., myError = function(e) ...)`.
+
+---
+
+## all.equal
+
+- Tests **near equality** with tolerance (default `1.5e-8`, i.e., `sqrt(.Machine$double.eps)`).
+- Returns `TRUE` or a **character string** describing the difference — NOT `FALSE`. Use `isTRUE(all.equal(x, y))` in conditionals.
+- `tolerance` argument controls numeric tolerance. `scale` for absolute vs relative comparison.
+- Checks attributes, names, dimensions — more thorough than `==`.
+
+---
+
+## combn
+
+- `combn(n, m)` or `combn(x, m)`: generates all combinations of `m` items from `x`.
+- Returns a **matrix** with `m` rows; each column is one combination.
+- `FUN` argument applies a function to each combination: `combn(5, 3, sum)` returns sums of all 3-element subsets.
+- `simplify = FALSE` returns a list instead of a matrix.
+
+---
+
+## modifyList
+
+- `modifyList(x, val)` replaces elements of list `x` with those in `val` by **name**.
+- Setting a value to `NULL` **removes** that element from the list.
+- **Does** add new names not in `x` — it uses `x[names(val)] <- val` internally, so any name in `val` gets added or replaced.
+
+---
+
+## relist
+
+- Inverse of `unlist`: given a flat vector and a skeleton list, reconstructs the nested structure.
+- `relist(flesh, skeleton)` — `flesh` is the flat data, `skeleton` provides 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 with `close(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). Use `find.package()` or `requireNamespace()` to check for a specific package.
+- `update.packages(ask = FALSE)` updates all packages without prompting.
+- `lib.loc` specifies 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)`: `frequency` is observations per unit time (12 for monthly, 4 for quarterly).
+- `acf` default `type = "correlation"`. Use `type = "partial"` for PACF. `plot = FALSE` to 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.
+- `arima` handles `NA` values in the time series (via Kalman filter).
+- `stl` requires `s.window` (seasonal window) — must be specified, no default. `s.window = "periodic"` assumes fixed seasonality.
+- `decompose`: simpler than `stl`, uses moving averages. `type = "additive"` or `"multiplicative"`.
+- `stl` result components: `$time.series` matrix with columns `seasonal`, `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]` is `m[2, 1]` not `m[1, 2]` — argument names are **ignored** in `[`, positional matching only. Never name index args.
+- Factor indexing: `x[f]` uses integer codes of factor `f`, not its character labels. Use `x[as.character(f)]` for label-based indexing.
+- `x[[]]` with no index is always an error. `x$name` does partial matching by default; `x[["name"]]` does not (exact by default).
+- Assigning `NULL` via `x[[i]] <- NULL` or `x$name <- NULL` **deletes** that list element.
+- Data frame `[` with single column: `df[, 1]` returns a **vector** (drop=TRUE default for columns), but `df[1, ]` returns a **data frame** (drop=FALSE for rows). Use `drop = FALSE` explicitly.
+- Matrix indexing a data frame (`df[cbind(i,j)]`) coerces to matrix first — avoid.
+
+---
+
+## subset
+
+Use interactively only; unsafe for programming.
+
+- `subset` argument 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.
+- `NA`s in the logical condition are treated as `FALSE` (rows silently dropped).
+- Factors may retain unused levels after subsetting; call `droplevels()`.
+
+---
+
+## match / %in%
+
+- `%in%` **never returns NA** — this makes it safe for `if()` conditions unlike `==`.
+- `match()` returns position of **first** match only; duplicates in `table` are ignored.
+- Factors, raw vectors, and lists are all converted to character before matching.
+- `NaN` matches `NaN` but not `NA`; `NA` matches `NA` only.
+
+---
+
+## apply
+
+- On a **data frame**, `apply` coerces to matrix via `as.matrix` first — 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 with `X`, `MARGIN`, or `FUN` (partial matching risk).
+
+---
+
+## lapply / sapply / vapply
+
+- `sapply` can return a vector, matrix, or list unpredictably — use `vapply` in non-interactive code with explicit `FUN.VALUE` template.
+- Calling primitives directly in `lapply` can cause dispatch issues; wrap in `function(x) is.numeric(x)` rather than bare `is.numeric`.
+- `sapply` with `simplify = "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 = NA` fills empty cells; set `default = 0` for sum-like operations. Before R 3.4.0 this was hard-coded to `NA`.
+- Use `array2DF()` to convert result to a data frame.
+
+---
+
+## mapply
+
+- Argument name is `SIMPLIFY` (all caps) not `simplify` — inconsistent with `sapply`.
+- `MoreArgs` must be a **list** of args not vectorized over.
+- Recycles shorter args to common length; zero-length arg gives zero-length result.
+
+---
+
+## merge
+
+- Default `by` is `intersect(names(x), names(y))` — can silently merge on unintended columns if data frames share column names.
+- `by = 0` or `by = "row.names"` merges on row names, adding a "Row.names" column.
+- `by = NULL` (or both `by.x`/`by.y` length 0) produces **Cartesian product**.
+- Result is sorted on `by` columns by default (`sort = TRUE`). For unsorted output use `sort = FALSE`.
+- Duplicate key matches produce **all combinations** (one row per match pair).
+
+---
+
+## split
+
+- If `f` is a list of factors, interaction is used; levels containing `"."` can cause unexpected splits unless `sep` is changed.
+- `drop = FALSE` (default) retains empty factor levels as empty list elements.
+- Supports formula syntax: `split(df, ~ Month)`.
+
+---
+
+## cbind / rbind
+
+- `cbind` on data frames calls `data.frame(...)`, not `cbind.matrix`. Mixing matrices and data frames can give unexpected results.
+- `rbind` on data frames matches columns **by name**, not position. Missing columns get `NA`.
+- `cbind(NULL)` returns `NULL` (not a matrix). For consistency, `rbind(NULL)` also returns `NULL`.
+
+---
+
+## table
+
+- By default **excludes NA** (`useNA = "no"`). Use `useNA = "ifany"` or `exclude = NULL` to count NAs.
+- Setting `exclude` non-empty and non-default implies `useNA = "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
+
+- `duplicated` marks the **second and later** occurrences as TRUE, not the first. Use `fromLast = TRUE` to reverse.
+- For data frames, operates on whole rows. For lists, compares recursively.
+- `unique` keeps the **first** occurrence of each value.
+
+---
+
+## data.frame (gotchas)
+
+- `stringsAsFactors = FALSE` is 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. Use `as.numeric(levels(f))[f]` or `as.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)`) drops `NA` groups by default.
+- The data frame method requires `by` as 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 `-x` for descending numeric, or `decreasing = TRUE`.
+- For character sorting, depends on locale. Use `method = "radix"` for locale-independent fast sorting.
+- `sort.int()` with `method = "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)
+
+- `Date` objects 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 a `difftime` object 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) <- n` pads with `NA`s 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)` uses `tz = "UTC"` by default — may give different date than intended if original was in another timezone.
+- `Sys.time()` returns POSIXct in current timezone.
+- `strptime` returns POSIXlt; `as.POSIXct(strptime(...))` to get POSIXct.
+- `difftime` arithmetic: 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)` returns `user`, `system`, and `elapsed` time.
+- `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 than `user` (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 to `print`/`format`/`cat` but not `sprintf`.
+- `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 (affects `format`, `print`, NOT `sprintf`).
+- `options(stringsAsFactors = FALSE)`: global default for `data.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 in `print` output.
+- `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.path` does NOT normalize paths (no `..` resolution); use `normalizePath()` for that.
+
+---
+
+## list.files
+
+- `list.files(pattern = "*.csv")` — `pattern` is a **regex**, not a glob! Use `glob2rx("*.csv")` or `"\\.csv$"`.
+- `full.names = FALSE` (default) returns basenames only. Use `full.names = TRUE` for complete paths.
+- `recursive = TRUE` to search subdirectories.
+- `all.files = TRUE` to include hidden files (starting with `.`).
+
+---
+
+## file.info
+
+- Returns data frame with `size`, `isdir`, `mode`, `mtime`, `ctime`, `atime`, `uid`, `gid`.
+- `mtime`: modification time (POSIXct). Useful for `file.info(f)$mtime`.
+- On some filesystems, `ctime` is 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. Use `comment.char = ""` to disable (also faster).
+- `header` auto-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.csv` defaults differ from `read.table`: `header = TRUE`, `sep = ","`, `fill = TRUE`, `comment.char = ""`.
+- For large files: specifying `colClasses` and `nrows` dramatically reduces memory usage. `read.table` is slow for wide data frames (hundreds of columns); use `scan` or `data.table::fread` for matrices.
+- `stringsAsFactors = FALSE` since R 4.0.0 (was TRUE before).
+
+---
+
+## write.table (gotchas)
+
+- `row.names = TRUE` by default — produces an unnamed first column that confuses re-reading. Use `row.names = FALSE` or `col.names = NA` for Excel-compatible CSV.
+- `write.csv` fixes `sep = ","`, `dec = "."`, and uses `qmethod = "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. `widths` is a vector of field widths.
+- **Negative widths skip** that many characters (useful for ignoring fields).
+- `buffersize` controls how many lines are read at a time; increase for large files.
+- Uses `read.table` internally after splitting fields.
+
+---
+
+## count.fields
+
+- Counts fields per line in a file — useful for diagnosing read errors.
+- `sep` and `quote` arguments match those of `read.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`/`pattern` are matched positionally to `ignore.case`, `perl`, etc. Common source of silent bugs.
+- `sub` replaces **first** match only; `gsub` replaces **all** matches.
+- Backreferences: `"\\1"` in replacement (double backslash in R strings). With `perl = TRUE`: `"\\U\\1"` for uppercase conversion.
+- `grep(value = TRUE)` returns matching **elements**; `grep(value = FALSE)` (default) returns **indices**.
+- `grepl` returns logical vector — preferred for filtering.
+- `regexpr` returns first match position + length (as attributes); `gregexpr` returns all matches as a list.
+- `regexec` returns match + capture group positions; `gregexec` does 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 = ""` or `split = character(0)` splits into individual characters.
+- Match at beginning of string: first element of result is `""`. Match at end: no trailing `""`.
+- `fixed = TRUE` is 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 but `last` defaults to `1000000L` (effectively "to end"). Vectorized over `first`/`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)` returns `NA` (not 2). `nchar(factor)` works on the level labels.
+- `keepNA = TRUE` (default since R 3.3.0); set to `FALSE` to count `"NA"` as 2 characters.
+
+---
+
+## format / formatC
+
+- `format(x, digits, nsmall)`: `nsmall` forces minimum decimal places. `big.mark = ","` adds thousands separator.
+- `formatC(x, format = "f", digits = 2)`: C-style formatting. `format = "e"` for scientific, `"g"` for general.
+- `format` returns 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 Rscript` or full path.
+- `--vanilla` or `--no-init-file` to skip `.Rprofile` loading.
+- 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 = TRUE` to 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)`. Without `I()`, `^` means interaction crossing.
+- `*` = main effects + interaction: `a*b` expands to `a + b + a:b`.
+- `(a+b+c)^2` = all main effects + all 2-way interactions (not squaring).
+- `-` removes terms: `(a+b+c)^2 - a:b` drops only the `a:b` interaction.
+- `/` means nesting: `a/b` = `a + b %in% a` = `a + a:b`.
+- `.` in formula means "all other columns in data" (in `terms.formula` context) or "previous contents" (in `update.formula`).
+- Formula objects carry an **environment** used for variable lookup; `as.formula("y ~ x")` uses `parent.frame()`.
+
+---
+
+## terms / model.matrix
+
+- `model.matrix` creates the design matrix including dummy coding. Default contrasts: `contr.treatment` for unordered factors, `contr.poly` for ordered.
+- `terms` object attributes: `order` (interaction order per term), `intercept`, `factors` matrix.
+- Column names from `model.matrix` can be surprising: e.g., `factorLevelName` concatenation.
+
+---
+
+## glm
+
+- Default `family = gaussian(link = "identity")` — `glm()` with no `family` silently fits OLS (same as `lm`, but slower and with deviance-based output).
+- Common families: `binomial(link = "logit")`, `poisson(link = "log")`, `Gamma(link = "inverse")`, `inverse.gaussian()`.
+- `binomial` accepts response as: 0/1 vector, logical, factor (second level = success), or 2-column matrix `cbind(success, failure)`.
+- `weights` in `glm` means **prior weights** (not frequency weights) — for frequency weights, use the cbind trick or offset.
+- `predict.glm(type = "response")` for predicted probabilities; default `type = "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
+
+- `aov` is a wrapper around `lm` that 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 to `contr.sum`/`contr.helmert`.
+- Error strata for repeated measures: `aov(y ~ A*B + Error(Subject/B))`.
+- `summary.aov` gives 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.
+- `weights` argument for weighted NLS; `na.action` for missing value handling.
+
+---
+
+## step / add1
+
+- `step` does **stepwise** model selection by AIC (default). Use `k = log(n)` for BIC.
+- Direction: `direction = "both"` (default), `"forward"`, or `"backward"`.
+- `add1`/`drop1` evaluate single-term additions/deletions; `step` calls these iteratively.
+- `scope` argument defines the upper/lower model bounds for search.
+- `step` modifies the model object in place — can be slow for large models with many candidate terms.
+
+---
+
+## predict.lm / predict.glm
+
+- `predict.lm` with `interval = "confidence"` gives CI for **mean** response; `interval = "prediction"` gives PI for **new observation** (wider).
+- `newdata` must have columns matching the original formula variables — factors must have the same levels.
+- `predict.glm` with `type = "response"` gives predictions on the response scale (e.g., probabilities for logistic); `type = "link"` (default) gives on the link scale.
+- `se.fit = TRUE` returns standard errors; for `predict.glm` these are on the **link** scale regardless of `type`.
+- `predict.lm` with `type = "terms"` returns the contribution of each term.
+
+---
+
+## loess
+
+- `span` controls 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; set `FALSE` for spatial coords.
+
+---
+
+## lowess vs loess
+
+- `lowess` is the older function; returns `list(x, y)` — cannot predict at new points.
+- `loess` is the newer formula interface with `predict` method.
+- `lowess` parameter is `f` (span, default 2/3); `loess` parameter is `span` (default 0.75).
+- `lowess` `iter` default is 3 (robustifying iterations); `loess` default `family = "gaussian"` (no robustness).
+
+---
+
+## smooth.spline
+
+- Default smoothing parameter selected by **GCV** (generalized cross-validation).
+- `cv = TRUE` uses ordinary leave-one-out CV instead — do not use with duplicate x values.
+- `spar` and `lambda` control smoothness; `df` can specify equivalent degrees of freedom.
+- Returns object with `predict`, `print`, `plot` methods. The `fit` component 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"` or `optimize()`.
+- `"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.
+- `fn` can return `NA`/`Inf` (except `"L-BFGS-B"` which requires finite values always). Initial value must be finite.
+
+---
+
+## optimize / uniroot
+
+- `optimize`: 1D minimization on a bounded interval. Returns `minimum` and `objective`.
+- `uniroot`: finds a root of `f` in `[lower, upper]`. **Requires** `f(lower)` and `f(upper)` to have opposite signs.
+- `uniroot` with `extendInt = "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 from `fn` (unusual interface).
+
+---
+
+## TukeyHSD
+
+- Requires a fitted `aov` object (not `lm`).
+- 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 with `B = 2000` replicates (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. `p` must sum to 1 (or set `rescale.p = TRUE`).
+- Return object includes `$expected`, `$residuals` (Pearson), and `$stdres` (standardized).
+
+---
+
+## wilcox.test
+
+- `exact = TRUE` by default for small samples with no ties. With ties, normal approximation used.
+- `correct = TRUE` applies continuity correction to normal approximation.
+- `conf.int = TRUE` computes Hodges-Lehmann estimator and confidence interval (not just the p-value).
+- Paired test: `paired = TRUE` uses 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.
+- `workspace` controls memory for the network algorithm; increase if you get errors on large tables.
+- `or` argument 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 `dgof` or `ks.test` with `exact = NULL` for discrete distributions.
+
+---
+
+## p.adjust
+
+- Methods: `"holm"` (default), `"BH"` (Benjamini-Hochberg FDR), `"bonferroni"`, `"BY"`, `"hochberg"`, `"hommel"`, `"fdr"` (alias for BH), `"none"`.
+- `n` argument: total number of hypotheses (can be larger than `length(p)` if some p-values are excluded).
+- Handles `NA`s: adjusted p-values are `NA` where input is `NA`.
+
+---
+
+## pairwise.t.test / pairwise.wilcox.test
+
+- `p.adjust.method` defaults 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 > 1` recommended (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 `dist` object. Use `as.dist()` to convert a symmetric matrix.
+- `hang = -1` in `plot()` aligns all labels at the bottom.
+
+---
+
+## dist
+
+- `method = "euclidean"` (default). Other options: `"manhattan"`, `"maximum"`, `"canberra"`, `"binary"`, `"minkowski"`.
+- Returns a `dist` object (lower triangle only). Use `as.matrix()` to get full matrix.
+- `"canberra"`: terms with zero numerator and denominator are **omitted** from the sum (not treated as 0/0).
+- `Inf` values: Euclidean distance involving `Inf` is `Inf`. Multiple `Inf`s in same obs give `NaN` for some methods.
+
+---
+
+## prcomp vs princomp
+
+- `prcomp` uses **SVD** (numerically superior); `princomp` uses `eigen` on covariance (less stable, N-1 vs N scaling).
+- `scale. = TRUE` in `prcomp` standardizes variables; important when variables have very different scales.
+- `princomp` standard deviations differ from `prcomp` by factor `sqrt((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.5` halves the bandwidth (less smooth).
+- Default kernel: `"gaussian"`. Range of density extends beyond data range (controlled by `cut`, default 3 bandwidths).
+- `n = 512`: number of evaluation points. Increase for smoother plotting.
+- `from`/`to`: explicitly bound the evaluation range.
+
+---
+
+## quantile
+
+- **Nine** `type` options (1-9). Default `type = 7` (R default, linear interpolation). Type 1 = inverse of empirical CDF (SAS default). Types 4-9 are continuous; 1-3 are discontinuous.
+- `na.rm = FALSE` by default — returns NA if any NAs present.
+- `names = TRUE` by 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:
+
+- **`n` argument in `r*()` functions**: if `length(n) > 1`, uses `length(n)` as the count, not `n` itself. So `rnorm(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 = FALSE` gives survival function P(X > x) directly (more accurate than 1 - pnorm() in tails).
+- **Gamma**: parameterized by `shape` and `rate` (= 1/scale). Default `rate = 1`. Specifying both `rate` and `scale` is an error.
+- **Beta**: `shape1` (alpha), `shape2` (beta) — no `mean`/`sd` parameterization.
+- **Poisson `dpois`**: `x` can be non-integer (returns 0 with a warning for non-integer values if `log = FALSE`).
+- **Weibull**: `shape` and `scale` (no `rate`). R's parameterization: `f(x) = (shape/scale)(x/scale)^(shape-1) exp(-(x/scale)^shape)`.
+- **Lognormal**: `meanlog` and `sdlog` are 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 `NA` in weights: observation is dropped if weight is `NA`.
+- 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`/`mfcol` resets `cex` to 1 and `mex` to 1. With 2x2 layout, base `cex` is multiplied by 0.83; with 3+ rows/columns, by 0.66.
+- `mai` (inches), `mar` (lines), `pin`, `plt`, `pty` all interact. Restoring all saved parameters after device resize can produce inconsistent results — last-alphabetically wins.
+- `bg` set via `par()` also sets `new = FALSE`. Setting `fg` via `par()` also sets `col`.
+- `xpd = NA` clips to device region (allows drawing in outer margins); `xpd = TRUE` clips 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 in `mex` units.
+- `las`: 0 = parallel to axis, 1 = horizontal, 2 = perpendicular, 3 = vertical. Does **not** respond to `srt`.
+- `tck = 1` draws grid lines across the plot. `tcl = -0.5` (default) gives outward ticks.
+- `usr` with log scale: contains **log10** of the coordinate limits, not the raw values.
+- Read-only parameters: `cin`, `cra`, `csi`, `cxy`, `din`, `page`.
+
+---
+
+## layout
+
+- `layout(mat)` where `mat` is a matrix of integers specifying figure arrangement.
+- `widths`/`heights` accept `lcm()` for absolute sizes mixed with relative sizes.
+- More flexible than `mfrow`/`mfcol` but cannot be queried once set (unlike `par("mfrow")`).
+- `layout.show(n)` visualizes the layout for debugging.
+
+---
+
+## axis / mtext
+
+- `axis(side, at, labels)`: `side` 1=bottom, 2=left, 3=top, 4=right.
+- Default gap between axis labels controlled by `par("mgp")`. Labels can overlap if not managed.
+- `mtext`: `line` argument positions text in margin lines (0 = adjacent to plot, positive = outward). `adj` controls horizontal position (0-1).
+- `mtext` with `outer = TRUE` writes in the **outer** margin (set by `par(oma = ...)`).
+
+---
+
+## curve
+
+- First argument can be an **expression** in `x` or a function: `curve(sin, 0, 2*pi)` or `curve(x^2 + 1, 0, 10)`.
+- `add = TRUE` to overlay on existing plot. Default `n = 101` evaluation points.
+- `xname = "x"` by default; change if your expression uses a different variable name.
+
+---
+
+## pairs
+
+- `panel` function receives `(x, y, ...)` for each pair. `lower.panel`, `upper.panel`, `diag.panel` for different regions.
+- `gap` controls spacing between panels (default 1).
+- Formula interface: `pairs(~ var1 + var2 + var3, data = df)`.
+
+---
+
+## coplot
+
+- Conditioning plots: `coplot(y ~ x | a)` or `coplot(y ~ x | a * b)` for two conditioning variables.
+- `panel` function can be customized; `rows`/`columns` control layout.
+- Default panel draws points; use `panel = panel.smooth` for loess overlay.
+
+---
+
+## matplot / matlines / matpoints
+
+- Plots columns of one matrix against columns of another. Recycles `col`, `lty`, `pch` across columns.
+- `type = "l"` by default (unlike `plot` which defaults to `"p"`).
+- Useful for plotting multiple time series or fitted curves simultaneously.
+
+---
+
+## contour / filled.contour / image
+
+- `contour(x, y, z)`: `z` must be a matrix with `dim = c(length(x), length(y))`.
+- `filled.contour` has a non-standard layout — it creates its own plot region for the color key. **Cannot use `par(mfrow)` with it**. Adding elements requires the `plot.axes` argument.
+- `image`: plots z-values as colored rectangles. Default color scheme may be misleading; set `col` explicitly.
+- For `image`, `x` and `y` specify **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.
+- `shade` and `col` control surface shading. `border = NA` removes 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 with `col`; `border` controls 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 like `pdf`).
+- `dev.off()` on the **last** open device reverts to null device.
+- `dev.copy(pdf, file = "plot.pdf")` followed by `dev.off()` to save current plot.
+- `dev.list()` returns all open devices; `dev.cur()` the active one.
+
+---
+
+## pdf
+
+- 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 `%d` in filename for numbering).
+- `useDingbats = FALSE` recommended to avoid issues with certain PDF viewers and pch symbols.
+- Default size: 7x7 inches. `family` controls font family.
+
+---
+
+## png / bitmap devices
+
+- `res` controls DPI (default 72). For publication: `res = 300` with appropriate `width`/`height` in pixels or inches (with `units = "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)` — note `maxColorValue` default 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
+
+- `colorRamp` returns a **function** mapping [0,1] to RGB matrix.
+- `colorRampPalette` returns a **function** taking `n` and returning `n` interpolated 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 [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.
+
+```r
+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](https://github.com/iremaydas) — PhD candidate, occasional R user, full-time Googler of things I should probably know by now.*
+```
+
+
+
diff --git a/prompts.csv b/prompts.csv
index 64f5a3f6..c919b216 100644
--- a/prompts.csv
+++ b/prompts.csv
@@ -92004,3 +92004,1882 @@ When you ask for help, the agent responds with:
- **Evidence artifacts** to collect
- **Links** to specific documentation
",FALSE,TEXT,papanito
+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 installation
+---
+
+# 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
+
+```r
+# 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
+
+```r
+# 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
+
+```r
+# 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
+
+```r
+# 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
+
+```r
+# 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
+
+```r
+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
+
+```r
+# 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
+
+```r
+# 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
+
+```r
+# 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
+
+```r
+# 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
+
+```r
+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`/`r` prefixes).
+- **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()` over `sapply()` in production code — it enforces return types
+- Prefer `seq_along(x)` over `1:length(x)` — the latter breaks when `x` is empty
+- Use `stringsAsFactors = FALSE` in `read.csv()` / `data.frame()` (default changed in R 4.0)
+- Vectorize operations instead of writing loops when possible
+- Use `stop()`, `warning()`, `message()` for error handling — not `print()`
+- `<<-` assigns to parent environment — use sparingly and intentionally
+- `with(df, expr)` avoids repeating `df$` everywhere
+- `Sys.setenv()` and `.Renviron` for 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)` — `args` must be a **list**, even for a single argument.
+- `quote = TRUE` prevents evaluation of arguments before the call — needed when passing expressions/symbols.
+- Behavior of `substitute` inside `do.call` differs 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 function `f` cumulatively: `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's `itertools.accumulate`.
+- `Reduce(f, x, right = TRUE)` folds from the right: `f(x1, f(x2, f(x3, x4)))`.
+- `Reduce` with `init` adds a starting value: `Reduce(f, x, init = v)` = `f(f(f(v, x1), x2), x3)`.
+- `Filter(f, x)` keeps elements where `f(elem)` is `TRUE`. Unlike `x[sapply(x, f)]`, handles `NULL`/empty correctly.
+- `Map(f, ...)` is a simple wrapper for `mapply(f, ..., SIMPLIFY = FALSE)` — always returns a list.
+- `Find(f, x)` returns the **first** element where `f(elem)` is `TRUE`. `Find(f, x, right = TRUE)` for last.
+- `Position(f, x)` returns the **index** of the first match (like `Find` but returns position, not value).
+
+---
+
+## lengths
+
+- `lengths(x)` returns the length of **each element** of a list. Equivalent to `sapply(x, length)` but faster (implemented in C).
+- Works on any list-like object. Returns integer vector.
+
+---
+
+## conditions (tryCatch / withCallingHandlers)
+
+- `tryCatch` **unwinds** the call stack — handler runs in the calling environment, not where the error occurred. Cannot resume execution.
+- `withCallingHandlers` does 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. Use `withCallingHandlers` + `invokeRestart(""muffleWarning"")` to suppress warnings but continue.
+- `tryCatch` `finally` clause 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 with `tryCatch(..., myError = function(e) ...)`.
+
+---
+
+## all.equal
+
+- Tests **near equality** with tolerance (default `1.5e-8`, i.e., `sqrt(.Machine$double.eps)`).
+- Returns `TRUE` or a **character string** describing the difference — NOT `FALSE`. Use `isTRUE(all.equal(x, y))` in conditionals.
+- `tolerance` argument controls numeric tolerance. `scale` for absolute vs relative comparison.
+- Checks attributes, names, dimensions — more thorough than `==`.
+
+---
+
+## combn
+
+- `combn(n, m)` or `combn(x, m)`: generates all combinations of `m` items from `x`.
+- Returns a **matrix** with `m` rows; each column is one combination.
+- `FUN` argument applies a function to each combination: `combn(5, 3, sum)` returns sums of all 3-element subsets.
+- `simplify = FALSE` returns a list instead of a matrix.
+
+---
+
+## modifyList
+
+- `modifyList(x, val)` replaces elements of list `x` with those in `val` by **name**.
+- Setting a value to `NULL` **removes** that element from the list.
+- **Does** add new names not in `x` — it uses `x[names(val)] <- val` internally, so any name in `val` gets added or replaced.
+
+---
+
+## relist
+
+- Inverse of `unlist`: given a flat vector and a skeleton list, reconstructs the nested structure.
+- `relist(flesh, skeleton)` — `flesh` is the flat data, `skeleton` provides 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 with `close(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). Use `find.package()` or `requireNamespace()` to check for a specific package.
+- `update.packages(ask = FALSE)` updates all packages without prompting.
+- `lib.loc` specifies 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)`: `frequency` is observations per unit time (12 for monthly, 4 for quarterly).
+- `acf` default `type = ""correlation""`. Use `type = ""partial""` for PACF. `plot = FALSE` to 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.
+- `arima` handles `NA` values in the time series (via Kalman filter).
+- `stl` requires `s.window` (seasonal window) — must be specified, no default. `s.window = ""periodic""` assumes fixed seasonality.
+- `decompose`: simpler than `stl`, uses moving averages. `type = ""additive""` or `""multiplicative""`.
+- `stl` result components: `$time.series` matrix with columns `seasonal`, `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]` is `m[2, 1]` not `m[1, 2]` — argument names are **ignored** in `[`, positional matching only. Never name index args.
+- Factor indexing: `x[f]` uses integer codes of factor `f`, not its character labels. Use `x[as.character(f)]` for label-based indexing.
+- `x[[]]` with no index is always an error. `x$name` does partial matching by default; `x[[""name""]]` does not (exact by default).
+- Assigning `NULL` via `x[[i]] <- NULL` or `x$name <- NULL` **deletes** that list element.
+- Data frame `[` with single column: `df[, 1]` returns a **vector** (drop=TRUE default for columns), but `df[1, ]` returns a **data frame** (drop=FALSE for rows). Use `drop = FALSE` explicitly.
+- Matrix indexing a data frame (`df[cbind(i,j)]`) coerces to matrix first — avoid.
+
+---
+
+## subset
+
+Use interactively only; unsafe for programming.
+
+- `subset` argument 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.
+- `NA`s in the logical condition are treated as `FALSE` (rows silently dropped).
+- Factors may retain unused levels after subsetting; call `droplevels()`.
+
+---
+
+## match / %in%
+
+- `%in%` **never returns NA** — this makes it safe for `if()` conditions unlike `==`.
+- `match()` returns position of **first** match only; duplicates in `table` are ignored.
+- Factors, raw vectors, and lists are all converted to character before matching.
+- `NaN` matches `NaN` but not `NA`; `NA` matches `NA` only.
+
+---
+
+## apply
+
+- On a **data frame**, `apply` coerces to matrix via `as.matrix` first — 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 with `X`, `MARGIN`, or `FUN` (partial matching risk).
+
+---
+
+## lapply / sapply / vapply
+
+- `sapply` can return a vector, matrix, or list unpredictably — use `vapply` in non-interactive code with explicit `FUN.VALUE` template.
+- Calling primitives directly in `lapply` can cause dispatch issues; wrap in `function(x) is.numeric(x)` rather than bare `is.numeric`.
+- `sapply` with `simplify = ""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 = NA` fills empty cells; set `default = 0` for sum-like operations. Before R 3.4.0 this was hard-coded to `NA`.
+- Use `array2DF()` to convert result to a data frame.
+
+---
+
+## mapply
+
+- Argument name is `SIMPLIFY` (all caps) not `simplify` — inconsistent with `sapply`.
+- `MoreArgs` must be a **list** of args not vectorized over.
+- Recycles shorter args to common length; zero-length arg gives zero-length result.
+
+---
+
+## merge
+
+- Default `by` is `intersect(names(x), names(y))` — can silently merge on unintended columns if data frames share column names.
+- `by = 0` or `by = ""row.names""` merges on row names, adding a ""Row.names"" column.
+- `by = NULL` (or both `by.x`/`by.y` length 0) produces **Cartesian product**.
+- Result is sorted on `by` columns by default (`sort = TRUE`). For unsorted output use `sort = FALSE`.
+- Duplicate key matches produce **all combinations** (one row per match pair).
+
+---
+
+## split
+
+- If `f` is a list of factors, interaction is used; levels containing `"".""` can cause unexpected splits unless `sep` is changed.
+- `drop = FALSE` (default) retains empty factor levels as empty list elements.
+- Supports formula syntax: `split(df, ~ Month)`.
+
+---
+
+## cbind / rbind
+
+- `cbind` on data frames calls `data.frame(...)`, not `cbind.matrix`. Mixing matrices and data frames can give unexpected results.
+- `rbind` on data frames matches columns **by name**, not position. Missing columns get `NA`.
+- `cbind(NULL)` returns `NULL` (not a matrix). For consistency, `rbind(NULL)` also returns `NULL`.
+
+---
+
+## table
+
+- By default **excludes NA** (`useNA = ""no""`). Use `useNA = ""ifany""` or `exclude = NULL` to count NAs.
+- Setting `exclude` non-empty and non-default implies `useNA = ""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
+
+- `duplicated` marks the **second and later** occurrences as TRUE, not the first. Use `fromLast = TRUE` to reverse.
+- For data frames, operates on whole rows. For lists, compares recursively.
+- `unique` keeps the **first** occurrence of each value.
+
+---
+
+## data.frame (gotchas)
+
+- `stringsAsFactors = FALSE` is 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. Use `as.numeric(levels(f))[f]` or `as.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)`) drops `NA` groups by default.
+- The data frame method requires `by` as 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 `-x` for descending numeric, or `decreasing = TRUE`.
+- For character sorting, depends on locale. Use `method = ""radix""` for locale-independent fast sorting.
+- `sort.int()` with `method = ""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)
+
+- `Date` objects 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 a `difftime` object 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) <- n` pads with `NA`s 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)` uses `tz = ""UTC""` by default — may give different date than intended if original was in another timezone.
+- `Sys.time()` returns POSIXct in current timezone.
+- `strptime` returns POSIXlt; `as.POSIXct(strptime(...))` to get POSIXct.
+- `difftime` arithmetic: 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)` returns `user`, `system`, and `elapsed` time.
+- `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 than `user` (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 to `print`/`format`/`cat` but not `sprintf`.
+- `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 (affects `format`, `print`, NOT `sprintf`).
+- `options(stringsAsFactors = FALSE)`: global default for `data.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 in `print` output.
+- `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.path` does NOT normalize paths (no `..` resolution); use `normalizePath()` for that.
+
+---
+
+## list.files
+
+- `list.files(pattern = ""*.csv"")` — `pattern` is a **regex**, not a glob! Use `glob2rx(""*.csv"")` or `""\\.csv$""`.
+- `full.names = FALSE` (default) returns basenames only. Use `full.names = TRUE` for complete paths.
+- `recursive = TRUE` to search subdirectories.
+- `all.files = TRUE` to include hidden files (starting with `.`).
+
+---
+
+## file.info
+
+- Returns data frame with `size`, `isdir`, `mode`, `mtime`, `ctime`, `atime`, `uid`, `gid`.
+- `mtime`: modification time (POSIXct). Useful for `file.info(f)$mtime`.
+- On some filesystems, `ctime` is 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. Use `comment.char = """"` to disable (also faster).
+- `header` auto-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.csv` defaults differ from `read.table`: `header = TRUE`, `sep = "",""`, `fill = TRUE`, `comment.char = """"`.
+- For large files: specifying `colClasses` and `nrows` dramatically reduces memory usage. `read.table` is slow for wide data frames (hundreds of columns); use `scan` or `data.table::fread` for matrices.
+- `stringsAsFactors = FALSE` since R 4.0.0 (was TRUE before).
+
+---
+
+## write.table (gotchas)
+
+- `row.names = TRUE` by default — produces an unnamed first column that confuses re-reading. Use `row.names = FALSE` or `col.names = NA` for Excel-compatible CSV.
+- `write.csv` fixes `sep = "",""`, `dec = "".""`, and uses `qmethod = ""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. `widths` is a vector of field widths.
+- **Negative widths skip** that many characters (useful for ignoring fields).
+- `buffersize` controls how many lines are read at a time; increase for large files.
+- Uses `read.table` internally after splitting fields.
+
+---
+
+## count.fields
+
+- Counts fields per line in a file — useful for diagnosing read errors.
+- `sep` and `quote` arguments match those of `read.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`/`pattern` are matched positionally to `ignore.case`, `perl`, etc. Common source of silent bugs.
+- `sub` replaces **first** match only; `gsub` replaces **all** matches.
+- Backreferences: `""\\1""` in replacement (double backslash in R strings). With `perl = TRUE`: `""\\U\\1""` for uppercase conversion.
+- `grep(value = TRUE)` returns matching **elements**; `grep(value = FALSE)` (default) returns **indices**.
+- `grepl` returns logical vector — preferred for filtering.
+- `regexpr` returns first match position + length (as attributes); `gregexpr` returns all matches as a list.
+- `regexec` returns match + capture group positions; `gregexec` does 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 = """"` or `split = character(0)` splits into individual characters.
+- Match at beginning of string: first element of result is `""""`. Match at end: no trailing `""""`.
+- `fixed = TRUE` is 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 but `last` defaults to `1000000L` (effectively ""to end""). Vectorized over `first`/`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)` returns `NA` (not 2). `nchar(factor)` works on the level labels.
+- `keepNA = TRUE` (default since R 3.3.0); set to `FALSE` to count `""NA""` as 2 characters.
+
+---
+
+## format / formatC
+
+- `format(x, digits, nsmall)`: `nsmall` forces minimum decimal places. `big.mark = "",""` adds thousands separator.
+- `formatC(x, format = ""f"", digits = 2)`: C-style formatting. `format = ""e""` for scientific, `""g""` for general.
+- `format` returns 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 Rscript` or full path.
+- `--vanilla` or `--no-init-file` to skip `.Rprofile` loading.
+- 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 = TRUE` to 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)`. Without `I()`, `^` means interaction crossing.
+- `*` = main effects + interaction: `a*b` expands to `a + b + a:b`.
+- `(a+b+c)^2` = all main effects + all 2-way interactions (not squaring).
+- `-` removes terms: `(a+b+c)^2 - a:b` drops only the `a:b` interaction.
+- `/` means nesting: `a/b` = `a + b %in% a` = `a + a:b`.
+- `.` in formula means ""all other columns in data"" (in `terms.formula` context) or ""previous contents"" (in `update.formula`).
+- Formula objects carry an **environment** used for variable lookup; `as.formula(""y ~ x"")` uses `parent.frame()`.
+
+---
+
+## terms / model.matrix
+
+- `model.matrix` creates the design matrix including dummy coding. Default contrasts: `contr.treatment` for unordered factors, `contr.poly` for ordered.
+- `terms` object attributes: `order` (interaction order per term), `intercept`, `factors` matrix.
+- Column names from `model.matrix` can be surprising: e.g., `factorLevelName` concatenation.
+
+---
+
+## glm
+
+- Default `family = gaussian(link = ""identity"")` — `glm()` with no `family` silently fits OLS (same as `lm`, but slower and with deviance-based output).
+- Common families: `binomial(link = ""logit"")`, `poisson(link = ""log"")`, `Gamma(link = ""inverse"")`, `inverse.gaussian()`.
+- `binomial` accepts response as: 0/1 vector, logical, factor (second level = success), or 2-column matrix `cbind(success, failure)`.
+- `weights` in `glm` means **prior weights** (not frequency weights) — for frequency weights, use the cbind trick or offset.
+- `predict.glm(type = ""response"")` for predicted probabilities; default `type = ""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
+
+- `aov` is a wrapper around `lm` that 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 to `contr.sum`/`contr.helmert`.
+- Error strata for repeated measures: `aov(y ~ A*B + Error(Subject/B))`.
+- `summary.aov` gives 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.
+- `weights` argument for weighted NLS; `na.action` for missing value handling.
+
+---
+
+## step / add1
+
+- `step` does **stepwise** model selection by AIC (default). Use `k = log(n)` for BIC.
+- Direction: `direction = ""both""` (default), `""forward""`, or `""backward""`.
+- `add1`/`drop1` evaluate single-term additions/deletions; `step` calls these iteratively.
+- `scope` argument defines the upper/lower model bounds for search.
+- `step` modifies the model object in place — can be slow for large models with many candidate terms.
+
+---
+
+## predict.lm / predict.glm
+
+- `predict.lm` with `interval = ""confidence""` gives CI for **mean** response; `interval = ""prediction""` gives PI for **new observation** (wider).
+- `newdata` must have columns matching the original formula variables — factors must have the same levels.
+- `predict.glm` with `type = ""response""` gives predictions on the response scale (e.g., probabilities for logistic); `type = ""link""` (default) gives on the link scale.
+- `se.fit = TRUE` returns standard errors; for `predict.glm` these are on the **link** scale regardless of `type`.
+- `predict.lm` with `type = ""terms""` returns the contribution of each term.
+
+---
+
+## loess
+
+- `span` controls 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; set `FALSE` for spatial coords.
+
+---
+
+## lowess vs loess
+
+- `lowess` is the older function; returns `list(x, y)` — cannot predict at new points.
+- `loess` is the newer formula interface with `predict` method.
+- `lowess` parameter is `f` (span, default 2/3); `loess` parameter is `span` (default 0.75).
+- `lowess` `iter` default is 3 (robustifying iterations); `loess` default `family = ""gaussian""` (no robustness).
+
+---
+
+## smooth.spline
+
+- Default smoothing parameter selected by **GCV** (generalized cross-validation).
+- `cv = TRUE` uses ordinary leave-one-out CV instead — do not use with duplicate x values.
+- `spar` and `lambda` control smoothness; `df` can specify equivalent degrees of freedom.
+- Returns object with `predict`, `print`, `plot` methods. The `fit` component 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""` or `optimize()`.
+- `""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.
+- `fn` can return `NA`/`Inf` (except `""L-BFGS-B""` which requires finite values always). Initial value must be finite.
+
+---
+
+## optimize / uniroot
+
+- `optimize`: 1D minimization on a bounded interval. Returns `minimum` and `objective`.
+- `uniroot`: finds a root of `f` in `[lower, upper]`. **Requires** `f(lower)` and `f(upper)` to have opposite signs.
+- `uniroot` with `extendInt = ""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 from `fn` (unusual interface).
+
+---
+
+## TukeyHSD
+
+- Requires a fitted `aov` object (not `lm`).
+- 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 with `B = 2000` replicates (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. `p` must sum to 1 (or set `rescale.p = TRUE`).
+- Return object includes `$expected`, `$residuals` (Pearson), and `$stdres` (standardized).
+
+---
+
+## wilcox.test
+
+- `exact = TRUE` by default for small samples with no ties. With ties, normal approximation used.
+- `correct = TRUE` applies continuity correction to normal approximation.
+- `conf.int = TRUE` computes Hodges-Lehmann estimator and confidence interval (not just the p-value).
+- Paired test: `paired = TRUE` uses 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.
+- `workspace` controls memory for the network algorithm; increase if you get errors on large tables.
+- `or` argument 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 `dgof` or `ks.test` with `exact = NULL` for discrete distributions.
+
+---
+
+## p.adjust
+
+- Methods: `""holm""` (default), `""BH""` (Benjamini-Hochberg FDR), `""bonferroni""`, `""BY""`, `""hochberg""`, `""hommel""`, `""fdr""` (alias for BH), `""none""`.
+- `n` argument: total number of hypotheses (can be larger than `length(p)` if some p-values are excluded).
+- Handles `NA`s: adjusted p-values are `NA` where input is `NA`.
+
+---
+
+## pairwise.t.test / pairwise.wilcox.test
+
+- `p.adjust.method` defaults 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 > 1` recommended (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 `dist` object. Use `as.dist()` to convert a symmetric matrix.
+- `hang = -1` in `plot()` aligns all labels at the bottom.
+
+---
+
+## dist
+
+- `method = ""euclidean""` (default). Other options: `""manhattan""`, `""maximum""`, `""canberra""`, `""binary""`, `""minkowski""`.
+- Returns a `dist` object (lower triangle only). Use `as.matrix()` to get full matrix.
+- `""canberra""`: terms with zero numerator and denominator are **omitted** from the sum (not treated as 0/0).
+- `Inf` values: Euclidean distance involving `Inf` is `Inf`. Multiple `Inf`s in same obs give `NaN` for some methods.
+
+---
+
+## prcomp vs princomp
+
+- `prcomp` uses **SVD** (numerically superior); `princomp` uses `eigen` on covariance (less stable, N-1 vs N scaling).
+- `scale. = TRUE` in `prcomp` standardizes variables; important when variables have very different scales.
+- `princomp` standard deviations differ from `prcomp` by factor `sqrt((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.5` halves the bandwidth (less smooth).
+- Default kernel: `""gaussian""`. Range of density extends beyond data range (controlled by `cut`, default 3 bandwidths).
+- `n = 512`: number of evaluation points. Increase for smoother plotting.
+- `from`/`to`: explicitly bound the evaluation range.
+
+---
+
+## quantile
+
+- **Nine** `type` options (1-9). Default `type = 7` (R default, linear interpolation). Type 1 = inverse of empirical CDF (SAS default). Types 4-9 are continuous; 1-3 are discontinuous.
+- `na.rm = FALSE` by default — returns NA if any NAs present.
+- `names = TRUE` by 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:
+
+- **`n` argument in `r*()` functions**: if `length(n) > 1`, uses `length(n)` as the count, not `n` itself. So `rnorm(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 = FALSE` gives survival function P(X > x) directly (more accurate than 1 - pnorm() in tails).
+- **Gamma**: parameterized by `shape` and `rate` (= 1/scale). Default `rate = 1`. Specifying both `rate` and `scale` is an error.
+- **Beta**: `shape1` (alpha), `shape2` (beta) — no `mean`/`sd` parameterization.
+- **Poisson `dpois`**: `x` can be non-integer (returns 0 with a warning for non-integer values if `log = FALSE`).
+- **Weibull**: `shape` and `scale` (no `rate`). R's parameterization: `f(x) = (shape/scale)(x/scale)^(shape-1) exp(-(x/scale)^shape)`.
+- **Lognormal**: `meanlog` and `sdlog` are 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 `NA` in weights: observation is dropped if weight is `NA`.
+- 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`/`mfcol` resets `cex` to 1 and `mex` to 1. With 2x2 layout, base `cex` is multiplied by 0.83; with 3+ rows/columns, by 0.66.
+- `mai` (inches), `mar` (lines), `pin`, `plt`, `pty` all interact. Restoring all saved parameters after device resize can produce inconsistent results — last-alphabetically wins.
+- `bg` set via `par()` also sets `new = FALSE`. Setting `fg` via `par()` also sets `col`.
+- `xpd = NA` clips to device region (allows drawing in outer margins); `xpd = TRUE` clips 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 in `mex` units.
+- `las`: 0 = parallel to axis, 1 = horizontal, 2 = perpendicular, 3 = vertical. Does **not** respond to `srt`.
+- `tck = 1` draws grid lines across the plot. `tcl = -0.5` (default) gives outward ticks.
+- `usr` with log scale: contains **log10** of the coordinate limits, not the raw values.
+- Read-only parameters: `cin`, `cra`, `csi`, `cxy`, `din`, `page`.
+
+---
+
+## layout
+
+- `layout(mat)` where `mat` is a matrix of integers specifying figure arrangement.
+- `widths`/`heights` accept `lcm()` for absolute sizes mixed with relative sizes.
+- More flexible than `mfrow`/`mfcol` but cannot be queried once set (unlike `par(""mfrow"")`).
+- `layout.show(n)` visualizes the layout for debugging.
+
+---
+
+## axis / mtext
+
+- `axis(side, at, labels)`: `side` 1=bottom, 2=left, 3=top, 4=right.
+- Default gap between axis labels controlled by `par(""mgp"")`. Labels can overlap if not managed.
+- `mtext`: `line` argument positions text in margin lines (0 = adjacent to plot, positive = outward). `adj` controls horizontal position (0-1).
+- `mtext` with `outer = TRUE` writes in the **outer** margin (set by `par(oma = ...)`).
+
+---
+
+## curve
+
+- First argument can be an **expression** in `x` or a function: `curve(sin, 0, 2*pi)` or `curve(x^2 + 1, 0, 10)`.
+- `add = TRUE` to overlay on existing plot. Default `n = 101` evaluation points.
+- `xname = ""x""` by default; change if your expression uses a different variable name.
+
+---
+
+## pairs
+
+- `panel` function receives `(x, y, ...)` for each pair. `lower.panel`, `upper.panel`, `diag.panel` for different regions.
+- `gap` controls spacing between panels (default 1).
+- Formula interface: `pairs(~ var1 + var2 + var3, data = df)`.
+
+---
+
+## coplot
+
+- Conditioning plots: `coplot(y ~ x | a)` or `coplot(y ~ x | a * b)` for two conditioning variables.
+- `panel` function can be customized; `rows`/`columns` control layout.
+- Default panel draws points; use `panel = panel.smooth` for loess overlay.
+
+---
+
+## matplot / matlines / matpoints
+
+- Plots columns of one matrix against columns of another. Recycles `col`, `lty`, `pch` across columns.
+- `type = ""l""` by default (unlike `plot` which defaults to `""p""`).
+- Useful for plotting multiple time series or fitted curves simultaneously.
+
+---
+
+## contour / filled.contour / image
+
+- `contour(x, y, z)`: `z` must be a matrix with `dim = c(length(x), length(y))`.
+- `filled.contour` has a non-standard layout — it creates its own plot region for the color key. **Cannot use `par(mfrow)` with it**. Adding elements requires the `plot.axes` argument.
+- `image`: plots z-values as colored rectangles. Default color scheme may be misleading; set `col` explicitly.
+- For `image`, `x` and `y` specify **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.
+- `shade` and `col` control surface shading. `border = NA` removes 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 with `col`; `border` controls 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 like `pdf`).
+- `dev.off()` on the **last** open device reverts to null device.
+- `dev.copy(pdf, file = ""plot.pdf"")` followed by `dev.off()` to save current plot.
+- `dev.list()` returns all open devices; `dev.cur()` the active one.
+
+---
+
+## pdf
+
+- 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 `%d` in filename for numbering).
+- `useDingbats = FALSE` recommended to avoid issues with certain PDF viewers and pch symbols.
+- Default size: 7x7 inches. `family` controls font family.
+
+---
+
+## png / bitmap devices
+
+- `res` controls DPI (default 72). For publication: `res = 300` with appropriate `width`/`height` in pixels or inches (with `units = ""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)` — note `maxColorValue` default 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
+
+- `colorRamp` returns a **function** mapping [0,1] to RGB matrix.
+- `colorRampPalette` returns a **function** taking `n` and returning `n` interpolated 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 [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.
+
+```r
+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](https://github.com/iremaydas) — PhD candidate, occasional R user, full-time Googler of things I should probably know by now.*",FALSE,TEXT,iremaydas