# install.packages("pak")
pak::pak(c("here", "pins"))
pak::pak("adamoshen/pinsqs")December 4, 2025
I use {pak} to install my packages because it typically has better success at installing packages on the first try.
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!
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:
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
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
{here} packageThe {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().
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:
[1] "C:/Users/ShenA/Documents/farm-animals/data/chickens.csv"
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:
[1] "C:/Users/ShenA/Documents/farm-animals/data/chickens.csv"
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:
[1] "C:/Users/ShenA/Documents/farm-animals/data"
[2] "C:/Users/ShenA/Documents/farm-animals/chickens.csv"
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!
[1] "C:/Users/ShenA/Documents/farm-animals/this/doesnt/exist.html"
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:
We can then read in chickens.csv as usual, where here::here(...) takes the place of the file path we would usually specify:
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
{pins} packageThe {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).
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.
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).
Note
This needs to be declared at the beginning of all your scripts!
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.
Create 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
Write it to the board:
Create a linear model:
Write it to the board:
# 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>
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"
# 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
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.
{pinsqs} packageThe {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.
new_rockWrite the new_rock data set to the pin board as a qs file using multiple threads:
# 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>
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"
# 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
Use .Rproj files when possible.
Use here::here() to specify paths relative to the .Rproj.
Use {pins} to store data objects.