Skip to content

harmonize r style - #65

Merged
psadil merged 18 commits into
mainfrom
style/harmonize-r
Aug 6, 2026
Merged

psadil merged 18 commits into
mainfrom
style/harmonize-r

Conversation

@psadil

@psadil psadil commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

psadil and others added 16 commits August 2, 2026 18:29
Write the conventions down in contributing.md so the harmonization that
follows has something to point at: native pipe, one alphabetized setup
chunk, no pkg::fun(), individual packages over library(tidyverse),
current dplyr/tidyr idiom, \(x) lambdas, labs(), kebab-case chunk
labels, here() for data/ paths, and plain fences for illustrative code.

Also note that read.csv() in the CRF appendices is deliberate -- read_csv()
reports parsing errors on those raw exports -- so it is not a target.

This commit only touches chapters with no executable R, so nothing here
needs a re-render:

- cat12.qmd: drop the df-print front matter. The chapter has no R chunks,
  so it was configuring a table printer that never runs.
- genomic_imputation.qmd: snake_case and spacing in the two supplementary
  scripts. air and panache skip plain ```r fences, so these had never been
  formatted. Also drops a duplicated re-read of the same .fam file.
- contributing.md: the df-print example pointed at freesurfer.qmd, which
  has no front matter; bids-qc-joining.qmd is the actual example.

Declare r-here and r-tibble in pixi.toml. Both are used across the kits
but were only present transitively, the same gap 5f1722f closed for the
others. No package versions change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Code:
- library(tidyverse) -> dplyr, ggplot2, here, readr, the four packages
  the kit actually uses.
- read_csv() paths go through here() instead of being relative to the
  render directory.
- the one %>% becomes |>.

Prose:
- the chapter had "## Considerations While Working on the Project" twice,
  with near-identical paragraphs under "### Methods" and "### Data
  Generation". Merged into one section; the surviving paragraph drops a
  "(see below for more details)" that pointed at the duplicate.
- "functiona" -> "function", "assessement" -> "assessment".
- #| label: peak -> peek.

The rendered output also picks up the current text of
_snippets/a2cps_citations.qmd. fa43a07 edited that snippet without
clearing the frozen output of the chapters that include it, so this
chapter had been publishing the pre-edit citation blurb. eddyqc,
genetic_variants, mri-derivative-qc, mriqc and raw-mri are still stale
the same way and will correct themselves when they are re-rendered.

Rendered with LANG=en_US.UTF-8. Without it R falls back to the C locale
and pillar prints ASCII "~" where the committed output has "…", which
shows up as a spurious diff in every chapter that glimpse()es a frame.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
These 21 chapters already used |>, so this is the rest of the rules.

Setup chunks: one per chapter, immediately after the H1, alphabetized,
holding only packages the chapter actually calls. Moved out from
mid-document (freesurfer, fslanat, eddyqc, fcn) and from above the H1
(neural-pain-signatures). raw-mri's "vars" chunk keeps its job but no
longer doubles as the library block, and its .vars is renamed to
variables so there is no hidden top-level object.

Dropped six library() calls nothing used: tidyr in brainager, dplyr in
dwi_biomarker1, ggdist in postgift, and the lone readr in fmriprep,
qsiprep and reconstruction -- those three contain no R at all, so their
setup chunk and _freeze are gone entirely.

Removed 47 pkg::fun() prefixes and loaded the packages instead, adding
setup chunks to ids, mri-idps and nda-mri, which had none. Package
references in *prose* keep their prefix: "[`fs::dir_ls`]" tells a reader
where the function comes from, which is the point of the rule.

Also: six ~ .x lambdas become \(x); xlab()/ylab() become labs();
na.omit() becomes drop_na(); summarize() becomes summarise(); the
dangling comma in eddyqc's map() is gone; fcn's anti_join() states its
by = join_by(sub) instead of emitting a join message onto the page;
colour becomes color in mri-derivative-qc.

Two figures were publishing with no caption at all: brainager used
"#| caption:" and mriqc used "#| fig-caption:", neither of which is a
Quarto cell option, so both were silently ignored. Both are now fig-cap
and the captions render.

The seven executable cells that only display a command or a JSON payload
are plain fences now, per contributing.md -- shortcodes do not expand
inside executable cells. reconstruction had a stray "#| eval: false"
sitting inside a *plain* fence, where it rendered as a literal line of
code.

fslanat's two separate() calls become separate_wider_delim(). They need
too_many = "drop": names like "Left-Thalamus-Proper" and
"Left-Accumbens-area" have to reduce to their stem for the FSL rows to
join the FreeSurfer ones, which separate() was doing via a warning that
the project-wide warning: false hid. Verified identical on the real
structure names.

NOT RE-RENDERED HERE, freeze deliberately left at its previous state:
fcn, mri-derivative-qc, postgift, dwi_biomarker1, mriqc. This working
copy holds a partial data/ tree, and re-rendering them would republish
results computed on it -- postgift's model goes from 1286 participants
to 584. Their sources are harmonized but their frozen output is not, so
the preview will flag them; re-render on a machine with the full release
before merging. The source edits are behaviour-preserving: running the
old and new sources against the same data leaves identical objects, with
a HEAD-vs-HEAD control to separate the edits from fcn's own row-order
non-determinism.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
biospecimen, Funtional and Imaging: 594 %>% become |>.

The only magrittr placeholders in these files were 11 nrow(.) dots, all
one shape:

  dict <- dict %>%
    slice(seq_len(nrow(.))) %>%
    add_row(.after = nrow(.), !!!new_row)

slice(seq_len(nrow(.))) selects every row in order, so it does nothing,
and .after = nrow(.) is add_row()'s default. Both are gone; the chained
case in Funtional still appends in the same order. The remaining dots are
purrr lambda arguments in ~ replace_na(., "") and are untouched -- those
are bound by the lambda, not the pipe.

The pipe sweep itself is proven inert: with the dots handled first, the
before and after sources parse to identical ASTs across 49, 58 and 92
top-level expressions.

library(tidyverse) becomes the packages each file actually calls. forcats
and janitor were loaded by all three and used by none; hablar is only
used by Funtional, for retype().

here::here() becomes here() at 16 sites -- the files used the bare form
for reads and the namespaced form for writes, for no reason. top_n(1, x)
becomes slice_max(x, n = 1) at 12 sites, mutate_all() becomes
across(everything()) at 6, and a trailing TRUE ~ becomes .default = at 8.

Two labels: data-diciontary -> data-dictionary, and the Imaging kit had a
chunk labelled "qst" copied from the QST kit, now imaging-qst.

Verified twice. Running the old and new sources against the same data
leaves every object identical up to row order -- slice_max() sorts where
top_n() preserved input order -- and the rendered output is byte-for-byte
unchanged in all three chapters, so that reordering never reaches a page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
885 %>% become |>. This file had both magrittr placeholder shapes.

The 14 left_join(., y, by = ...) sites are the ones worth care. magrittr
suppresses first-argument insertion when a `.` appears as a top-level
argument, so `x %>% left_join(., y)` already means `left_join(x, y)` --
verified -- and the fix is to delete the `.,` line. Dropping just the dot
instead would leave `left_join(x, ., y)`, which is a hard error under |>,
not a silent wrong answer.

The other 20 dots are the same no-op slice(seq_len(nrow(.))) and default
.after = nrow(.) removed from the other CRF appendices.

With the dots gone, the sweep is proven inert: the before and after
sources parse to identical ASTs across 166 top-level expressions.

library(tidyverse) becomes the seven packages the file uses. here::here()
becomes here() at 4 sites, top_n(1, x) becomes slice_max(x, n = 1) at 4,
mutate_all() becomes across(everything()) at 4, and a trailing TRUE ~
becomes .default = at 39.

The 40 rowwise() calls are LEFT ALONE. They are not the redundant
rowSums(across(...)) wrapper they look like from a distance -- they are
followed by mean(c(a, b, c)), which is scalar-reducing. Under rowwise()
that is a per-row mean; without it, it collapses to one grand mean
recycled down the column. On c(1,10)/c(2,20)/c(3,30) that is "2, 20"
versus "11, 11". Removing them would silently corrupt every derived QST
summary in the file.

Verified: old and new sources leave all 140 objects identical up to row
order (slice_max sorts where top_n preserved input order), and the
rendered output is byte-for-byte unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
395 %>% become |>. This file held 68 of the repo's 113 magrittr
placeholders, in two shapes that both translate to pick():

  60x  rowSums(!is.na(select(., all_of(v))))  ->  pick(all_of(v))
   6x  rowSums(!is.na(.[, cols]))             ->  pick(all_of(cols))

pick() and select(., ...) diverge silently when an earlier argument of
the same mutate() creates a column in the selection, so that is checked
rather than assumed: all 74 dot-bearing mutate() calls in this file take
exactly one argument, which makes the two equivalent. The check is in the
harness and should be re-run before anyone merges adjacent mutate() calls
here -- psycho_social:1228 (all_koos) and :2147 (ssi_all) select columns
built by preceding pipe stages and would break quietly.

The `.[, cols]` rewrite is anchored on the ", " so it cannot touch the
prose citations `.[@wolfe2016]` and `.[Berkeley personality lab]`, which
look like the same pattern but are markdown.

The remaining 22 dots are lambda arguments in ~ if_else(is.na(.), m, .)
and are left alone.

With the dots handled, the sweep is proven inert: identical ASTs across
420 top-level expressions. There is now no %>% anywhere in the repo.

library(tidyverse) becomes the eight packages actually used -- GGally,
ComplexUpset, gt and janitor were loaded across 4988 lines and never
called, and ggplot2 was loaded twice. This was also the only setup chunk
in the book without a label.

here::here() becomes here() at 31 sites, a trailing TRUE ~ becomes
.default = at 47, ifelse() becomes if_else() at 31 (the file already
mixed both), applyFilter becomes apply_filter, and the trailing
return(filtered_data) is dropped.

Labelling the setup chunk shifts every downstream unnamed-chunk-N by one,
so 53 figures are renamed. Their content is unchanged: hashing the
decoded pixels of every figure before and after gives the same 53 distinct
images, with none lost and none gained.

Verified: old and new sources leave all 157 objects identical -- exactly,
with no sorting needed -- and the rendered output differs only in those
figure filenames and in the prose describing apply_filter. The numeric
output is byte-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tive disk

Every write chunk in the CRF and psychosocial appendices builds its path
with here(), and 125 of those paths do not point at a directory that
exists:

  37  omit "pre-surgery", e.g. here("data", "blood-draw", ...)
  58  say "Reformatted" where the directory is "reformatted"
  30  say "Psychosocial" where the directory is "psychosocial"

All of them sit in chunks marked eval: false, so nothing has ever run
them, and macOS is case-insensitive by default, so the casing errors
would work on a developer's laptop and fail on Linux -- which is where
these scripts are meant to run. All 11 distinct write targets now resolve
against the real data/ tree.

The rendered output changes only where the prose repeated the mistake:
four sentences in biospecimen and two in Imaging told the reader to look
in a folder named "Reformatted". Nothing else in any of the five chapters
moves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
postgift plotted the wrong data. It computes run1_amplitudes_without_red,
the prose above the figure says "let's exclude the 'red' scans", and the
plot was then handed run1_amplitudes. The published MA plot has been
showing the scans the text says are excluded. Source only -- this chapter
is in the set awaiting a full-data re-render.

postdtifit coerced sub with as.numeric() and then joined on the resulting
double; every sibling kit uses as.integer(). Rendered output is
unchanged, so the join was working, but the type disagreed with the five
other chapters doing the same thing.

bedpostx assigned gamma <- 1.8 and never read it, hardcoding 1 / 1.8 in
four places instead. Now wired up, and renamed to gamma_correction so it
does not shadow base::gamma. Its comment said "take every 10th slice"
over a filter that takes every 5th; the prose above the chunk already
said every fifth. Numerically identical output.

genetic_variants read and wrote through two hardcoded /Users/sethberke
paths, which nobody else can run. They are relative now, matching the
./-prefixed style used throughout that script. No personal paths remain
anywhere in the repo.

genetic_variants also called read_table2(), deprecated in readr 2.0, now
read_table().

Its rendered output additionally picks up the current text of
_snippets/a2cps_citations.qmd, which fa43a07 edited without clearing the
freeze of the chapters that include it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Renames 20 chunk labels that were actively unhelpful, and leaves the
other 465 unlabelled chunks alone. Labelling is not required here --
.panache.toml switches missing-chunk-labels off -- and labelling
everything would rename 142 tracked figures for a change no reader sees.
contributing.md said "label every chunk", which contradicted the repo's
own config; it now describes what to do when you do label one.

Eight labels shadowed a function name: filter and gather (eddyqc),
freesurfer (fslanat), fcn, and session/image/rating/coordinate
(mri-derivative-qc). load, join and plot are conventional chunk names and
are kept. Twelve ran words together: helpercor, helpertimecourse,
mainplot, loadfalff, loadstats, loadapi, showimg, imgmeta, fsltidy,
visitsmapping, brainage2, pairs2.

Three of those labels name a figure, so brainage2-1.png,
pairs2-1.png and mainplot-1.png are renamed. The superseded files are
deleted rather than left behind as orphans.

genetic_variants.qmd had never been formatted. air and panache skip plain
```r fences, and they were also skipping four of this file's ```{r}
chunks -- panache reported the file clean while one line ran to 589
characters. Piping each fence body through air directly fixes it, and the
hooks neither complain nor revert afterwards.

That file also carried 93 identifier occurrences in dot.case or camelCase
(number.geno, matrix.missed, bioDF_Mapping_Plink, problem_IIDs), now
snake_case, and three top-level assignments with = that air rewrote to
<- on its own. Its three executable tibble::tribble() calls are
de-namespaced with library(tibble) added to the setup chunk.

The two supplementary scripts in <details> blocks keep library(tidyverse)
and one jsonlite:: call. They are standalone programs meant to run
outside this project, on files that are not in the release, and two of
their dependencies are not in the environment at all.

Verified: eddyqc, fslanat, postdtifit and ids render byte-identically;
gift, brainager and freesurfer differ only in the renamed figure path;
genetic_variants' 19 executed table rows are unchanged, with the rest of
its diff being the reformatted display fences, which are page content.

ids needed a fix to the comparison rather than to the code -- DT mints a
fresh htmlwidget id on every render, the same way gt does for tables, so
its 8 "changed" lines were noise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
open_dataset() returns rows in whatever order Arrow's threaded reader
delivers them, and the select() here drops ses, task and run -- so the
18132 rows collapse to 5862 distinct (sub, source, target) keys, roughly
three rows per key, and head() published whichever one arrived first.
Rendering the unmodified chapter twice produced different numbers on the
page.

Sorting before the select makes the output stable: three consecutive runs
now agree, and they agree with the values in the committed freeze.

Source only -- this chapter is in the set awaiting a re-render on a
machine with the full release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The earlier reproducibility fix covered the DMN table but missed the
difumo one feeding the discriminability analysis, which had the same
problem: open_dataset() row order is not stable, and head() published
whichever rows arrived first.

This chapter's data turned out to be complete after all, so it is no
longer waiting on a full-data re-render. Evidence: the six rows the
committed freeze displayed (sub 10045, rest1, source 1, targets 10-15)
reproduce from the current data to seven decimal places, and every
numeric value outside the two head() tables is unchanged. The small
participant count in subs_with_all_runs is just the filter(n == 4)
doing its job -- keeping only participants with all four functional
scans -- not evidence of a partial release.

The discriminability statistic itself was never order-sensitive: the six
values and their row order are identical with and without the sort, from
either source. Only the two displayed tables move, and now they are
deterministic.

plot-discriminability-1.png changes for a reason unrelated to any of
this: rendering HEAD's own unmodified source today produces a different
PNG than the committed one, with identical inputs. That is graphics
environment drift, not data and not this branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
geom_jitter() places points at random and nothing set a seed, so this
figure had never been reproducible: rendering the same unmodified source
twice produces PNGs differing by 0.91 on a 0-1 scale, which is the same
magnitude as the difference against the committed freeze. Now
geom_point(position = position_jitter(seed = 1)), and two independent
renders come out byte-identical.

This also settles the chapter: its data is complete, so it is no longer
waiting on a full-data re-render. The networks tree holds 1266 subjects
x 4 modules = 5064 rows, more coverage than participants.tsv (1019), and
the rendered markdown is byte-identical to the committed freeze. The
figure churn that had it held back was the unseeded jitter, not a
partial release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
position_jitter(height = 0) still jitters horizontally, and nothing set a
seed, so the motion comparison figure changed on every render. Seeded
now, and two independent renders come out byte-identical for both
figures in the chapter.

This chapter's data is complete, so it is no longer waiting on a
full-data re-render: nrow(all_bold) publishes 1245400 both in the
committed freeze and now, the MRIQC reference database is all 252
parquet files, and the group_bold table is unchanged. It was held back on
figure churn that turned out to be the unseeded jitter.

The remaining output changes are both intended. The quantile figure gains
its caption -- it was written as "#| fig-caption:", which is not a Quarto
cell option, so it had been silently dropped and the figure published
with no caption at all. And the citation blurb picks up the current text
of _snippets/a2cps_citations.qmd, which fa43a07 edited without clearing
the freezes that bake it in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two outputs in this chapter were picked non-deterministically from
duckdb reads, whose row order is not stable:

  slice_head(n = 1, by = n_clicks)   -- one arbitrary image per click count
  distinct(sub, task, run) |> head() -- an arbitrary six failed coregistrations

Running the first query three times returned img_id 2784, then 576, then
2784 again. Both are sorted now and three consecutive runs agree.

Because the selection was arbitrary rather than wrong, the displayed
image changes: the chapter now shows img_id 576 (sub-10040) where it
showed 2784 (sub-10168). Both have 8 clicks; 576 is simply the lowest id
among them, and it will stay that way from here.

This chapter's data is complete, so it is no longer waiting on a
full-data re-render: nrow(definite_coregistration_failures) publishes 126
both in the committed freeze and now, and all four qc parquet inputs are
present. It was held back because its subject ids moved between renders,
which turned out to be this, not a partial release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The amplitude tree now holds 981 subjects across 1962 parquet files, up
from 584 across 1210 when this branch started, and the count is stable
rather than mid-sync. All 981 appear in participants.tsv, and every
model/run combination agrees on the same 981, so the tree is internally
consistent.

The mixed model therefore moves: 165480 observations over 921
participants after the red-scan exclusion, where the previous frozen
output reported 230160 over 1286.

Also carries this branch's earlier fix to plot run1_amplitudes_without_red
rather than run1_amplitudes, which is what the surrounding prose has
always described, so the MA plot changes for that reason as well as the
new data.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
github-actions Bot added a commit that referenced this pull request Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

The preview for this pull request has been removed.

github-actions Bot added a commit that referenced this pull request Aug 6, 2026
@psadil
psadil merged commit 4d73cf0 into main Aug 6, 2026
3 checks passed
github-actions Bot added a commit that referenced this pull request Aug 6, 2026
@psadil
psadil deleted the style/harmonize-r branch August 13, 2026 19:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant