R Workflows

Adam Shen

December 4, 2025

Packages

# install.packages("pak")
pak::pak(c("here", "pins"))
pak::pak("adamoshen/pinsqs")

I use {pak} to install my packages because it typically has better success at installing packages on the first try.

Folder structure

The R Project (.Rproj) file

  • An R Project can be created by clicking the blue cube in the top-right corner of RStudio and selecting New Project....

  • The .Rproj file is more than just a shortcut to opening a new R session to the desired directory.

  • It can also act as an anchor or a reference point when specifying paths in a working document related to a project.

  • When a project has a .Rproj file, unless otherwise specified, it should always be assumed that paths are relative to the .Rproj file.

Recall

For reproducibility and portability, your code should always use relative paths, never absolute paths!

A simple project (no .Rproj)

farm-animals
├── 00-data-prep.Rmd
├── 01-analysis.Rmd
└─── data
     ├── chickens.csv
     └── cows.csv
  • In this example, the .Rmd files and the data folder are all at the same level.

  • A path specification to a data file from the .Rmd document is straightforward and might look like:

    chickens <- readr::read_csv("data/chickens.csv")

A complicated project (no .Rproj)

farm-animals
├── data
│   ├── chickens.csv
│   └── cows.csv
├── models
│   ├── chicken-model.rds
│   └── cow-model.rds
├── R
│   ├── misc-script1.R
│   └── misc-script2.R
└── Rmd
    ├── 00-data-prep.Rmd
    └── 01-analysis.Rmd
  • In this example, many items live in their own folders. It is unclear how paths should be specified.
  • If my main analysis scripts are the .Rmd files, should my working directory be set to the Rmd folder?

A complicated project (no .Rproj)

farm-animals
├── data
│   ├── chickens.csv
│   └── cows.csv
├── models
│   ├── chicken-model.rds
│   └── cow-model.rds
├── R
│   ├── misc-script1.R
│   └── misc-script2.R
└── Rmd
    ├── 00-data-prep.Rmd
    └── 01-analysis.Rmd
  • When knitting .Rmd files, the default setting knits in the directory of the .Rmd file (in this case, the Rmd folder). Paths specified when running code in your .Rmd file interactively may not work during rendering time if your working directory is not the directory of the .Rmd file.
  • What if my .Rmd file also sources a .R file, where the .R file reads in one of the data files? How should paths across files be specified in this scenario?

The {here} package

The {here} package solves our problems by making use of the .Rproj file as the reference point.

  • The main function is the here() function, i.e. here::here().

  • here::here() is simply a path generator -- you supply a path relative to the .Rproj file, and it returns an absolute path.

Tip

We typically don't attach the {here} package via library(here) since we only use one function. It's also kind of fun to write here::here().

Using here::here()

farm-animals
├── data
│   ├── chickens.csv
│   └── cows.csv
├── farm-animals.Rproj
├── models
│   ├── chicken-model.rds
│   └── cow-model.rds
├── R
│   ├── misc-script1.R
│   └── misc-script2.R
└── Rmd
    ├── 00-data-prep.Rmd
    └── 01-analysis.Rmd

We can construct paths by supplying a full relative path:

here::here("data/chickens.csv")
[1] "C:/Users/ShenA/Documents/farm-animals/data/chickens.csv"

Using here::here()

farm-animals
├── data
│   ├── chickens.csv
│   └── cows.csv
├── farm-animals.Rproj
├── models
│   ├── chicken-model.rds
│   └── cow-model.rds
├── R
│   ├── misc-script1.R
│   └── misc-script2.R
└── Rmd
    ├── 00-data-prep.Rmd
    └── 01-analysis.Rmd

Or we can supply a number of individual strings:

here::here("data", "chickens.csv")
[1] "C:/Users/ShenA/Documents/farm-animals/data/chickens.csv"

Using here::here()

farm-animals
├── data
│   ├── chickens.csv
│   └── cows.csv
├── farm-animals.Rproj
├── models
│   ├── chicken-model.rds
│   └── cow-model.rds
├── R
│   ├── misc-script1.R
│   └── misc-script2.R
└── Rmd
    ├── 00-data-prep.Rmd
    └── 01-analysis.Rmd

Note that supplying a character vector has a different effect:

here::here(c("data", "chickens.csv"))
[1] "C:/Users/ShenA/Documents/farm-animals/data"        
[2] "C:/Users/ShenA/Documents/farm-animals/chickens.csv"

Using here::here()

farm-animals
├── data
│   ├── chickens.csv
│   └── cows.csv
├── farm-animals.Rproj
├── models
│   ├── chicken-model.rds
│   └── cow-model.rds
├── R
│   ├── misc-script1.R
│   └── misc-script2.R
└── Rmd
    ├── 00-data-prep.Rmd
    └── 01-analysis.Rmd

Also note that here::here() is just a path constructor -- it does not verify that the path exists!

here::here("this/doesnt/exist.html")
[1] "C:/Users/ShenA/Documents/farm-animals/this/doesnt/exist.html"

Actual use case

farm-animals
├── data
│   ├── chickens.csv
│   └── cows.csv
├── farm-animals.Rproj
├── models
│   ├── chicken-model.rds
│   └── cow-model.rds
├── R
│   ├── misc-script1.R
│   └── misc-script2.R
└── Rmd
    ├── 00-data-prep.Rmd
    └── 01-analysis.Rmd
  • With a .Rproj file and the {here} package, regardless of whether we are in an R script or an Rmd document, we can always reference the chickens.csv file using:

    here::here("data/chickens.csv")
  • We can then read in chickens.csv as usual, where here::here(...) takes the place of the file path we would usually specify:

    chickens <- readr::read_csv(here::here("data/chickens.csv"))

Saving, organising, (and sharing) data and models

Has this happened to you?

farm-animals
├── data
│   ├── clean-data.rds
│   ├── clean-data-scaled-15.rds
│   ├── clean-data-scaled-20.rds
│   ├── clean-data-with-extra-covariates.rds
│   └── raw-data.rds
├── farm-animals.Rproj
├── models
│   ├── bad-model1.rds
│   ├── bad-model2.rds
│   ├── good-model.rds
│   └── okayish-model.rds
├── R
│   ├── misc-script1.R
│   └── misc-script2.R
└── Rmd
    ├── 00-data-prep.Rmd
    └── 01-analysis.Rmd
  • You want to be as descriptive as possible with file naming, but also, the longer the filename is, the more typing you have to do to read it back in.
  • You might have a reference sheet elsewhere that describes the parameter sets used for the data and models, but it's not clear to collaborators what/where this file may be.

The {pins} package

The {pins} package allows you to save miscellaneous objects ("pins") to a remote/local folder ("pin board"). The {pins} package is developed by Posit and is available for both R and Python!

  • Pins are saved as a folder containing the data file and metadata. Fields include a title and a description so that you can be as descriptive as needed.

  • A pin board can be "versioned" meaning that pins are never overwritten and all versions of pins are kept (with timestamps). This is useful if you need to revert to a previous version of a pin.

  • Pin boards are easy to share so that your analysis code is reproducible -- as a pin board is simply a folder, you just need to ensure that collaborators have access to the folder.

  • Pins boards supported include local folders and folders found in remote storage drives/containers (might not be approved for usage at work though).

Core functions

  • pin_write(): Write an object to a pin board.

  • pin_read(): Read an object from a pin board.

  • pin_search(): List all objects on a pin board.

A quick example

library(tidyverse)
library(pins)

Create the pin board

Consider our previous setup but with an empty data folder that we wish to use as our pin board:

farm-animals
├── data
├── farm-animals.Rproj
├── models
│   ├── chicken-model.rds
│   └── cow-model.rds
├── R
│   ├── misc-script1.R
│   └── misc-script2.R
└── Rmd
    ├── 00-data-prep.Rmd
    └── 01-analysis.Rmd

Here, db stands for "data board" (less typing).

db <- board_folder(here::here("data"))

Create the pin board

Note

This needs to be declared at the beginning of all your scripts!

db <- board_folder(here::here("data"))

Combining {here} with {pins} we can place pin boards wherever we want!

Note

If the folder does not already exist, board_folder() will create it.

Write some data to the pin board

Create some data:

new_rock <- datasets::rock %>%
  as_tibble() %>%
  mutate(blah = area * shape)

new_rock
# A tibble: 48 × 5
    area  peri  shape  perm  blah
   <int> <dbl>  <dbl> <dbl> <dbl>
 1  4990 2792. 0.0903   6.3  451.
 2  7002 3893. 0.149    6.3 1041.
 3  7558 3931. 0.183    6.3 1385.
 4  7352 3869. 0.117    6.3  861.
 5  7943 3949. 0.122   17.1  972.
 6  7979 4010. 0.167   17.1 1333.
 7  9333 4346. 0.190   17.1 1770.
 8  8209 4345. 0.164   17.1 1347.
 9  8393 3682. 0.204  119   1709.
10  6425 3099. 0.162  119   1043.
# ℹ 38 more rows

Write some data to the pin board

Write it to the board:

db %>%
  pin_write(
    new_rock,
    name = "new-rock", # What we use to refer to it
    title = "A new data set based off of `rock`",
    description = "Take the rock dataset and create a new variable called `blah` by multiplying `area` with `shape`"
  )

Write a model to the pin board

Create a linear model:

idk_model <- lm(peri ~ perm + blah, data = new_rock)

broom::tidy(idk_model)
# A tibble: 3 × 5
  term        estimate std.error statistic  p.value
  <chr>          <dbl>     <dbl>     <dbl>    <dbl>
1 (Intercept) 2475.      247.        10.0  5.11e-13
2 perm          -2.60      0.246    -10.6  8.31e-14
3 blah           0.843     0.139      6.07 2.45e- 7

Write a model to the pin board

Write it to the board:

db %>%
  pin_write(
    idk_model,
    name = "idk-model",
    title = "Just some model, idk",
    description = "Legit just throwing stuff together"
  )

Inspect our pin board

db %>%
  pin_search()
# A tibble: 2 × 6
  name      type  title                 created             file_size meta      
  <chr>     <chr> <chr>                 <dttm>              <fs::byt> <list>    
1 idk-model rds   Just some model, idk  2025-12-07 00:15:18     3.29K <pins_met>
2 new-rock  rds   A new data set based… 2025-12-07 00:15:14     1.38K <pins_met>
db %>%
  pin_meta("idk-model")
List of 13
 $ file       : chr "idk-model.rds"
 $ file_size  : 'fs_bytes' int 3.29K
 $ pin_hash   : chr "eed4db53af399021"
 $ type       : chr "rds"
 $ title      : chr "Just some model, idk"
 $ description: chr "Legit just throwing stuff together"
 $ tags       : NULL
 $ urls       : NULL
 $ created    : POSIXct[1:1], format: "2025-12-07 00:15:18"
 $ api_version: int 1
 $ user       : list()
 $ name       : chr "idk-model"
 $ local      :List of 3
  ..$ dir    : 'fs_path' chr "C:/Users/Adam/My Drive/GitHub/deep-dives/data/idk-model/20251207T051518Z-eed4d"
  ..$ url    : NULL
  ..$ version: chr "20251207T051518Z-eed4d"

Read items from the board

some_data <- db %>%
  pin_read("new-rock")

some_data
# A tibble: 48 × 5
    area  peri  shape  perm  blah
   <int> <dbl>  <dbl> <dbl> <dbl>
 1  4990 2792. 0.0903   6.3  451.
 2  7002 3893. 0.149    6.3 1041.
 3  7558 3931. 0.183    6.3 1385.
 4  7352 3869. 0.117    6.3  861.
 5  7943 3949. 0.122   17.1  972.
 6  7979 4010. 0.167   17.1 1333.
 7  9333 4346. 0.190   17.1 1770.
 8  8209 4345. 0.164   17.1 1347.
 9  8393 3682. 0.204  119   1709.
10  6425 3099. 0.162  119   1043.
# ℹ 38 more rows

Read items from the board

some_model <- db %>%
  pin_read("idk-model")

broom::tidy(some_model)
# A tibble: 3 × 5
  term        estimate std.error statistic  p.value
  <chr>          <dbl>     <dbl>     <dbl>    <dbl>
1 (Intercept) 2475.      247.        10.0  5.11e-13
2 perm          -2.60      0.246    -10.6  8.31e-14
3 blah           0.843     0.139      6.07 2.45e- 7

Extensions

On pin reading/writing

  • Out of the box, the {pins} package supports the reading and writing of files in csv, json, rds, parquet, arrow, and qs (single threaded).

  • If the file type is not specified, the default is rds.

  • Extensions can be created on top of the {pins} infrastructure to further customise supported file types and the behaviour of their reading/writing.

The {pinsqs} package

  • The {pinsqs} package (authored by myself!) provides the utilities to read and write pins in the qs format with support for usage of multiple threads.

  • The {qs} package uses qsave() and qread(), so the {pinsqs} equivalents are

    • pin_qread() instead of pin_read()
    • pin_qsave() instead of pin_write()
  • By default, the number of threads used is half of the available threads on your device.

Example - new_rock

Write the new_rock data set to the pin board as a qs file using multiple threads:

library(pinsqs)
db %>%
  pin_qsave(
    new_rock,
    name = "new-rock-qs", # What we use to refer to it
    title = "A new data set based off of `rock` (qs)",
    description = "Take the rock dataset and create a new variable called `blah` by multiplying `area` with `shape` (qs)"
  )

Inspect the pin board again

db %>%
  pin_search()
# A tibble: 3 × 6
  name        type  title               created             file_size meta      
  <chr>       <chr> <chr>               <dttm>              <fs::byt> <list>    
1 idk-model   rds   Just some model, i… 2025-12-07 00:15:18     3.29K <pins_met>
2 new-rock    rds   A new data set bas… 2025-12-07 00:15:14     1.38K <pins_met>
3 new-rock-qs file  A new data set bas… 2025-12-07 00:15:30     1.35K <pins_met>
db %>%
  pin_meta("new-rock-qs")
List of 13
 $ file       : chr "new-rock-qs.qs"
 $ file_size  : 'fs_bytes' int 1.35K
 $ pin_hash   : chr "a49b5e2f5a499806"
 $ type       : chr "file"
 $ title      : chr "A new data set based off of `rock` (qs)"
 $ description: chr "Take the rock dataset and create a new variable called `blah` by multiplying `area` with `shape` (qs)"
 $ tags       : NULL
 $ urls       : NULL
 $ created    : POSIXct[1:1], format: "2025-12-07 00:15:30"
 $ api_version: int 1
 $ user       : list()
 $ name       : chr "new-rock-qs"
 $ local      :List of 3
  ..$ dir    : 'fs_path' chr "C:/Users/Adam/My Drive/GitHub/deep-dives/data/new-rock-qs/20251207T051530Z-a49b5"
  ..$ url    : NULL
  ..$ version: chr "20251207T051530Z-a49b5"

Read the qs file from the board

new_rock_qs <- db %>%
  pin_qread("new-rock-qs")

new_rock_qs
# A tibble: 48 × 5
    area  peri  shape  perm  blah
   <int> <dbl>  <dbl> <dbl> <dbl>
 1  4990 2792. 0.0903   6.3  451.
 2  7002 3893. 0.149    6.3 1041.
 3  7558 3931. 0.183    6.3 1385.
 4  7352 3869. 0.117    6.3  861.
 5  7943 3949. 0.122   17.1  972.
 6  7979 4010. 0.167   17.1 1333.
 7  9333 4346. 0.190   17.1 1770.
 8  8209 4345. 0.164   17.1 1347.
 9  8393 3682. 0.204  119   1709.
10  6425 3099. 0.162  119   1043.
# ℹ 38 more rows

Summary

  1. Use .Rproj files when possible.

  2. Use here::here() to specify paths relative to the .Rproj.

  3. Use {pins} to store data objects.