Version it, pin it, publish it

Reproducible Workflows · Session 3

1 Recap: Sessions 1 & 2 → Session 3

1.1 Where we are

You have a working, versioned portfolio book; today we make it durable and shareable.

  • Session 1: write, Quarto documents, transparency.
  • Session 2: source, real data, cleaned, pinned, and under git in your book’s Chapter 1.
  • Session 3 (today): deepen git (PRs, review), pin the environment (uvr), fix randomness ({withr}), publish (Posit Connect), and finish Chapter 1.

Trade “it works on my machine” for: versioned, pinned (version + packages), published at a URL anyone can open.

2 Deepen your git

2.1 Why version control (recap)

Your smallest reproducible unit is not a file: it’s the history of changes to a project.

  • Undo anything: every state of your project is recoverable.
  • Answer “what changed and when?”: a full log of your analysis’s evolution.
  • Collaborate safely: branches, PRs, review, no clobbering.
  • A backup that lives off-machine: your laptop dying stops being a catastrophe.

Git is the time-machine around your analysis; today we make the habit rigorous enough to share and review.

2.2 Commit, branch, push: already yours

You built these reflexes in Session 2; today we sharpen them:

git status                   # your compass: where am I?
git add <file>               # stage what you want to keep
git commit -m "message"      # snapshot with a note
git checkout -b <branch-name>   # never work on main
  • Commit often: small, descriptive commits beat rare mega-commits.
  • Never push to main: a PR is how a change reaches the shared history.

usethis::use_git() and the Positron source-control pane do all of this with clicks.

2.3 Keep a branch aligned with main

Before you touch a branch, fold in whatever main gained, so you never diverge silently:

git checkout main
git fetch --all --prune     # see remote changes
git pull                    # bring main up to date
git checkout <branch-name>
git merge main              # fold main into your branch

Do this before changing a branch, and your branch tracks main: conflicts largely disappear.

2.4 A conflict, and what it looks like

Two people change the same line, git can’t choose: it marks the file for you.

# my-analysis.R (merge conflict)
<<<<<<< upstream
penguins_clean <- janitor::clean_names(penguins)
=======
penguins_clean <- readr::read_csv("raw.csv") |>
                   janitor::clean_names()
>>>>>>> incoming
# a genuine .csv conflict also shows this
<<<<<<< upstream
394,Adelie,2027
=======
394,Adelie,2026
>>>>>>> incoming
  • Text between <<<<<<< and >>>>>>> is what each side proposed; delete the markers and keep the right answer.
  • In Positron: use the inline merge editor (Source Control → merge) to pick upstream / incoming / both.

A conflict just means two edits hit the same place: resolve it, commit, move on.

2.5 Pull requests

A PR is “please merge my branch into main”, reviewed before it lands:

  • Positron way: the Source Control pane → create a pull request via the GitHub extension from your pushed branch.
  • GitHub way: push the branch, then GitHub’s web UI offers “Compare & pull request” from the repo page.
  • A PR is the reviewable unit: it’s where your Chapter 1 gets feedback.

Merge to main only via a PR; main stays an always-working, always-reproducible line.

2.6 Review, comment, accept

A PR is a conversation, not just a merge button:

  • Assign a reviewer (your instructor, a classmate) to the PR.
  • Comment on specific lines: both sides of the diff can leave notes.
  • Request changes or approve: the author updates the branch, pushes, and the PR updates.
  • Works the same in Positron (GitHub extension) and on the GitHub web site.

Review is how your work becomes reproducible to another human; you’ll use this on Chapter 1.

2.7 Your instructor, in the loop

Reserve a branch per idea (e.g. eda-chapter-1), push it, and open a draft PR when you’d like feedback:

  • During the gap and today, the PR is how your instructor comments on and reviews your Chapter 1.
  • Address comments, push, and the PR updates; no emailing files around.

3 Pin your environment: uvr

3.1 The moving-environment problem

Your analysis depended on package versions, which silently change:

  • install.packages() updates packages globally, and today’s tidyverse may differ from last month’s.
  • The R version itself also moves: a book may render today and break next year on a newer R.
  • “Works on my machine” becomes “worked on that machine, that day”.

The fix: pin the versions your project needs, committed with the code. uvr pins the packages and the R version.

3.2 A widely used alternative: renv

renv gives each project its own library and locks package versions with a renv.lock file. Widely used and proven, but it does not version the language: the R version is left floating.

  • uvr pins packages and the R version, so a fresh clone rebuilds exactly.
  • That one addition is what makes the whole environment reproducible, not just the packages.

Use what fits: renv for packages only, or uvr to also pin R itself. This deck follows uvr.

3.3 uvr: pin R itself, not just packages

uvr wraps uv (the Python manager) so R gets the same “one engine, both languages” discipline:

  • uvr::init() sets up a project manifest + lockfile (a uv project).
  • uvr::r_install("4.6.1") installs a project-managed R version.
  • uvr::r_pin("4.6.1") records that version in the manifest, so it’s committed and reproducible.
  • uvr::add("tidyverse") adds to the manifest, regenerates the lockfile, and installs, all in one call.

r_list() shows versions; r_use() picks one for the session, r_pin() records it in the project (that’s what survives a fresh clone).

3.4 Install uvr

The companion R package from GitHub, then the uvr binary:

pak::pak("nbafrank/uvr-r")   # the uvr R package, from GitHub
uvr::install_uvr()           # installs the uvr binary (cross-platform)
  • One-time setup: the R package is the interface; install_uvr() puts the engine on your machine.
  • It relies on uv, the same tool that manages Python later in your book.

Installing the R package first, then the binary, keeps the whole uvr workflow runnable from R functions inside your project.

3.5 What a lockfile actually pins

uv.lock (via uvr) records each R/Python package with exact version + hashes, and pins the R version too. It stores only the recipe to rebuild them, not the packages themselves.

[r]
version = "4.6.1"

[[package]]
name = "DBI"
version = "1.3.0"
source = "cran"
raw_version = "1.3.0"
url = "https://cran.r-project.org/src/contrib/DBI_1.3.0.tar.gz"
checksum = "md5:ce5e3153cc7395a2aac452db1099d003"

A lockfile isn’t magic; it’s an explicit, machine-readable record of what you ran with. Diff it over time to see exactly when an environment changed.

3.6 R and Python, side by side

Every reproducibility tool you’ve met has an equivalent in Python, with the same ideas:

Role R Python
Environment uvr uv
Data & artifacts {pins} pins
Model versioning/deploy {vetiver} vetiver

uvr literally runs on uv, so R and Python share one engine. Your book is Python-compatible by design, and the reproducible pipeline is the same in both languages.

4 Reproducible habits: randomness

4.1 Random numbers break reproducibility

Any code that uses randomness gives a different answer on every run, unless told otherwise:

  • Sampling, simulations, plot jitter, cross-validation splits: all change a figure or number each render.
  • A bare set.seed() at the top of a document leaks into the whole session and never restores it.
  • Result: the same .qmd renders “differently”, so a reader can’t reproduce your exact figure.

A document whose figure changes every render isn’t reproducible. Fix the seed where the randomness happens, not globally.

4.2 Scope a seed with {withr}

{withr} sets an option for one block and restores it afterward, locally and explicitly:

withr::with_seed(2026, {
  # anything random here, for this block only
  sample(penguins$species, 5)
})
[1] Gentoo    Adelie    Chinstrap Adelie    Adelie   
Levels: Adelie Chinstrap Gentoo
# outside the block, the previous seed state is restored
  • withr::with_seed(seed, { ... }): set the seed for exactly that expression.
  • withr::local_seed(seed) inside a chunk: same idea, restored when the chunk ends.
  • Fix the figure inside the cell that draws it; every render then matches.

Make randomness intentional and local; your Chapter 1’s figures become exactly reproducible without leaking a seed into the session.

5 Publish it

5.1 What Posit Connect is

A published analysis is one anyone can open at a URL, with no local environment required:

  • Posit Connect hosts R and Python content (books, reports, apps, dashboards) behind a stable URL.
  • Publishing pushes your rendered book to the server; readers just open the link.
  • This is the “anywhere” end of the spectrum: reachable, shareable, current.

A URL is the end state of the loop: data → analysis → report → versioned → published.

5.2 Pushing a Quarto book

Rendering is local and reproducible; publishing is a one-step deploy:

quarto render          # build the book (local, reproducible)
quarto publish connect # push to Posit Connect
  • First publish asks you to log in / configure the server once.
  • Every later publish updates the same URL: your portfolio stays current.

The URL shares work outside the notebook: an employer, a teammate, or your future self all see the same, current, reproducible book.

5.3 Proof: reproduce it on a blank machine

The real test of everything this course taught: hand someone the recipe and see if the book reappears:

git clone <your-url>        # 1. get the source + history
uvr::sync()                 # 2. rebuild env + R version
quarto render               # 3. rebuild the book
  • Three commands, no “works on my machine”: a second person (or your future self) rebuilds your exact book.
  • If this doesn’t work on a fresh clone, you’ve found a reproducibility gap to fix.

Publishing gives a URL to send; clone + sync + render proves the loop closed.

5.4 The full loop, closed

Everything this course taught, in one connected picture:

  1. Write the analysis in Quarto: source is the report.
  2. Source the data by code: {pins} versions the fetched copy.
  3. Version with git: every step reversible, reviewed via PRs.
  4. Pin your environment with uvr/uv.
  5. Deploy your model with {vetiver}, pinned like your data.
  6. Publish to a URL: reachable and shareable.

Miss any link and the chain weakens. Keep all six and your analysis is reproducible by anyone, anywhere, at any time.

6 Portfolio handoff

6.1 How your book keeps growing

Your Chapter 1 was the seed: now it’s part of a living artefact:

  • Later courses append chapters to your book (02-semester-2.qmd, 03-semester-3.qmd): the placeholders you kept are the roadmap.
  • Because everything is versioned, pinned, and published, those additions are themselves reproducible.
  • By term’s end: a multi-chapter, multi-language, published portfolio.

The conventions you followed make it easy for other instructors to build on your work.

6.2 Your EDA chapter: the rubric

How Chapter 1 will be assessed: use this as your checklist for the work block that follows:

Criterion Looks like
Question a clear one-sentence motivation up front
Reproducible data fetched by code and pinned with {pins}; provenance recorded
Cleaning decisions documented why, not just what
Evidence captioned figures, each referred to in prose
Honesty states what was found and what wasn’t
Structure the 5-section recipe (≈ IMRAD)

6.3 Your turn: finish Chapter 1 (most of the session)

Work block (the rest of the session). Turn the EDA you built into a finished Chapter 1, then publish it.

  1. Apply the rubric: complete the question, cleaning decisions, captions, and findings.
  2. Pin your data ({pins}) and environment (uvr); scope randomness with {withr}.
  3. Commit to a branch, push, open a PR, address the instructor’s review.
  4. Publish the book to Posit Connect and open the URL.

6.4 Models and the Python side

Your portfolio may grow models and extra languages: same reproducible pipeline, meeting them at the door:

  • {vetiver} pins a trained model + the versions that built it, right beside your data on the same board.
  • Where a later chapter ships a model, {vetiver} gives it a version to deploy, not just saveRDS() and hope.
  • Python twins for everything: uvr↔︎uv, {pins}↔︎pins, {vetiver}↔︎vetiver.

The rubric’s criteria scale to any language: source by code, pin data and models, version, publish. A {vetiver} model re-deploys anywhere.

6.6 Three moves, one career

Write, version, publish.

6.7 You built this

By the end of this course you can turn any analysis into a versioned, pinned, published, reproducible portfolio.

Write

Your analysis lives in a Quarto doc: source is the report.

Source & Version

Data and models pinned ({pins}/{vetiver}); history kept by git (branches, PRs); environment and R version pinned by uvr.

Publish

The whole thing behind a URL anyone can open, reproducible anywhere.

6.8 Your portfolio, published

From one-off report to a career you keep.

Every chapter is a reproducible chapter

Everything from this course (the Quarto report you source() from, the git history, the uvr/uv lockfile, the published URL) stacks into one portfolio you keep growing. Bring it to your next course, your internship, your first job: an analysis anyone can open, re-run, and trust. That is the reproducible habit, made visible.

6.9 Thank you: keep the habit

Reproducible is a habit, not a tool.

Keep your portfolio growing: every course adds a chapter, and every chapter is reproducible by default. R · Quarto · git · uvr/uv · Posit Connect. Thank you!