Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when writing R code: follow modern tidyverse style, not base-R defaults — native |> pipe, .by= grouping, cli::cli_abort() errors, {{ }} embracing, type-stable map_*().
.claude/skills/r-tidyverse-style/SKILL.md| Model | Eval pass | Runs |
|---|---|---|
| gemini-3.6-flash | 100% | 4 |
| Model | Lift | Δ tokens | Δ turns | Cases | Verified |
|---|---|---|---|---|---|
| gemini-3.5-flashbest | +96% | — | 0% | 24 | 86d ago |
| gemini-3.6-flash | +41% | +186% | 0% | 22 | 54d ago |
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-12 | ✗→✓ | ▲ Improved | — | — |
| case-15 | ✗→✓ | ▲ Improved | — | — |
| case-13 | ✗→✓ | ▲ Improved | — | — |
| case-17 | ✗→✓ | ▲ Improved | — | — |
| case-14 | ✗→✓ | ▲ Improved | — | — |
Enforces the modern-tidyverse form of R. Apply to every R snippet, function, or script you write. Each rule below overrides a base-R or legacy-tidyverse default that the base model reaches for by reflex; emit the conforming form instead.
<-. Reserve = strictly for passing functionarguments (mean(x, na.rm = TRUE)). Never assign a top-level object with =.
|>. Never use the magrittr %>%, and do notlibrary(magrittr) to get it.
.by =argument inside summarise() or mutate(). Never write group_by() + summarise() + ungroup(). Multiple keys go in .by = c(a, b).
column name and forwards it to dplyr, embrace it with {{ col }}. Never pass the column as a string, and never use substitute(), deparse(), df[[col]], eval(), or aes_string().
map_dbl(), map_chr(),map_lgl(), map_int(). Never use sapply() or vapply(), and never bare map() when the output type is known. Use bare map() only when each result is itself a list/data frame.
cli::cli_abort(). Never stop(). Pass a charactervector c(...): the FIRST element states the problem, the remaining elements are NAMED bullets — x = for the failure detail, i = for a hint/fix. Use inline markup: {.arg name} (argument), {.fn name} (function), {.cls {class(x)}} (class), {.val {x}} (value), {?s} (pluralise).
read_csv() / write_csv().Never base read.csv() / write.csv().
str_to_lower(), str_to_upper(),str_detect(), str_replace_all(), str_remove_all(), str_extract(), str_trim(), str_squish(). Never base tolower(), toupper(), grepl(), gsub(), sub(), regmatches(), trimws(), substr() for these jobs.
TRUE / FALSE in full. Never T / F (they arereassignable variables, not constants).
NULLand guard with if (!is.null(x)). Never default a bound to NA, -Inf, Inf, or a sentinel number.
cleanly (data |> f(...)). Prefix optional/meta arguments with . (.by, .data) to avoid clashing with columns captured by ....
(calculate_mean), variables are nouns (user_data); no dots in names except S3 methods (print.myclass).
calls; one verb per line in a pipe chain.
Each pair shows the base model's wrong default (BEFORE) and the conforming form (AFTER).
Rule 1 — assignment
r# BEFORE total = sum(x) # AFTER total <- sum(x)
Rule 2 — native pipe
r# BEFORE sales %>% filter(region == "west") %>% summarise(n = n()) # AFTER sales |> filter(region == "west") |> summarise(n = n())
Rule 3 — .by grouping
r# BEFORE sales |> group_by(region) |> summarise(avg = mean(revenue)) |> ungroup() # AFTER sales |> summarise(avg = mean(revenue), .by = region)
Rule 4 — embracing
r# BEFORE filter_above <- function(data, column, threshold) { data[data[[column]] > threshold, ] } # AFTER filter_above <- function(data, column, threshold) { data |> filter({{ column }} > threshold) }
Rule 5 — type-stable iteration
r# BEFORE r2 <- sapply(models, function(m) summary(m)$r.squared) # AFTER r2 <- map_dbl(models, \(m) summary(m)$r.squared)
Rule 6 — cli errors
r# BEFORE validate_input <- function(x) { if (!is.numeric(x)) stop("x must be numeric, not ", typeof(x)) } # AFTER validate_input <- function(x) { if (!is.numeric(x)) { cli::cli_abort(c( "{.arg x} must be numeric.", x = "You supplied {.cls {class(x)}}.", i = "Convert with {.fn as.numeric} first." )) } }
Rule 7 — readr I/O
r# BEFORE df <- read.csv("data/input.csv") write.csv(out, "data/output.csv") # AFTER df <- read_csv("data/input.csv") write_csv(out, "data/output.csv")
Rule 8 — stringr
r# BEFORE people$name <- trimws(tolower(people$name)) df <- df[grepl("@stanford.edu", df$email), ] # AFTER people |> mutate(name = str_trim(str_to_lower(name))) df |> filter(str_detect(email, "@stanford.edu"))
Rule 9 — full logicals
r# BEFORE read_csv(path, show_col_types = F) # AFTER read_csv(path, show_col_types = FALSE)
Rule 10 — NULL-guarded options
r# BEFORE range_filter <- function(df, min_value = -Inf, max_value = Inf) { df |> filter(value >= min_value, value <= max_value) } # AFTER range_filter <- function(df, min_value = NULL, max_value = NULL) { if (!is.null(min_value)) df <- df |> filter(value >= min_value) if (!is.null(max_value)) df <- df |> filter(value <= max_value) df }
Rule 11 — data-first signature
r# BEFORE my_transform <- function(threshold, data) { ... } # AFTER my_transform <- function(data, threshold = 0.5) { ... }
Rule 12 — names
r# BEFORE CalculateMean <- function(DataFrame) { ... } # camel/Pascal, noun verb # AFTER calculate_mean <- function(data) { ... } # snake_case, verb
= is correct inside calls. summarise(avg = mean(x)) andf(na.rm = TRUE) keep = — the <- rule is only for object assignment.
map() is correct when each element is a list/data frame/model(map(paths, read_csv) returns a list of tibbles). Only switch to map_*() when the per-element result is a single atomic value.
c(). .by = region, but.by = c(store, month) for two or more.
%>% is acceptable only if the code must run on R < 4.1 (no native pipe).Assume a modern R; emit |>.
summary.lm, print.tbl) andfor the conventional tidyverse meta-args (.by, .data, .before). Do not use dots as a word separator elsewhere.
cli::cli_abort() first element is unnamed (the headline); only thefollow-up bullets are named x = / i = / !=. Putting a name on the first element silently turns it into a bullet with no headline.
return() is fine for guard clauses (if (is.null(x)) return(NULL));prefer implicit return of the last expression for the normal path.
library(magrittr) for %>%; always use the built-in |>.group_by() |> ... |> ungroup(); always .by = inside the verb.df[[col]] / a string column; always embrace with {{ col }}.sapply(); always the typed map_dbl/chr/lgl/int().stop("..."); always cli::cli_abort(c(headline, x = ..., i = ...)).read.csv()/write.csv(); always read_csv()/write_csv().tolower()/grepl()/gsub()/trimws(); always the str_*() form.T/F; always TRUE/FALSE.NA/Inf; always NULL + if (!is.null()).%>% out of habit — it is the single most common base default.group_by() + summarise() + ungroup() triple instead of .by =.[[ ]], instead ofembracing the bare name with {{ }}.
sapply() (whose return type is unpredictable) instead of map_dbl().stop() with paste()-style string concatenation rather than astructured cli::cli_abort() bullet list.
gsub, read.csv) into otherwise-tidy code.TRUE/FALSE to T/F.-Inf/Inf/NA instead of NULL.<- not = · |> not %>% · .by = not group_by() · {{ }} not strings · map_dbl/chr/lgl/int() not sapply() · cli::cli_abort(c(...)) not stop() · read_csv/write_csv not base · str_*() not base · TRUE/FALSE not T/F · NULL + if (!is.null()) for options · data-first signature · snake_case · 2-space indent.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-12 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.5-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +41 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.5-flash | verified | 6/27/2026 | +96% |
Other measured skills in the registry, with their headline benchmark lift.