<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>MLOps and Admin on Posit Open Source</title>
    <link>https://opensource.posit.co/topics/mlops-and-admin/</link>
    <description>Recent content in MLOps and Admin on Posit Open Source</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Thu, 15 Oct 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://opensource.posit.co/topics/mlops-and-admin/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>rig 0.11.0 part three: R projects</title>
      <link>https://opensource.posit.co/blog/2026-10-15_rig-0-11-0-part-three/</link>
      <pubDate>Thu, 15 Oct 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-10-15_rig-0-11-0-part-three/</guid>
      <dc:creator>Gábor Csárdi</dc:creator><description><![CDATA[<!--
TODO:
- [x] Add image (1920×1080 PNG or JPG) and image-alt
- [x] Trim topics, software, and languages to only what applies
- [x] Open a PR against main for a Netlify preview
-->
<style>
.prose pre {
  font-size: 0.65em;
}
</style>
<p>We are thrilled to announce rig 0.11.0 (and 0.10.0), with lots of new
functionalities: user mode R installations, R package and project management
and much more. This is the third of three blog posts about the new rig
release. This post is about managing R projects.</p>
<p>The <a href="https://opensource.posit.co/blog/2026-10-09_rig-0-11-0-part-one/">first post</a> covered user mode R installations, the <a href="https://opensource.posit.co/blog/2026-10-12_rig-0-11-0-part-two/">second</a> focused on R
packages.</p>
<p><a href="https://rig.r-lib.org/news.html" target="_blank" rel="noopener">Full changelog on the rig web site</a>.</p>
<h2 id="introduction">Introduction
</h2>
<p>The new <code>rig proj</code> command lets you manage R projects and virtual
environments. An R project lets you share and run R code in a standardized
way. rig&rsquo;s R projects use an <code>rproj.toml</code> manifest, which is still
<a href="https://github.com/tidyverse/tidyups/pull/34" target="_blank" rel="noopener">under development</a>.</p>
<div class="callout callout-note" role="note" aria-label="Note">
<div class="callout-header">
<span class="callout-title">Note</span>
</div>
<div class="callout-body">
<p><code>rig proj</code> is new and less battle-tested than other parts of rig.
Please report any issues you find on the <a href="https://github.com/r-lib/rig/issues" target="_blank" rel="noopener">rig GitHub repository</a>.</p>
</div>
</div>
<p>rig&rsquo;s R project implementation largely works the same way as other project
management tools for Python, Rust, and other languages.</p>
<h2 id="the-project-lifecycle">The project lifecycle
</h2>
<p>A project lifecycle typically involves</p>
<ol>
<li><code>rig proj init</code> (or <code>rig proj import</code>) to create a new project.</li>
<li><code>rig proj add</code> (and <code>rig proj remove</code>) to manage project dependencies.</li>
<li><code>rig proj lock</code> to resolve how the project is installed over a range
of R versions in a range of platforms.</li>
<li><code>rig proj sync</code> to install a project according to the lock file.</li>
<li><code>rig run</code> to execute R code within the context of the project.</li>
</ol>
<h2 id="creating-projects">Creating projects
</h2>
<p>There are three ways to create an R project. One is to create a brand new
project with <code>rig proj init</code>. This creates a minimal <code>rproj.toml</code> manifest
file that you can either edit manually or via <code>rig proj add</code>:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig proj init
</span></span><span class="line"><span class="cl">✓ Created rproj.toml
</span></span><span class="line"><span class="cl">✓ Created .Renviron
</span></span><span class="line"><span class="cl">✓ Created .gitignore
</span></span><span class="line"><span class="cl">✓ Created .rvenvlib/rvenv
</span></span><span class="line"><span class="cl">ℹ Project set up for R 4.6.1. Next: add dependencies to rproj.toml, then
</span></span><span class="line"><span class="cl">  run `rig proj lock` and `rig proj sync`.</span></span></code></pre></div></div>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ cat rproj.toml
</span></span><span class="line"><span class="cl">[project]
</span></span><span class="line"><span class="cl">name = &#34;x&#34;
</span></span><span class="line"><span class="cl">version = &#34;0.1.0&#34;
</span></span><span class="line"><span class="cl">type = &#34;project&#34;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">[dependencies]
</span></span><span class="line"><span class="cl">R = &#34;&gt;= 4.6&#34;</span></span></code></pre></div></div>
<p>Another way is to convert an existing project to an <code>rproj.toml</code> manifest.
Start with an existing R project with a <code>DESCRIPTION</code>
file and run <code>rig proj import</code> to create an <code>rproj.toml</code> manifest from it.
Or start with an renv lock file and call <code>rig proj renv import</code>.
<code>rig proj import</code> keeps all fields of <code>DESCRIPTION</code>, including various
types of dependencies. For R packages you can use <code>rproj.toml</code> as the
source of truth for package metadata and dependencies and (re)generate
<code>DESCRIPTION</code> from it as needed, e.g. for CRAN submission.</p>
<p>A third way to create a project is to write an <code>rproj.toml</code> manifest manually.</p>
<h3 id="project-types">Project types
</h3>
<p>There are two types of R manifests: <em>projects</em> and <em>packages</em>. In general
we use <em>project</em> as a generic name for both types and consider a <em>package</em> as
a specialized project.</p>
<p>For a package, create the <code>R/</code>, <code>src/</code>, etc. directories as usual. See below
for a possible way to use the devtools based development tools for a rig
project.</p>
<p>For projects rig does not have a suggested way for organizing R and other
code, documentation and tests. If in doubt, follow the conventions of
R packages. Use <code>[[bin]]</code> to specify executable scripts within the project,
relative to the project root. You&rsquo;ll be able to run these scripts with
<code>rig run</code>.</p>
<h2 id="locking-a-project">Locking a project
</h2>
<p><code>rig proj lock</code> reads <code>rproj.toml</code> and works out which exact packages
should be downloaded and installed to satisfy <em>all</em> requirements from
<code>rproj.toml</code>. All requirements always include the optional and soft
dependencies. The result is a lock file called <code>rproj.lock</code>. This is also a
TOML file, typically much bigger than the original <code>rproj.toml</code> project
manifest.</p>
<h3 id="universal-lock-files">Universal lock files
</h3>
<p>The lock can and usually does include packages for multiple platforms.
The exact set of platforms and R versions to include in the lock file can
be specified in <code>rproj.toml</code> or at the command line. By default rig includes
the current R release and five platforms: the platform of the current
machine, macOS arm64, Windows x86_64, generic glibc Linux and <code>source</code>
(four platforms if the current platform coincides with one of the others).</p>
<p>A multi-platform lock file means that it is much easier to plan the
cross-platform deployment of a project: a team can include all relevant
platforms and R versions in the lock file and commit a lock file to version
control.</p>
<p>rig also records system requirements in the lock file, for platforms that
need them.</p>
<h3 id="stable-urls">Stable URLs
</h3>
<p>Universal lock files only really work well if they contain stable URLs for
binary packages for all platforms and R versions. This is why it is
important to use P3M (or another Posit Package Manager instance) with
extended metadata about all older package versions and all their
corresponding binary builds. With P3M rig is typically able to generate
lock files with binary packages for the last 3-4 R versions on all common
platforms.</p>
<h3 id="sticky-lock-files">Sticky lock files
</h3>
<p>Adding new dependencies to a project runs <code>rig proj lock</code>, but it keeps the
package versions already in <code>rproj.lock</code>, unless they conflict with the new
requirements. This is important to be able to maintain a stable set of
package versions while still adding new dependencies. To update an already
existing dependency to a newer version, you need to run
<code>rig proj lock --upgrade-package</code>. Or run <code>rig proj lock --upgrade</code> to
upgrade all packages.</p>
<h3 id="preferring-binary-packages">Preferring binary packages
</h3>
<p>By default rig does not choose an older binary package over a newer source
package. You can change that with the <code>prefer-binary</code> setting in <code>rproj.toml</code>.
E.g. <code>prefer-binary = true</code> means that rig will choose an older binary
package over a newer source package, if the older package also satisfies
the dependency requirements.</p>
<h3 id="excluding-newer-package-versions">Excluding newer package versions
</h3>
<p>You can prevent rig from automatically upgrading to newer package versions
by using the <code>exclude-newer</code> setting in <code>rproj.toml</code>. For example,
<code>exclude-newer = &quot;2026-09-30&quot;</code> will exclude any package versions that were
published after the specified date. This works for P3M&rsquo;s CRAN and
Bioconductor repositories, but not for generic CRAN-like repositories
which typically do not have release dates.</p>
<h3 id="repositories">Repositories
</h3>
<p>Projects ignore the global repository settings configured via <code>rig repos</code>.
By default projects use P3M&rsquo;s CRAN and Bioconductor repositories.
This can be configured with <code>[[repository]]</code> entries in <code>rproj.toml</code>.
You can turn off P3M&rsquo;s CRAN and Bioconductor repositories and you can
also add your own custom repositories.</p>
<p>rig&rsquo;s solver and universal lock files work best with P3M&rsquo;s CRAN and
Bioconductor metadata, so we strongly recommend using them whenever
possible.</p>
<h2 id="syncing-a-project">Syncing a project
</h2>
<p><code>rig proj sync</code> installs the packages listed in <code>rproj.lock</code> into the
project library in <code>.rvenv/lib</code>, inside the project&rsquo;s virtual environment.
It locks first if the project manifest has changed since creating the
lock file.</p>
<p>If the current project is a package (<code>type = &quot;package&quot;</code>), then
<code>rig proj sync</code> will also install the package itself into the project
library.</p>
<p><code>rig proj sync</code> also installs the R version requested in the lock file,
if it is not already installed. This makes it very easy to deploy a
project in an empty container or virtual machine: only rig and the lock
file are needed and rig installs everything required for the project: R,
system requirements and R packages.</p>
<p><code>rig proj sync</code> installs the hard requirements and the <code>dev</code> dependency
group by default. For a production deployment you can use the <code>--no-dev</code>
option to skip installing development dependencies. Use <code>--group</code> and
<code>--extra</code> to install optional dependency groups and extra dependencies.</p>
<h2 id="fast-and-lean-libraries">Fast and lean libraries
</h2>
<p>rig clones the package files when installing packages into the project
library, whenever possible. This means that installation is very fast and
space-efficient. Having the same package version installed for ten projects
on the same disk only takes up the space of a single copy.</p>
<h2 id="the-virtual-environment">The virtual environment
</h2>
<p>A rig project has two kinds of files. <code>rig proj init</code> creates the ones that
belong in version control: <code>rproj.toml</code>, <code>.Renviron</code>, <code>.rvenvlib/rvenv</code> and
<code>.gitignore</code>. Commit these, together with <code>rproj.lock</code>, and a fresh clone of
the project works after a <code>rig proj sync</code>, or right away with <code>rig run</code>.</p>
<p><code>rig proj sync</code> creates the rest in the <code>.rvenv</code> directory: the project
library in <code>.rvenv/lib</code>, <code>R</code> and <code>Rscript</code> wrapper scripts in <code>.rvenv/bin</code>,
and some configuration files. <code>.rvenv</code> is specific to the machine, it is in
<code>.gitignore</code>, and you can delete it any time, <code>rig proj sync</code> creates it
again.</p>
<p>The <code>.Renviron</code> file and the small <code>rvenv</code> package in <code>.rvenvlib</code> make sure
that R uses the project library whenever it starts in the project directory:
from a terminal, or from RStudio, Positron or VS Code. You do not need to
activate the project explicitly, although <code>.rvenv/bin</code> has activation
scripts for the common shells if you prefer that.</p>
<p>Inside the project R only sees the project library, the <code>tools</code> library
(see below) and the packages that come with R. Packages from your user or
site library do not leak into the project.</p>
<p>If the project library is out of date compared to <code>rproj.lock</code>, e.g. after
a <code>git pull</code>, R prints a warning at startup, and <code>rig proj status</code> tells
you what changed. If you start R with <code>rig run</code> in the project directory,
then it automatically runs <code>rig proj sync</code> as needed. If you use Positron,
VS Code, RStudio or another IDE, then R will warn at startup if the virtual
environment is out of date and suggest that you run <code>rig proj sync</code> and
restart R.</p>
<h2 id="managing-dependencies">Managing dependencies
</h2>
<p>R package dependencies are specified in the <code>rproj.toml</code> manifest under three
types of tables. The <code>[dependencies]</code> table includes packages that are
always installed. These are considered as R packages required for
the project. Packages in <code>[dependency-groups.&lt;name&gt;]</code> are groups of
development dependencies and they are only installed if requested, except
for the <code>dev</code> group which is installed by default. For example, the packages
that are needed for building the pkgdown website can go into a
<code>[dependency-groups.website]</code> table. <code>dependency-groups</code> is similar to
<code>Config/Needs/&lt;name&gt;</code> in <code>DESCRIPTION</code> files of R packages.
Dependencies of optional features go into <code>[optional-dependencies.&lt;name&gt;]</code>
tables and are only installed if requested. For example, if your package
has functions for visualization, but these are not required for the core
functionality, you may put them in an <code>[optional-dependencies.viz]</code> table
and other packages using yours may opt in to the <code>viz</code> feature.
(The infrastructure for depending on optional features in packages is not
built yet, and package repositories need to support it as well.)</p>
<p>You can edit the <code>rproj.toml</code> manifest manually to add dependencies or
use <code>rig proj</code> commands to do it. <code>rig proj add</code> adds packages to
<code>rproj.toml</code>, then updates <code>rproj.lock</code> and installs the new packages into
the project library:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig proj add dplyr cli</span></span></code></pre></div></div>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ cat rproj.toml
</span></span><span class="line"><span class="cl">[project]
</span></span><span class="line"><span class="cl">name = &#34;x&#34;
</span></span><span class="line"><span class="cl">version = &#34;0.1.0&#34;
</span></span><span class="line"><span class="cl">type = &#34;project&#34;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">[dependencies]
</span></span><span class="line"><span class="cl">R = &#34;&gt;= 4.6&#34;
</span></span><span class="line"><span class="cl">cli = &#34;*&#34;
</span></span><span class="line"><span class="cl">dplyr = &#34;*&#34;</span></span></code></pre></div></div>
<p>Without a version requirement rig adds a package as <code>&quot;*&quot;</code>, any version. The
exact version is recorded in <code>rproj.lock</code>, so you only need a version
requirement if the project really needs a certain version. Version
requirements use the same syntax as for <code>rig pkg install</code>, e.g.
<code>'cli@&gt;= 3.6'</code> or <code>'dplyr@^1.1.0'</code>. <code>rig proj remove</code> removes packages.</p>
<p>Use <code>--dev</code> to add a development dependency, e.g. testthat. These go into
the <code>[dependency-groups.dev]</code> table.</p>
<p>Packages from GitHub, GitLab, git repositories, URLs and local paths use the
same syntax as <code>rig pkg install</code>, see the <a href="https://opensource.posit.co/blog/2026-10-12_rig-0-11-0-part-two/">second post</a>. Bioconductor or
other packages coming from the configured repositories work the same way as
CRAN packages. To pin a package to a certain repository, e.g. an R universe,
use <code>--with-repos</code>:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig proj add --with-repos rlib=https://r-lib.r-universe.dev cli</span></span></code></pre></div></div>
<p>This adds the repository to <code>rproj.toml</code> and pins <code>cli</code> to it:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">dependencies</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">cli</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">version</span> <span class="p">=</span> <span class="s2">&#34;*&#34;</span><span class="p">,</span> <span class="nx">repository</span> <span class="p">=</span> <span class="s2">&#34;rlib&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[[</span><span class="nx">repository</span><span class="p">]]</span>
</span></span><span class="line"><span class="cl"><span class="nx">name</span> <span class="p">=</span> <span class="s2">&#34;rlib&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">url</span> <span class="p">=</span> <span class="s2">&#34;https://r-lib.r-universe.dev&#34;</span></span></span></code></pre></div></div>
<p>If rig cannot resolve the new dependencies, e.g. because of a typo in a
package name, it restores <code>rproj.toml</code>.</p>
<h2 id="running-a-project">Running a project
</h2>
<p><code>rig run</code> starts R in the project&rsquo;s environment, from anywhere inside the
project. It locks and syncs the project first, if needed, so day to day you
mostly need <code>rig proj add</code>, <code>rig proj remove</code> and <code>rig run</code>.</p>
<p><code>rig run script.R</code> runs a script in the project&rsquo;s environment. Arguments
after the script name go to the script, and <code>commandArgs(TRUE)</code> picks them
up. Use <code>--no-project</code> to run the script without the project.</p>
<p>A project can give names to its scripts, in <code>[[bin]]</code> tables of
<code>rproj.toml</code>:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[[</span><span class="nx">bin</span><span class="p">]]</span>
</span></span><span class="line"><span class="cl"><span class="nx">name</span> <span class="p">=</span> <span class="s2">&#34;report&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">path</span> <span class="p">=</span> <span class="s2">&#34;scripts/report.R&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Build the report&#34;</span></span></span></code></pre></div></div>
<p>Then <code>rig run report --format pdf</code> runs <code>scripts/report.R</code> with the
<code>--format pdf</code> arguments, and <code>rig run --list</code> lists all named scripts of
the project.</p>
<p>You can also run the <code>.rvenv/bin/R</code> and <code>.rvenv/bin/Rscript</code> wrappers
directly, or start R from your IDE in the project directory, see the
virtual environment above.</p>
<h2 id="package-development-with-devtools">Package development with devtools
</h2>
<p>To develop an R package as a rig project, use <code>type = &quot;package&quot;</code> in
<code>rproj.toml</code>, or run <code>rig proj import</code> on an existing package.
<code>rig proj sync</code> then installs the package itself into the project library,
and creates <code>DESCRIPTION</code> from <code>rproj.toml</code> (if missing), so <code>R CMD build</code>,
<code>R CMD check</code> and other tools keep working.</p>
<p>Development tools like devtools, usethis and roxygen2 are not dependencies
of the package, so they should not be in <code>rproj.toml</code>. rig has a special
global package library for them, called <code>tools</code>, and adds it to the library
path of every project. Install your tools there once:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig pkg install -l tools devtools usethis roxygen2</span></span></code></pre></div></div>
<p>and then they are available in all projects:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig run
</span></span><span class="line"><span class="cl">&gt; devtools::load_all()
</span></span><span class="line"><span class="cl">&gt; devtools::document()
</span></span><span class="line"><span class="cl">&gt; devtools::test()</span></span></code></pre></div></div>
<p>Packages that the package&rsquo;s tests and documentation need, e.g. testthat or
knitr, are different. Add these with <code>rig proj add --dev</code>, so they are
locked together with the other dependencies.</p>
<h2 id="inline-scripts">Inline scripts
</h2>
<p>Sometimes a project is just a single R script. For these rig supports inline
dependencies: the script declares the R version and the packages it needs in
a special comment block with a <code># /// script</code> header and a <code># ///</code> footer:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># /// script</span>
</span></span><span class="line"><span class="cl"><span class="c1"># [dependencies]</span>
</span></span><span class="line"><span class="cl"><span class="c1"># R = &#34;&gt;= 4.4&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># cli = &#34;*&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># dplyr = &#34;&gt;= 1.1&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ///</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">dplyr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">cli</span><span class="o">::</span><span class="nf">cli_text</span><span class="p">(</span><span class="s">&#34;Hello from {.pkg cli}!&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>Without the leading <code>#</code> characters, the block is the same TOML as in
<code>rproj.toml</code>.</p>
<p><code>rig run script.R</code> installs the right R version if needed, installs the
packages into a separate environment in rig&rsquo;s cache, and then runs the
script. Your own package libraries do not change. The next run reuses the
environment, so it starts right away. You can share a single <code>.R</code> file, and
it runs the same way on every machine that has rig.</p>
<p>You can write the block by hand, or let rig manage it:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig proj init --script script.R
</span></span><span class="line"><span class="cl">❯ rig proj add --script script.R cli dplyr
</span></span><span class="line"><span class="cl">❯ rig proj remove --script script.R dplyr</span></span></code></pre></div></div>
<p>To pin the exact R and package versions, lock the script:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig proj lock --script script.R</span></span></code></pre></div></div>
<p>This writes <code>script.R.lock</code> next to the script. Keep the two files together,
and <code>rig run</code> will use the versions from the lock file.</p>
<p>On macOS and Linux, start the script with <code>#!/usr/bin/env -S rig run</code> and
make it executable, to run it like any other command. On Windows,
<code>rig system script-assoc</code> lets you run <code>.R</code> files from <code>cmd</code> and PowerShell.
See the <a href="https://rig.r-lib.org/scripts.html" target="_blank" rel="noopener">scripts guide</a> for more.</p>
<h2 id="caches">Caches
</h2>
<p>rig caches the files it downloads: repository metadata, package files,
packages it built from source, git repositories of git and GitHub
dependencies, and the environments of inline scripts. <code>rig pkg</code>,
<code>rig proj</code> and <code>rig run</code> share the same cache, and so do all projects, so
rig downloads or builds a package only once. rig refreshes the repository
metadata once a day by default.</p>
<p><code>rig cache info</code> shows how much space the cache takes, by category, and
<code>rig cache clean</code> deletes it, or only one category of it, with
<code>--category</code>:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig cache info
</span></span><span class="line"><span class="cl">❯ rig cache clean --category scripts</span></span></code></pre></div></div>
<p>Use <code>--no-cache</code> to skip the cache for a single command, and
<code>rig system dirs --cache</code> to see where the cache is.</p>
<h2 id="your-feedback-is-welcome">Your feedback is welcome!
</h2>
<p>We would love to hear your thoughts, suggestions, and any issues you
encounter while using rig. Please use the <a href="https://github.com/r-lib/rig/issues" target="_blank" rel="noopener">issue tracker</a> for bug reports and feature
requests, and the <a href="https://github.com/r-lib/rig/discussions" target="_blank" rel="noopener">discussion forum</a> or <a href="https://forum.posit.co/" target="_blank" rel="noopener">Posit community</a> for general questions and discussions.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-10-15_rig-0-11-0-part-three/featured.jpg" length="424877" type="image/jpeg" />
    </item>
    <item>
      <title>rig 0.11.0 part two: R packages</title>
      <link>https://opensource.posit.co/blog/2026-10-12_rig-0-11-0-part-two/</link>
      <pubDate>Mon, 12 Oct 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-10-12_rig-0-11-0-part-two/</guid>
      <dc:creator>Gábor Csárdi</dc:creator><description><![CDATA[<!--
TODO:
- [x] Add image (1920×1080 PNG or JPG) and image-alt
- [x] Trim topics, software, and languages to only what applies
- [x] Open a PR against main for a Netlify preview
-->
<style>
.prose pre {
  font-size: 0.65em;
}
</style>
<p>We are thrilled to announce rig 0.11.0 (and 0.10.0), with lots of new
functionalities: user mode R installations, R package and project management
and much more. This is the second of three blog posts about the new rig
release. This post is about managing R packages.</p>
<p>The <a href="https://opensource.posit.co/blog/2026-10-09_rig-0-11-0-part-one/">first post</a> covered user mode R installations. The <a href="https://opensource.posit.co/blog/2026-10-15_rig-0-11-0-part-three/">third post</a> focuses on
<a href="https://rig.r-lib.org/proj-guide.html" target="_blank" rel="noopener">project management</a>.</p>
<p><a href="https://rig.r-lib.org/news.html" target="_blank" rel="noopener">Full changelog on the rig web site</a>.</p>
<h2 id="introduction">Introduction
</h2>
<p>The new <code>rig pkg</code> command lets you manage R packages: installing,
removing, and querying package dependencies. It supports CRAN-like
repositories, packages in git repositories (including on GitHub), URLs
pointing to package files and packages in local files and directories.</p>
<p><code>rig pkg</code> is aimed at R users and developers who use a single (or a few)
package libraries. If you prefer to use project-specific libraries, you
probably want to use <code>rig proj</code> instead.</p>
<div class="callout callout-note" role="note" aria-label="Note">
<div class="callout-header">
<span class="callout-title">Note</span>
</div>
<div class="callout-body">
<p><code>rig pkg</code> is new and less battle-tested than other parts of rig.
Please report any issues you find on the <a href="https://github.com/r-lib/rig/issues" target="_blank" rel="noopener">rig GitHub repository</a>.</p>
</div>
</div>
<h2 id="managing-installed-packages">Managing installed packages
</h2>
<p><code>rig pkg list</code> lists installed packages in a library. <code>rig pkg remove</code>
removes a package from a library.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig pkg list
</span></span><span class="line"><span class="cl">559 packages (R 4.6.1, main: /Users/gaborcsardi/Library/R/arm64/4.6/library)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Package              Version      Built   Platform                   Source
</span></span><span class="line"><span class="cl">----------------------------------------------------------------------------------------------------------------
</span></span><span class="line"><span class="cl">abind                1.4-8        4.6.0   -                          RSPM
</span></span><span class="line"><span class="cl">ade4                 1.7-24       4.6.0   aarch64-apple-darwin23     CRAN
</span></span><span class="line"><span class="cl">adegenet             2.1.11       4.6.0   aarch64-apple-darwin23     CRAN
</span></span><span class="line"><span class="cl">AdmixPoly            1.0.1        4.6.0   aarch64-apple-darwin20     RSPM
</span></span><span class="line"><span class="cl">affy                 1.90.0       4.6.1   aarch64-apple-darwin23     RSPM
</span></span><span class="line"><span class="cl">affyio               1.82.0       4.6.0   aarch64-apple-darwin20     RSPM
</span></span><span class="line"><span class="cl">[...]</span></span></code></pre></div></div>
<p><code>rig pkg</code> commands use the current default R version (as in <code>rig default</code>),
unless the <code>-r</code> or <code>--r-version</code> option specifies otherwise. They use the
default library (as in <code>rig library default</code>), unless the <code>-l</code> or
<code>--library</code> option specifies otherwise.</p>
<p><code>rig pkg doctor</code> checks the health of your package library, identifying any
issues with installed packages or their dependencies: missing dependencies,
dependency conflicts, ABI conflicts, etc. It also tells you how to fix these
issues.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig pkg doctor
</span></span><span class="line"><span class="cl">3 problems (1 error, 2 warnings) in 559 packages (R 4.6.1, main: /Users/gaborcsardi/Library/R/arm64/4.6/library)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Package              Version   Severity   Problem   Details
</span></span><span class="line"><span class="cl">-------------------------------------------------------------------------------------------------------------
</span></span><span class="line"><span class="cl">00LOCK-dress.graph   -         warning    lock      lock directory left behind by an interrupted installation
</span></span><span class="line"><span class="cl">dress.graph          -         warning    broken    no DESCRIPTION file
</span></span><span class="line"><span class="cl">missoNet             1.5.1     error      missing   Imports ComplexHeatmap, not installed
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">To fix the errors, run:
</span></span><span class="line"><span class="cl">  rig pkg install ComplexHeatmap
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">To remove the leftover lock directories, if no installation is running, run:
</span></span><span class="line"><span class="cl">  rm -rf /Users/gaborcsardi/Library/R/arm64/4.6/library/00LOCK-dress.graph
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">To remove the broken packages, run:
</span></span><span class="line"><span class="cl">  rm -rf /Users/gaborcsardi/Library/R/arm64/4.6/library/dress.graph
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">✗ Found 1 error in /Users/gaborcsardi/Library/R/arm64/4.6/library</span></span></code></pre></div></div>
<h2 id="installing-packages">Installing packages
</h2>
<p><code>rig pkg install</code> can install R packages from CRAN-like repositories,
git repositories, GitHub, URLs, and local paths. It uses the default R
version and the default package library (see below about libraries),
unless otherwise specified. It caches metadata and package files heavily,
avoids redundant downloads and installations, and performs downloads and
package builds in parallel. It uses the host OS&rsquo;s file cloning capabilities
to efficiently copy files when installing packages.</p>
<p>If you used the pak R package to install R packages previously, <code>rig pkg</code>
will probably feel familiar, except that you can run it from the command
line instead of from within R.</p>
<p>This is what installing a package into an empty library looks like, if the
package files are already cached:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig pkg install dplyr
</span></span><span class="line"><span class="cl">▶ Downloading binary package metadata
</span></span><span class="line"><span class="cl">▶ Solving dependencies
</span></span><span class="line"><span class="cl">✓ Solved dependencies
</span></span><span class="line"><span class="cl">15 of 15 packages to install (R 4.6.1, test: /Users/gaborcsardi/Library/R/arm64/4.6/library/__test)
</span></span><span class="line"><span class="cl">Package     Version  Type    Action   Reason
</span></span><span class="line"><span class="cl">cli         3.6.6    binary  install  not installed
</span></span><span class="line"><span class="cl">dplyr       1.2.1    binary  install  not installed
</span></span><span class="line"><span class="cl">generics    0.1.4    binary  install  not installed
</span></span><span class="line"><span class="cl">glue        1.8.1    binary  install  not installed
</span></span><span class="line"><span class="cl">lifecycle   1.0.5    binary  install  not installed
</span></span><span class="line"><span class="cl">magrittr    2.0.5    binary  install  not installed
</span></span><span class="line"><span class="cl">pillar      1.11.1   binary  install  not installed
</span></span><span class="line"><span class="cl">pkgconfig   2.0.3    binary  install  not installed
</span></span><span class="line"><span class="cl">R6          2.6.1    binary  install  not installed
</span></span><span class="line"><span class="cl">rlang       1.3.0    binary  install  not installed
</span></span><span class="line"><span class="cl">tibble      3.3.1    binary  install  not installed
</span></span><span class="line"><span class="cl">tidyselect  1.2.1    binary  install  not installed
</span></span><span class="line"><span class="cl">utf8        1.2.6    binary  install  not installed
</span></span><span class="line"><span class="cl">vctrs       0.7.3    binary  install  not installed
</span></span><span class="line"><span class="cl">withr       3.0.3    binary  install  not installed
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">▶ Downloading 15 packages
</span></span><span class="line"><span class="cl">✓ Cached: cli
</span></span><span class="line"><span class="cl">✓ Cached: dplyr
</span></span><span class="line"><span class="cl">✓ Cached: generics
</span></span><span class="line"><span class="cl">✓ Cached: glue
</span></span><span class="line"><span class="cl">✓ Cached: lifecycle
</span></span><span class="line"><span class="cl">✓ Cached: magrittr
</span></span><span class="line"><span class="cl">✓ Cached: pillar
</span></span><span class="line"><span class="cl">✓ Cached: pkgconfig
</span></span><span class="line"><span class="cl">✓ Cached: R6
</span></span><span class="line"><span class="cl">✓ Cached: rlang
</span></span><span class="line"><span class="cl">✓ Cached: tibble
</span></span><span class="line"><span class="cl">✓ Cached: tidyselect
</span></span><span class="line"><span class="cl">✓ Cached: utf8
</span></span><span class="line"><span class="cl">✓ Cached: vctrs
</span></span><span class="line"><span class="cl">✓ Cached: withr
</span></span><span class="line"><span class="cl">Complete: 0 downloaded, 15 cached [========================================] 15/15 packages
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">▶ Installing 15 packages.
</span></span><span class="line"><span class="cl">Installed R6 2.6.1
</span></span><span class="line"><span class="cl">Installed cli 3.6.6
</span></span><span class="line"><span class="cl">Installed rlang 1.3.0
</span></span><span class="line"><span class="cl">Installed magrittr 2.0.5
</span></span><span class="line"><span class="cl">Installed pkgconfig 2.0.3
</span></span><span class="line"><span class="cl">Installed glue 1.8.1
</span></span><span class="line"><span class="cl">Installed withr 3.0.3
</span></span><span class="line"><span class="cl">Installed utf8 1.2.6
</span></span><span class="line"><span class="cl">Installed generics 0.1.4
</span></span><span class="line"><span class="cl">Installed lifecycle 1.0.5
</span></span><span class="line"><span class="cl">Installed vctrs 0.7.3
</span></span><span class="line"><span class="cl">Installed tidyselect 1.2.1
</span></span><span class="line"><span class="cl">Installed pillar 1.11.1
</span></span><span class="line"><span class="cl">Installed tibble 3.3.1
</span></span><span class="line"><span class="cl">Installed dplyr 1.2.1
</span></span><span class="line"><span class="cl">✓ Installed all 15 packages successfully
</span></span><span class="line"><span class="cl">✓ Installed 15 packages (R 4.6.1, test: /Users/gaborcsardi/Library/R/arm64/4.6/library/__test)</span></span></code></pre></div></div>
<h3 id="version-constraints">Version constraints
</h3>
<p><code>rig pkg install</code> supports version constraints for packages. Request a
specific version of a package with <code>@==&lt;version&gt;</code>:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig pkg install ggplot2@==4.0.2</span></span></code></pre></div></div>
<p>If you are familiar with <code>~</code> and <code>^</code> version constraints from other languages,
you can use them with <code>rig pkg install</code> as well. E.g. to install the
latest version of the ggplot2 3.x.y series, you would run:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig pkg install ggplot2@^3.0.0</span></span></code></pre></div></div>
<h3 id="dependency-solver">Dependency solver
</h3>
<p>The rig dependency solver makes sure that all package dependencies are
resolved correctly before installation. If the combination of packages
cannot be installed on the specified R version or platform, rig will report
an error and refuse to proceed with the installation. For example:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig pkg install -r 4.0.5-x86_64 &#39;testthat@&gt;=3.3.0&#39;
</span></span><span class="line"><span class="cl">▶ Downloading binary package metadata
</span></span><span class="line"><span class="cl">▶ Solving dependencies
</span></span><span class="line"><span class="cl">✗ Cannot resolve dependencies for R 4.0.5 / x86_64-apple-darwin:
</span></span><span class="line"><span class="cl">  Because testthat &gt;=3.3.0 depends on R &gt;=4.1.0 and you requested
</span></span><span class="line"><span class="cl">  testthat &gt;=3.3.0, the requested packages cannot be installed.</span></span></code></pre></div></div>
<h3 id="packages-from-git-github-urls-paths">Packages from git, GitHub, URLs, paths
</h3>
<p>If you are familiar with pak&rsquo;s syntax for installing packages from
various non-CRAN sources, you can use the same syntax with rig. E.g. a
GitHub package is simply <code>username/repo</code>. You can use the usual syntax
for tags, branches, pull requests:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig pkg install r-lib/cli
</span></span><span class="line"><span class="cl">❯ rig pkg install r-lib/cli@badcafe
</span></span><span class="line"><span class="cl">❯ rig pkg install r-lib/cli@some-branch
</span></span><span class="line"><span class="cl">❯ rig pkg install r-lib/cli#41</span></span></code></pre></div></div>
<p>Prefix packages in git repositories with <code>git::</code>. To install a package
from a URL simply use the full URL. For a local path use a relative path
starting with <code>./</code>, <code>../</code> or <code>~/</code> or an absolute path:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig pkg install .
</span></span><span class="line"><span class="cl">❯ rig pkg install ./mypkg
</span></span><span class="line"><span class="cl">❯ rig pkg install ../mypkg
</span></span><span class="line"><span class="cl">❯ rig pkg install ~/works/mypkg
</span></span><span class="line"><span class="cl">❯ rig pkg install /opt/src/mypkg
</span></span><span class="line"><span class="cl">❯ rig pkg install ./mypkg_1.0.0.tar.gz</span></span></code></pre></div></div>
<h3 id="preferring-binary-packages">Preferring binary packages
</h3>
<p>rig prefers to install the binary package for a certain package version if
it is available, instead of building and potentially compiling it from
source. By default it does not choose an older version over a newer one
just because a binary is available for the older version. The
<code>--prefer-binary</code> option changes this. Use it if you prefer older binary
packages over newer source packages:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig pkg install --prefer-binary tidyverse</span></span></code></pre></div></div>
<h3 id="installing-dependencies">Installing dependencies
</h3>
<p>To install the dependencies of a package, without installing the package
itself, use <code>--no-install-project</code>. This is especially useful to install
all dependencies of a package tree in the current working directory. The
second variant also installs soft (development) dependencies:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig pkg install --no-install-project .
</span></span><span class="line"><span class="cl">❯ rig pkg install --no-install-project --dev .</span></span></code></pre></div></div>
<h2 id="repositories">Repositories
</h2>
<p><code>rig pkg</code> subcommands always use the configured repositories, see the
<code>rig repos</code> command. They also have <code>--with-repos</code> and <code>--without-repos</code>
options to include or exclude specific repositories when performing a single
package operation, without modifying the configured repositories. E.g. to
query the version of a package on CRAN and P3M you can run:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig pkg info otel --without-repos --with-repos=p3m --versions
</span></span><span class="line"><span class="cl">otel — 2 versions (latest 0.2.0)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Version   Published    R          Deps
</span></span><span class="line"><span class="cl">-------------------------------------------------------
</span></span><span class="line"><span class="cl">0.1.0     2025-07-31   &gt;= 3.6.0      0
</span></span><span class="line"><span class="cl">0.2.0     2025-08-29   &gt;= 3.6.0      0   ← latest</span></span></code></pre></div></div>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig pkg info otel --without-repos --with-repos=cran --versions
</span></span><span class="line"><span class="cl">otel — 1 version (latest 0.2.0)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Version   Published   R          Deps
</span></span><span class="line"><span class="cl">-------------------------------------------------------
</span></span><span class="line"><span class="cl">0.2.0     ?           &gt;= 3.6.0      0   ← latest</span></span></code></pre></div></div>
<h2 id="package-queries">Package queries
</h2>
<p><code>rig pkg info</code> prints information about a package, by default about the
latest version:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig pkg info tidymodels
</span></span><span class="line"><span class="cl">tidymodels 1.5.0 (RSPM)
</span></span><span class="line"><span class="cl">Easily Install and Load the &#39;Tidymodels&#39; Packages
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The tidy modeling &#34;verse&#34; is a collection of packages for modeling and
</span></span><span class="line"><span class="cl">statistical analysis that share the underlying design philosophy, grammar, and
</span></span><span class="line"><span class="cl">data structures of the tidyverse.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Maintainer    Max Kuhn &lt;max@posit.co&gt;
</span></span><span class="line"><span class="cl">License       MIT + file LICENSE
</span></span><span class="line"><span class="cl">Published     2026-04-23 06:20:02 UTC
</span></span><span class="line"><span class="cl">URL           https://tidymodels.tidymodels.org,
</span></span><span class="line"><span class="cl">              https://github.com/tidymodels/tidymodels
</span></span><span class="line"><span class="cl">BugReports    https://github.com/tidymodels/tidymodels/issues
</span></span><span class="line"><span class="cl">Compilation   no
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Depends       R (&gt;= 4.1)
</span></span><span class="line"><span class="cl">Imports       broom (&gt;= 1.0.12), cli (&gt;= 3.6.6), conflicted (&gt;= 1.2.0), dials
</span></span><span class="line"><span class="cl">              (&gt;= 1.4.3), dplyr (&gt;= 1.2.1), ggplot2 (&gt;= 4.0.3), hardhat (&gt;=
</span></span><span class="line"><span class="cl">              1.4.3), infer (&gt;= 1.1.0), modeldata (&gt;= 1.5.1), parsnip (&gt;=
</span></span><span class="line"><span class="cl">              1.5.0), purrr (&gt;= 1.2.2), recipes (&gt;= 1.3.2), rlang (&gt;= 1.2.0),
</span></span><span class="line"><span class="cl">              rsample (&gt;= 1.3.2), rstudioapi (&gt;= 0.18.0), tailor (&gt;= 0.1.0),
</span></span><span class="line"><span class="cl">              tidyr (&gt;= 1.3.2), tune (&gt;= 2.1.0), workflows (&gt;= 1.3.0),
</span></span><span class="line"><span class="cl">              workflowsets (&gt;= 1.1.1), yardstick (&gt;= 1.4.0)
</span></span><span class="line"><span class="cl">Suggests      covr, glue, knitr, rmarkdown, testthat (&gt;= 3.0.0), xml2</span></span></code></pre></div></div>
<p><code>rig pkg info --versions</code> lists all versions of a package. This only works
with P3M repositories because it needs extended metadata that plain
CRAN-like repositories typically do not have:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig pkg info tidymodels --versions
</span></span><span class="line"><span class="cl">tidymodels — 17 versions (latest 1.5.0)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Version   Published    R        Deps
</span></span><span class="line"><span class="cl">-------------------------------------------------------
</span></span><span class="line"><span class="cl">0.0.1     2018-07-27   &gt;= 3.1     18
</span></span><span class="line"><span class="cl">0.0.2     2018-11-27   &gt;= 3.1     20
</span></span><span class="line"><span class="cl">0.0.3     2019-10-04   &gt;= 3.1     20
</span></span><span class="line"><span class="cl">0.1.0     2020-02-16   &gt;= 3.1     22
</span></span><span class="line"><span class="cl">0.1.1     2020-07-14   &gt;= 3.1     20
</span></span><span class="line"><span class="cl">0.1.2     2020-11-22   &gt;= 3.1     19
</span></span><span class="line"><span class="cl">0.1.3     2021-04-19   &gt;= 3.1     20
</span></span><span class="line"><span class="cl">0.1.4     2021-10-01   &gt;= 3.1     21
</span></span><span class="line"><span class="cl">0.2.0     2022-03-19   &gt;= 3.4     21
</span></span><span class="line"><span class="cl">1.0.0     2022-07-13   &gt;= 3.4     21
</span></span><span class="line"><span class="cl">1.1.0     2023-05-01   &gt;= 3.4     21
</span></span><span class="line"><span class="cl">1.1.1     2023-08-24   &gt;= 3.4     21
</span></span><span class="line"><span class="cl">1.2.0     2024-03-25   &gt;= 3.6     21
</span></span><span class="line"><span class="cl">1.3.0     2025-02-21   &gt;= 3.6     21
</span></span><span class="line"><span class="cl">1.4.0     2025-09-05   &gt;= 4.1     21
</span></span><span class="line"><span class="cl">1.4.1     2025-09-08   &gt;= 4.1     21
</span></span><span class="line"><span class="cl">1.5.0     2026-04-23   &gt;= 4.1     21   ← latest</span></span></code></pre></div></div>
<p><code>rig pkg deps</code> prints the dependencies of a package. <code>rig pkg tree</code> does
the same, but as a dependency tree. You can use these commands to query
non-CRAN-like packages. E.g. the dependency tree of the package in the
current working directory is</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig pkg tree --dev .
</span></span><span class="line"><span class="cl">otelsdk 0.2.4.9000 (local: /Users/gaborcsardi/works/otelsdk) — 14 direct, 51 total
</span></span><span class="line"><span class="cl">├── otel 0.2.0
</span></span><span class="line"><span class="cl">│   └── R (&gt;= 3.6.0) [D]
</span></span><span class="line"><span class="cl">└── utils
</span></span><span class="line"><span class="cl">[Suggests]
</span></span><span class="line"><span class="cl">├── callr 3.8.0
</span></span><span class="line"><span class="cl">│   ├── R (&gt;= 3.4) [D]
</span></span><span class="line"><span class="cl">│   ├── otel 0.2.0 (&gt;= 0.2.0) (*)
</span></span><span class="line"><span class="cl">│   ├── processx 3.9.0 (&gt;= 3.6.1)
</span></span><span class="line"><span class="cl">│   │   ├── R (&gt;= 3.4.0) [D]
</span></span><span class="line"><span class="cl">│   │   ├── ps 1.9.3 (&gt;= 1.9.3)
</span></span><span class="line"><span class="cl">│   │   │   ├── R (&gt;= 3.4) [D]
</span></span><span class="line"><span class="cl">│   │   │   └── utils
</span></span><span class="line"><span class="cl">│   │   ├── R6 2.6.1
</span></span><span class="line"><span class="cl">│   │   │   └── R (&gt;= 3.6) [D]
</span></span><span class="line"><span class="cl">│   │   └── utils
</span></span><span class="line"><span class="cl">│   ├── R6 2.6.1 (*)
</span></span><span class="line"><span class="cl">│   └── utils
</span></span><span class="line"><span class="cl">├── cli 3.6.6
</span></span><span class="line"><span class="cl">│   ├── R (&gt;= 3.4) [D]
</span></span><span class="line"><span class="cl">│   └── utils
</span></span><span class="line"><span class="cl">├── curl 8.0.0
</span></span><span class="line"><span class="cl">│   └── R (&gt;= 3.0.0) [D]
</span></span><span class="line"><span class="cl">[...]</span></span></code></pre></div></div>
<p>The <code>--dev</code> flag includes development dependencies as well.</p>
<p><code>rig pkg search</code> searches all CRAN packages using R-hub package search
service:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig pkg search palette --short
</span></span><span class="line"><span class="cl">- &#34;palette&#34; ----------------------------------- 177 packages in 0.009 seconds -
</span></span><span class="line"><span class="cl"> #     package      version  by                     @ title
</span></span><span class="line"><span class="cl"> 1 100 RColorBrewer 1.1-3    Erich Neuwirth        5y ColorBrewer Palettes
</span></span><span class="line"><span class="cl"> 2  46 colorspace   2.1-3    Achim Zeileis         3M A Toolbox for Manipula...
</span></span><span class="line"><span class="cl"> 3  23 paletteer    1.7.0    Emil Hvitfeldt        9M Comprehensive Collecti...
</span></span><span class="line"><span class="cl"> 4  16 ggsci        5.2.0    Nan Xiao              2M Scientific Journal and...
</span></span><span class="line"><span class="cl"> 5  13 wesanderson  0.3.7    Karthik Ram           3y A Wes Anderson Palette...
</span></span><span class="line"><span class="cl"> 6   8 rcartocolor  2.1.2    Jakub Nowosad         1y &#39;CARTOColors&#39; Palettes
</span></span><span class="line"><span class="cl"> 7   8 pals         1.10     Kevin Wright          2y Color Palettes, Colorm...
</span></span><span class="line"><span class="cl"> 8   8 scico        1.5.0    Thomas Lin Pedersen   3y Colour Palettes Based...
</span></span><span class="line"><span class="cl"> 9   6 palette      0.0.3    Christopher T. Kenny  1y Color Scheme Helpers
</span></span><span class="line"><span class="cl">10   6 MetBrewer    0.2.0    Blake Robert Mills    5y Color Palettes Inspire...
</span></span><span class="line"><span class="cl">11   6 palr         0.5.0    Michael D. Sumner     2M Colour Palettes for Data
</span></span><span class="line"><span class="cl">12   6 blends       0.1.2    David Hodge           3M Blend Colours and Pale...
</span></span><span class="line"><span class="cl">13   6 biscale      1.1.0    Christopher Prener    1y Tools and Palettes for...
</span></span><span class="line"><span class="cl">14   5 palettes     0.2.2    Michael McCarthy      1y Methods for Colour Vec...
</span></span><span class="line"><span class="cl">15   5 PrettyCols   1.1.0    Nicola Rennie         2y Pretty Colour Palettes
</span></span><span class="line"><span class="cl">16   5 gifski       1.32.0-2 Jeroen Ooms           2y Highest Quality GIF En...
</span></span><span class="line"><span class="cl">17   5 wacolors     0.3.1    Cory McCartan         5y Colorblind-Friendly Pa...
</span></span><span class="line"><span class="cl">18   4 colourpicker 1.3.0    Dean Attali           3y A Colour Picker Tool f...
</span></span><span class="line"><span class="cl">19   4 amerika      0.1.1    Philip Waggoner       1y American Politics-Insp...
</span></span><span class="line"><span class="cl">20   4 makePalette  0.1.2    Jorge L. C. Musaja    3y Make Palette
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Next 20 results: rig pkg search palette --from 21 --short</span></span></code></pre></div></div>
<h2 id="package-libraries">Package libraries
</h2>
<p>rig creates a user-level package library for each R installation by default.
(See the <a href="https://rig.r-lib.org/faq.html" target="_blank" rel="noopener">rig FAQ</a> why.) This is the package
library that <code>rig pkg</code> commands use by default.</p>
<p>The <code>rig library</code> command supports creating other package libraries, still
stored in the user&rsquo;s home directory. You can refer to them by name and make
them the default library for <code>rig pkg</code> commands with <code>rig library default</code>.</p>
<p>For example to create a temporary library and install the Shiny package from
GitHub into it, including development dependencies, you can run:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">rig library add <span class="nb">test</span>
</span></span><span class="line"><span class="cl">rig library default <span class="nb">test</span>
</span></span><span class="line"><span class="cl">rig pkg install --dev rstudio/shiny</span></span></code></pre></div></div>
<p>You can also use the <code>-l</code> or <code>--library</code> option of the <code>rig pkg</code> commands to
change the package library temporarily for one command, without changing the
default.</p>
<p>If you are using the <a href="https://rig.r-lib.org/macos-app.html" target="_blank" rel="noopener">macOS menu bar app</a>, then you can also manage package
libraries through the app&rsquo;s interface.</p>
<h2 id="system-requirements">System requirements
</h2>
<p>On certain Linux systems rig supports automatic system requirements
installation. These are currently:</p>
<ul>
<li>Ubuntu Linux</li>
<li>RedHat Enterprise Linux and clones</li>
<li>openSUSE Linux and SUSE Linux Enterprise</li>
<li>Debian Linux</li>
<li>Fedora Linux</li>
<li>Alpine Linux</li>
</ul>
<p>For the automatic system requirements installation to work, rig must run
as the <code>root</code> user or with password-less <code>sudo</code> privileges to call the
system package manager.</p>
<p>Note that in user mode, rig installs the self-contained manylinux R build,
which does not require any system-level dependencies and also sets up
Posit&rsquo;s manylinux package repository, which has self-contained binaries for
most CRAN and Bioconductor packages. So in user mode you typically do not
need to worry about system-level dependencies.</p>
<h2 id="your-feedback-is-welcome">Your feedback is welcome!
</h2>
<p>We would love to hear your thoughts, suggestions, and any issues you
encounter while using rig. Please use the <a href="https://github.com/r-lib/rig/issues" target="_blank" rel="noopener">issue tracker</a> for bug reports and feature
requests, and the <a href="https://github.com/r-lib/rig/discussions" target="_blank" rel="noopener">discussion forum</a> or <a href="https://forum.posit.co/" target="_blank" rel="noopener">Posit community</a> for general questions and discussions.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-10-12_rig-0-11-0-part-two/featured.jpg" length="427378" type="image/jpeg" />
    </item>
    <item>
      <title>rig 0.11.0 part one: R installations</title>
      <link>https://opensource.posit.co/blog/2026-10-09_rig-0-11-0-part-one/</link>
      <pubDate>Fri, 09 Oct 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-10-09_rig-0-11-0-part-one/</guid>
      <dc:creator>Gábor Csárdi</dc:creator><description><![CDATA[<!--
TODO:
- [x] Add image (1920×1080 PNG or JPG) and image-alt
- [x] Trim topics, software, and languages to only what applies
- [x] Open a PR against main for a Netlify preview
-->
<style>
.prose pre {
  font-size: 0.65em;
}
</style>
<p>We are thrilled to announce rig 0.11.0 (and 0.10.0), with lots of new
functionalities: user mode R installations, R package and project management
and much more. This is the first of three blog posts about the new rig
release. This post is about managing R installations and the new user mode.
The <a href="https://opensource.posit.co/blog/2026-10-12_rig-0-11-0-part-two/">second post</a> shows how rig can help with <a href="https://rig.r-lib.org/pkg-guide.html" target="_blank" rel="noopener">managing R packages</a>, and the <a href="https://opensource.posit.co/blog/2026-10-15_rig-0-11-0-part-three/">third</a> focuses on
<a href="https://rig.r-lib.org/proj-guide.html" target="_blank" rel="noopener">project management</a>.</p>
<p><a href="https://rig.r-lib.org/news.html" target="_blank" rel="noopener">Full changelog on the rig web site</a>.</p>
<h2 id="new-web-site">New Web Site
</h2>
<p>rig has a new web site at <a href="https://rig.r-lib.org" target="_blank" rel="noopener">https://rig.r-lib.org</a>! See this web site for
installation instructions, including the new user mode install; reference
documentation (also in <code>rig help</code>!), guides about larger topics and
tutorials to get started.</p>
<figure>
<a href="https://rig.r-lib.org"><img src="https://opensource.posit.co/blog/2026-10-09_rig-0-11-0-part-one/rig-website.png" /></a>
<figcaption>The home page of the new rig web site, with a sidebar listing tutorials, guides and reference pages, and a quick start section.</figcaption>
</figure>
<h2 id="user-mode-r-installations">User Mode R Installations
</h2>
<p>rig can now install and manage R installations without
requiring administrative privileges. rig calls this <em>user mode</em>. In user
mode, R installations are installed in the user&rsquo;s home directory rather
than system-wide. System-wide installations are still the default currently,
and they are called <em>admin mode</em>.</p>
<p>You may have both user mode and admin mode installations on one computer,
but each rig command runs in either user mode or admin mode. The default is
admin mode. See below for how to change it. Use <code>--admin</code> or <code>--user</code> to
pick the mode for a single command, e.g. <code>rig ls --admin</code> always lists
admin mode installations, and <code>rig ls --user</code> always lists user mode ones.</p>
<p>Advantages of using user mode:</p>
<ul>
<li><strong>No admin</strong>: no need for administrative privileges.</li>
<li><strong>All Linux</strong>: works on all glibc (from glibc 2.34) and musl (from musl
1.2) based Linux systems.</li>
<li><strong>Multiple patch versions</strong>: on macOS you can install multiple patch
versions side by side.</li>
<li><strong>Convenience</strong>: no need to use <code>sudo</code> or an admin account to install and
manage R.</li>
</ul>
<p>User mode is quite new, though. It is much less tested than admin mode,
and you may encounter bugs or unexpected behavior more frequently. On Linux,
the generic distro-agnostic builds may be slower for numerical work than
builds specifically compiled for the distribution.</p>
<p>User mode (admin mode as well, actually) is slightly different on each
operating system. Here is a table about where rig stores its files on each
OS in both modes.</p>
<table>
  <thead>
      <tr>
          <th>Platform</th>
          <th>Mode</th>
          <th>R install root</th>
          <th>Binary directory (<code>R</code>, <code>Rscript</code>, <code>R-*</code>)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>macOS</td>
          <td>admin</td>
          <td><code>/Library/Frameworks/R.framework</code></td>
          <td><code>/usr/local/bin</code></td>
      </tr>
      <tr>
          <td>macOS</td>
          <td>user</td>
          <td><code>~/.local/share/rig/r</code></td>
          <td><code>~/.local/bin</code></td>
      </tr>
      <tr>
          <td>Linux</td>
          <td>admin</td>
          <td><code>/opt/R</code></td>
          <td><code>/usr/local/bin</code></td>
      </tr>
      <tr>
          <td>Linux</td>
          <td>user</td>
          <td><code>~/.local/share/rig/r</code></td>
          <td><code>~/.local/bin</code></td>
      </tr>
      <tr>
          <td>Windows</td>
          <td>admin</td>
          <td><code>C:\Program Files\R</code></td>
          <td><code>C:\Program Files\R\bin</code></td>
      </tr>
      <tr>
          <td>Windows</td>
          <td>user</td>
          <td><code>%APPDATA%\rig\data\r</code></td>
          <td><code>%USERPROFILE%\.local\bin</code></td>
      </tr>
  </tbody>
</table>
<p>You can call the new <code>rig system dirs</code> command to see where rig stores its
various files:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig system dirs
</span></span><span class="line"><span class="cl">Mode                  user
</span></span><span class="line"><span class="cl">Architecture          arm64
</span></span><span class="line"><span class="cl">R root                /Users/gaborcsardi/.local/share/rig/r
</span></span><span class="line"><span class="cl">Binary dir            /Users/gaborcsardi/.local/bin
</span></span><span class="line"><span class="cl">Config file           /Users/gaborcsardi/Library/Application Support/com.gaborcsardi.rig/config.json
</span></span><span class="line"><span class="cl">Data dir              /Users/gaborcsardi/Library/Application Support/com.gaborcsardi.rig
</span></span><span class="line"><span class="cl">Cache dir             /Users/gaborcsardi/Library/Caches/com.gaborcsardi.rig
</span></span><span class="line"><span class="cl">Download dir          /var/folders/h9/6ct_py4s319fxt88dxn506l00000gp/T/rig-502
</span></span><span class="line"><span class="cl">Logs dir              /Users/gaborcsardi/Library/Logs/com.gaborcsardi.rig
</span></span><span class="line"><span class="cl">Project library root  (in-project, .rvenv/lib)</span></span></code></pre></div></div>
<p>The same in admin mode:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig system dirs --admin
</span></span><span class="line"><span class="cl">Mode                  admin
</span></span><span class="line"><span class="cl">Architecture          arm64
</span></span><span class="line"><span class="cl">R root                /Library/Frameworks/R.framework/Versions
</span></span><span class="line"><span class="cl">Binary dir            /usr/local/bin
</span></span><span class="line"><span class="cl">Config file           /Users/gaborcsardi/Library/Application Support/com.gaborcsardi.rig/config.json
</span></span><span class="line"><span class="cl">Data dir              /Users/gaborcsardi/Library/Application Support/com.gaborcsardi.rig
</span></span><span class="line"><span class="cl">Cache dir             /Users/gaborcsardi/Library/Caches/com.gaborcsardi.rig
</span></span><span class="line"><span class="cl">Download dir          /var/folders/h9/6ct_py4s319fxt88dxn506l00000gp/T/rig-502
</span></span><span class="line"><span class="cl">Logs dir              /Users/gaborcsardi/Library/Logs/com.gaborcsardi.rig
</span></span><span class="line"><span class="cl">Project library root  (in-project, .rvenv/lib)</span></span></code></pre></div></div>
<h3 id="macos">macOS
</h3>
<p>On arm64 macOS rig can install both arm64 and x86_64 builds of R. The latter
ones need Rosetta to run. User mode has a different, simpler naming scheme
than admin mode on macOS, matching Linux and Windows. This is an example
list of user mode R installations:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig ls
</span></span><span class="line"><span class="cl">* name          version    aliases
</span></span><span class="line"><span class="cl">------------------------------------------
</span></span><span class="line"><span class="cl">  3.6.3-x86_64  (R 3.6.3)
</span></span><span class="line"><span class="cl">  4.0.5-x86_64  (R 4.0.5)
</span></span><span class="line"><span class="cl">  4.1.3
</span></span><span class="line"><span class="cl">  4.1.3-x86_64  (R 4.1.3)
</span></span><span class="line"><span class="cl">  4.2.3
</span></span><span class="line"><span class="cl">  4.3.3
</span></span><span class="line"><span class="cl">  4.4.3
</span></span><span class="line"><span class="cl">  4.5.3                    oldrel
</span></span><span class="line"><span class="cl">  4.5.3-x86_64  (R 4.5.3)  oldrel-x86_64
</span></span><span class="line"><span class="cl">  4.6.0
</span></span><span class="line"><span class="cl">* 4.6.1                    release
</span></span><span class="line"><span class="cl">  4.6.1-x86_64  (R 4.6.1)  release-x86_64
</span></span><span class="line"><span class="cl">  devel         (R 4.7.0)
</span></span><span class="line"><span class="cl">  devel-x86_64  (R 4.7.0)
</span></span><span class="line"><span class="cl">  next          (R 4.6.1)
</span></span><span class="line"><span class="cl">  next-x86_64   (R 4.6.1)</span></span></code></pre></div></div>
<ul>
<li>The native (arm64) builds are named simply with the version number.</li>
<li>R-devel is simply named <code>devel</code> and the next version of R (R-patched,
R-alpha, etc.) is named <code>next</code>.</li>
<li>The x86_64 builds are suffixed with <code>-x86_64</code>.</li>
<li>You can use the <code>release</code> alias for the most recent version of R and
the <code>oldrel</code> alias for the previous minor release branch. E.g. the
<code>R-release</code> quick link starts the latest version of R.</li>
</ul>
<h3 id="windows">Windows
</h3>
<p>On Windows rig installs CRAN&rsquo;s R builds as a regular user in user mode.
This installation writes the user&rsquo;s registry settings instead of the
system-wide registry settings.</p>
<p>The names of the R installations are the same as for admin mode. rig now
supports installing x86_64 and aarch64 R builds on aarch64 Windows, and
similarly to macOS, the non-native builds get a <code>-x86_64</code> suffix:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">PS C:\Users\Gabor Csardi&gt; rig ls
</span></span><span class="line"><span class="cl">* name          version    aliases
</span></span><span class="line"><span class="cl">------------------------------------------
</span></span><span class="line"><span class="cl">  4.4.3
</span></span><span class="line"><span class="cl">  4.4.3-x86_64  (R 4.4.3)
</span></span><span class="line"><span class="cl">  4.5.3
</span></span><span class="line"><span class="cl">* 4.6.1                    release
</span></span><span class="line"><span class="cl">  devel         (R 4.7.0)
</span></span><span class="line"><span class="cl">  next          (R 4.6.1)</span></span></code></pre></div></div>
<p>The new <code>rig rtools</code> command lets you manage Rtools installations. In user
mode they are installed into the user&rsquo;s roaming profile directory, and rig
configures R and Rtools to work together correctly. On aarch64 Windows,
you can install both x86_64 and aarch64 versions of Rtools.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">PS C:\Users\Gabor Csardi&gt; rig rtools ls
</span></span><span class="line"><span class="cl">name  version  full-version   arch     path
</span></span><span class="line"><span class="cl">------------------------------------------------------
</span></span><span class="line"><span class="cl">40    4.0      4.0.3.0        x86_64   C:\Users\Gabor Csardi\AppData\Roaming\rig\data\rtools\40-x86_64
</span></span><span class="line"><span class="cl">44    4.4      4.4.6459.6401  aarch64  C:\Users\Gabor Csardi\AppData\Roaming\rig\data\rtools\44
</span></span><span class="line"><span class="cl">45    4.5      4.5.6768.6492  aarch64  C:\Users\Gabor Csardi\AppData\Roaming\rig\data\rtools\45</span></span></code></pre></div></div>
<h3 id="linux">Linux
</h3>
<p>On Linux rig installs a <a href="https://github.com/rstudio/r-builds#portable-builds-experimental" target="_blank" rel="noopener">portable (manylinux) R build</a> in user
mode. These builds are self-contained, generic glibc (or musl) builds of R.
These builds are intended to work on a wide range of Linux distributions.
However, they are not as battle-tested as the builds rig uses in admin mode.</p>
<p>On glibc systems rig also configures the <a href="https://posit.co/blog/introducing-portable-linux-r-binary-packages" target="_blank" rel="noopener">manylinux repository</a>
for CRAN, that pairs very well with the portable R builds. These packages
are self-contained and compatible with most glibc-based Linux distributions:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="o">&gt;</span> <span class="nf">getOption</span><span class="p">(</span><span class="s">&#34;repos&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                                                         <span class="n">P3M</span><span class="o">-</span><span class="n">manylinux</span>
</span></span><span class="line"><span class="cl"><span class="s">&#34;https://packagemanager.posit.co/cran/__linux__/manylinux_2_28/latest&#34;</span>
</span></span><span class="line"><span class="cl">                                                                  <span class="n">CRAN</span>
</span></span><span class="line"><span class="cl">                                         <span class="s">&#34;https://cloud.r-project.org&#34;</span></span></span></code></pre></div></div>
<h3 id="positron-and-rstudio-configuration">Positron and RStudio configuration
</h3>
<p>rig automatically configures RStudio (when running <code>rig add</code> and
<code>rig default</code>), so that the rig-default R version is also the default in
RStudio. On macOS and Windows this is automatic. On Linux rig puts the rig
binary directory (see the table above) on the PATH in user mode, and it
is generally on the PATH in admin mode. This is enough to ensure that the
rig-default R version is also the default in RStudio on Linux.</p>
<p>Positron does not have a generic default R version like RStudio does, but
you can choose a default for each workspace. rig (in <code>rig add</code>
and <code>rig default</code>) updates your Positron configuration (the current user&rsquo;s
config file) to make sure that Positron finds all user-mode R installations.</p>
<h3 id="switching-to-user-mode">Switching to user mode
</h3>
<p>Call <code>rig system user-mode</code> to switch a previous setup from admin mode to
user mode. This will reinstall your current R installations in user mode,
and remove the admin mode installation. (But see its options to adjust this!)</p>
<p>See more about the migration at the <a href="https://rig.r-lib.org/tutorial-migrate.html" target="_blank" rel="noopener">rig homepage</a>.</p>
<h2 id="portable-linux">Portable Linux
</h2>
<p>Above you have seen how rig installs portable manylinux builds in user mode.
It is also possible to install them in admin mode. This can be useful for
Linux systems that rig does not have native builds for. To install a
portable Linux build use <code>--platform portable</code>:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">rig add --platform portable 4.6</span></span></code></pre></div></div>
<p>Moreover, if rig does not support a Linux distribution natively, it will
install the manylinux (or musllinux) builds instead, assuming that they
are supported, even in admin mode. With this, most Linux distributions
work automatically, in both admin and user modes.</p>
<h2 id="repository-management">Repository management
</h2>
<p>The rig release improved the <code>rig repos</code> subcommand substantially. These
are the biggest changes.</p>
<h3 id="p3m-by-default">P3M by default
</h3>
<p>rig now configures Posit Public Package Manager (P3M) as a default
repository, on all platforms except aarch64 Windows. (Because P3M would
serve incompatible x86_64 binaries here.) This is because we found
P3M more reliable and because P3M includes older package versions and has
more extensive metadata.</p>
<p>Nevertheless you can opt out with <code>rig add --without-repos=p3m ...</code> at
installation time, or with <code>rig repos setup --without-repos=p3m</code> at any
time.</p>
<h3 id="custom-repositories">Custom repositories
</h3>
<p>You can now add custom repositories to your rig configuration using
<code>rig repos add &lt;name&gt; &lt;url&gt;</code>. For example:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">rig repos add myrepo https://myrepo.example.com</span></span></code></pre></div></div>
<p>You can then turn these (and also the built-in) repositories on or off
using <code>rig repos enable</code> and <code>rig repos disable</code>.</p>
<h3 id="improved-repository-list">Improved repository list
</h3>
<p>The output of <code>rig repos available</code> is now more informative and easier to
read. Type <code>rig repos available &lt;reponame&gt;</code> to get more information about a
repository.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig repos available
</span></span><span class="line"><span class="cl">8 repositories
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Name              Default   Type       Title
</span></span><span class="line"><span class="cl">-----------------------------------------------------------------------------
</span></span><span class="line"><span class="cl">P3M               depends   built-in   Posit Public Package Manager
</span></span><span class="line"><span class="cl">P3M-manylinux     depends   built-in   Posit Package Manager manylinux portable packages
</span></span><span class="line"><span class="cl">RHUB              no        built-in   R-Hub repositories
</span></span><span class="line"><span class="cl">r-universe/cran   depends   built-in   R-universe CRAN mirror
</span></span><span class="line"><span class="cl">r-universe/bioc   no        built-in   R-universe Bioconductor mirror
</span></span><span class="line"><span class="cl">CRAN              yes       built-in   The Comprehensive R Archive Network
</span></span><span class="line"><span class="cl">CRAN-archive      depends   built-in   CRAN archive
</span></span><span class="line"><span class="cl">Bioconductor      no        built-in   Open source software for Bioinformatics
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Use `rig repos available &lt;name&gt;` to see a repository&#39;s URLs.
</span></span><span class="line"><span class="cl">`depends`: a default only on some platforms, architectures or R versions.</span></span></code></pre></div></div>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig repos available r-universe/cran
</span></span><span class="line"><span class="cl">r-universe/cran (1 URL)
</span></span><span class="line"><span class="cl">R-universe CRAN mirror
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">R-universe&#39;s mirror of CRAN. The only repository serving arm64 Windows binary
</span></span><span class="line"><span class="cl">packages.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Default      only on platforms matching aarch64-*-mingw32
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">URL          https://cran.r-universe.dev
</span></span><span class="line"><span class="cl">Platforms    windows, *-mingw32
</span></span><span class="line"><span class="cl">Archs        aarch64</span></span></code></pre></div></div>
<h3 id="repository-status">Repository status
</h3>
<p>Use <code>rig repos status</code> to check the status of all enabled repositories.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">❯ rig repos status
</span></span><span class="line"><span class="cl">6 repositories of R 4.6.1, index bin/macosx/sonoma-arm64/contrib/4.6
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">name              ping   status        types              updated            url
</span></span><span class="line"><span class="cl">-----------------------------------------------------------------------------
</span></span><span class="line"><span class="cl">P3M             601 ms   ok            source, win, mac   -                  https://packagemanager.posit.co/cran/latest
</span></span><span class="line"><span class="cl">BioCsoft        233 ms   ok            source, win, mac   2026-10-01 17:08   https://bioconductor.org/packages/3.23/bioc
</span></span><span class="line"><span class="cl">BioCann         120 ms   source only   source, win, mac   2026-08-01 16:49   https://bioconductor.org/packages/3.23/data/annotation
</span></span><span class="line"><span class="cl">BioCexp         111 ms   source only   source, win, mac   2026-09-15 20:32   https://bioconductor.org/packages/3.23/data/experiment
</span></span><span class="line"><span class="cl">BioCworkflows   111 ms   source only   source, win, mac   2026-08-01 16:39   https://bioconductor.org/packages/3.23/workflows
</span></span><span class="line"><span class="cl">BioCbooks       120 ms   source only   source, win, mac   2026-08-01 16:25   https://bioconductor.org/packages/3.23/books
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">`ping` is the time the repository took to answer for the index above.
</span></span><span class="line"><span class="cl">`types` is what the R installation&#39;s `repositories` file declares.
</span></span><span class="line"><span class="cl">`source only`: this repository has no index for this platform and R version.</span></span></code></pre></div></div>
<div class="callout callout-note" role="note" aria-label="Note">
<div class="callout-header">
<span class="callout-title">Note</span>
</div>
<div class="callout-body">
<p>rig does not manage repositories in Positron and RStudio sessions. Use
the built-in Positron and RStudio tools instead.</p>
</div>
</div>
<h2 id="other-improvements">Other improvements
</h2>
<h3 id="avoid-unneeded-reinstalls">Avoid unneeded reinstalls
</h3>
<p><code>rig add</code> now does not reinstall an R version if it is already installed.
Use <code>--reinstall</code> to force a reinstall.</p>
<h3 id="exe-quick-links-on-windows"><code>.exe</code> quick links on Windows
</h3>
<p>On Windows rig now uses <code>.exe</code> quick links instead of <code>.bat</code> files.
<code>.bat</code> files have a lot of issues with passing arguments, <code>.exe</code> files
work much better.</p>
<h3 id="run-r-with-r-on-windows">Run R with <code>R</code> on Windows
</h3>
<p>The new <code>rig system fix-r-alias</code> subcommand deletes the <code>R</code> alias from
PowerShell, so you can now use <code>R</code> to start R.</p>
<h3 id="rig-is-now-signed-on-windows">rig is now signed on Windows
</h3>
<p>Thanks to the <a href="https://signpath.org" target="_blank" rel="noopener">SignPath Foundation</a>, the rig
executables and the rig installer are all signed now.</p>
<h3 id="rig-system-blas"><code>rig system blas</code>
</h3>
<p>The new <code>rig system blas</code> command lets you manage the BLAS/LAPACK library
used by R on macOS.</p>
<h3 id="update-and-uninstall">Update and uninstall
</h3>
<p>Use <code>rig self update</code> to update rig itself, and <code>rig self uninstall</code> to
uninstall it. These commands only work if you installed rig using the
install script. If you installed it with a package manager or ran the
installer manually, you have to use the same package manager or
uninstall it manually.</p>
<h2 id="your-feedback-is-welcome">Your feedback is welcome!
</h2>
<p>We would love to hear your thoughts, suggestions, and any issues you
encounter while using rig. Please use the <a href="https://github.com/r-lib/rig/issues" target="_blank" rel="noopener">issue tracker</a> for bug reports and feature
requests, and the <a href="https://github.com/r-lib/rig/discussions" target="_blank" rel="noopener">discussion forum</a> or <a href="https://forum.posit.co/" target="_blank" rel="noopener">Posit community</a> for general questions and discussions.</p>
<h2 id="acknowledgments">Acknowledgments
</h2>
<p>Huge thanks to everyone who contributed to rig 0.10.0 and 0.11.0:
<a href="https://github.com/achubaty" target="_blank" rel="noopener">@achubaty</a>,
<a href="https://github.com/AdaemmerP" target="_blank" rel="noopener">@AdaemmerP</a>,
<a href="https://github.com/AdrienLeGuillou" target="_blank" rel="noopener">@AdrienLeGuillou</a>,
<a href="https://github.com/Adrilihan" target="_blank" rel="noopener">@Adrilihan</a>,
<a href="https://github.com/Andryas" target="_blank" rel="noopener">@Andryas</a>,
<a href="https://github.com/bashirhamidi" target="_blank" rel="noopener">@bashirhamidi</a>,
<a href="https://github.com/benyamins" target="_blank" rel="noopener">@benyamins</a>,
<a href="https://github.com/biocyberman" target="_blank" rel="noopener">@biocyberman</a>,
<a href="https://github.com/Bisaloo" target="_blank" rel="noopener">@Bisaloo</a>,
<a href="https://github.com/cderv" target="_blank" rel="noopener">@cderv</a>,
<a href="https://github.com/CGMossa" target="_blank" rel="noopener">@CGMossa</a>,
<a href="https://github.com/danielloader" target="_blank" rel="noopener">@danielloader</a>,
<a href="https://github.com/dkczk" target="_blank" rel="noopener">@dkczk</a>,
<a href="https://github.com/eitsupi" target="_blank" rel="noopener">@eitsupi</a>,
<a href="https://github.com/elendil95" target="_blank" rel="noopener">@elendil95</a>,
<a href="https://github.com/eliocamp" target="_blank" rel="noopener">@eliocamp</a>,
<a href="https://github.com/EllaKaye" target="_blank" rel="noopener">@EllaKaye</a>,
<a href="https://github.com/etiennebacher" target="_blank" rel="noopener">@etiennebacher</a>,
<a href="https://github.com/frosforever" target="_blank" rel="noopener">@frosforever</a>,
<a href="https://github.com/gdevenyi" target="_blank" rel="noopener">@gdevenyi</a>,
<a href="https://github.com/ggrothendieck" target="_blank" rel="noopener">@ggrothendieck</a>,
<a href="https://github.com/grantmcdermott" target="_blank" rel="noopener">@grantmcdermott</a>,
<a href="https://github.com/gvelasq" target="_blank" rel="noopener">@gvelasq</a>,
<a href="https://github.com/hadley" target="_blank" rel="noopener">@hadley</a>,
<a href="https://github.com/jabenninghoff" target="_blank" rel="noopener">@jabenninghoff</a>,
<a href="https://github.com/jameslairdsmith" target="_blank" rel="noopener">@jameslairdsmith</a>,
<a href="https://github.com/jennybc" target="_blank" rel="noopener">@jennybc</a>,
<a href="https://github.com/jeroen" target="_blank" rel="noopener">@jeroen</a>,
<a href="https://github.com/jfin4" target="_blank" rel="noopener">@jfin4</a>,
<a href="https://github.com/John15321" target="_blank" rel="noopener">@John15321</a>,
<a href="https://github.com/jonbry" target="_blank" rel="noopener">@jonbry</a>,
<a href="https://github.com/JosiahParry" target="_blank" rel="noopener">@JosiahParry</a>,
<a href="https://github.com/kalenkovich" target="_blank" rel="noopener">@kalenkovich</a>,
<a href="https://github.com/kenahoo" target="_blank" rel="noopener">@kenahoo</a>,
<a href="https://github.com/kieran-mace" target="_blank" rel="noopener">@kieran-mace</a>,
<a href="https://github.com/klmr" target="_blank" rel="noopener">@klmr</a>,
<a href="https://github.com/krlmlr" target="_blank" rel="noopener">@krlmlr</a>,
<a href="https://github.com/lotum-david-j" target="_blank" rel="noopener">@lotum-david-j</a>,
<a href="https://github.com/malcolmbarrett" target="_blank" rel="noopener">@malcolmbarrett</a>,
<a href="https://github.com/mcanouil" target="_blank" rel="noopener">@mcanouil</a>,
<a href="https://github.com/mhurtado13" target="_blank" rel="noopener">@mhurtado13</a>,
<a href="https://github.com/MilesMcBain" target="_blank" rel="noopener">@MilesMcBain</a>,
<a href="https://github.com/mitsuki5284" target="_blank" rel="noopener">@mitsuki5284</a>,
<a href="https://github.com/mns-nordicals" target="_blank" rel="noopener">@mns-nordicals</a>,
<a href="https://github.com/morphatic" target="_blank" rel="noopener">@morphatic</a>,
<a href="https://github.com/Nerwosolek" target="_blank" rel="noopener">@Nerwosolek</a>,
<a href="https://github.com/noamross" target="_blank" rel="noopener">@noamross</a>,
<a href="https://github.com/paluigi" target="_blank" rel="noopener">@paluigi</a>,
<a href="https://github.com/robjhyndman" target="_blank" rel="noopener">@robjhyndman</a>,
<a href="https://github.com/rumichaska" target="_blank" rel="noopener">@rumichaska</a>,
<a href="https://github.com/SaintRod" target="_blank" rel="noopener">@SaintRod</a>,
<a href="https://github.com/sammieephung" target="_blank" rel="noopener">@sammieephung</a>,
<a href="https://github.com/sda030" target="_blank" rel="noopener">@sda030</a>,
<a href="https://github.com/ShixiangWang" target="_blank" rel="noopener">@ShixiangWang</a>,
<a href="https://github.com/t-kalinowski" target="_blank" rel="noopener">@t-kalinowski</a>,
<a href="https://github.com/tylermorganwall" target="_blank" rel="noopener">@tylermorganwall</a>,
<a href="https://github.com/venpopov" target="_blank" rel="noopener">@venpopov</a>,
<a href="https://github.com/vikasrawal" target="_blank" rel="noopener">@vikasrawal</a>,
<a href="https://github.com/wzbillings" target="_blank" rel="noopener">@wzbillings</a>, and
<a href="https://github.com/zivankaraman" target="_blank" rel="noopener">@zivankaraman</a>.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-10-09_rig-0-11-0-part-one/featured.jpg" length="299922" type="image/jpeg" />
    </item>
    <item>
      <title>orbital 0.7.0</title>
      <link>https://opensource.posit.co/blog/2026-09-09_orbital-0-7-0/</link>
      <pubDate>Wed, 09 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-09-09_orbital-0-7-0/</guid>
      <dc:creator>Emil Hvitfeldt</dc:creator><description><![CDATA[<p>We&rsquo;re happy to announce the release of <a href="https://orbital.tidymodels.org/" target="_blank" rel="noopener">R-orbital</a> 0.7.0.
orbital turns a fitted tidymodels workflow into the set of equations that produce its predictions,
so you can run those predictions in a database instead of moving the data to R.
It uses <a href="https://tidypredict.tidymodels.org/" target="_blank" rel="noopener">tidypredict</a> under the hood to translate fitted models.
This post also covers the release of tidypredict 1.2.0.</p>
<p>This post is about the R package.
There is also a <a href="https://posit-dev.github.io/orbital/" target="_blank" rel="noopener">Python version of orbital</a> that works on scikit-learn pipelines.</p>
<p>You can install both from CRAN with:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="nf">c</span><span class="p">(</span><span class="s">&#34;orbital&#34;</span><span class="p">,</span> <span class="s">&#34;tidypredict&#34;</span><span class="p">))</span></span></span></code></pre></div></div>
<p>This post covers the highlights of these releases.
You can see the full list of changes in the <a href="https://orbital.tidymodels.org/news/index.html" target="_blank" rel="noopener">orbital release notes</a> and the <a href="https://tidypredict.tidymodels.org/news/index.html" target="_blank" rel="noopener">tidypredict release notes</a>.</p>
<h2 id="a-lot-more-models">A lot more models
</h2>
<p>Previously orbital and tidypredict supported a handful of useful models.
This included linear models, decision trees, random forests, and boosted trees models.
This release adds discriminant analysis, naive Bayes, neural networks, support vector machines, partial least squares,
and several more rule and ensemble methods.
This fills in the gaps of most of the known models that can be made to work with orbital.</p>
<p>Newly supported for regression:</p>
<ul>
<li><code>bart(engine = &quot;dbarts&quot;)</code></li>
<li><code>boost_tree(engine = &quot;h2o_gbm&quot;)</code></li>
<li><code>linear_reg(engine = &quot;glm&quot;)</code></li>
<li><code>mlp(engine = &quot;nnet&quot;)</code></li>
<li><code>null_model()</code></li>
<li><code>pls(engine = &quot;mixOmics&quot;)</code></li>
<li><code>rand_forest(engine = &quot;aorsf&quot;)</code></li>
<li><code>rand_forest(engine = &quot;partykit&quot;)</code></li>
<li><code>rule_fit(engine = &quot;h2o&quot;)</code></li>
<li><code>svm_linear(engine = &quot;kernlab&quot;)</code></li>
<li><code>svm_linear(engine = &quot;LiblineaR&quot;)</code></li>
</ul>
<p>Newly supported for classification:</p>
<ul>
<li><code>bag_tree(engine = &quot;rpart&quot;)</code> and <code>bag_tree(engine = &quot;C5.0&quot;)</code></li>
<li><code>boost_tree(engine = &quot;C5.0&quot;)</code></li>
<li><code>boost_tree(engine = &quot;h2o_gbm&quot;)</code></li>
<li><code>C5_rules(engine = &quot;C5.0&quot;)</code></li>
<li><code>decision_tree(engine = &quot;C5.0&quot;)</code></li>
<li><code>discrim_linear(engine = &quot;MASS&quot;)</code>, <code>discrim_quad(engine = &quot;MASS&quot;)</code></li>
<li><code>discrim_linear()</code> with the <code>&quot;mda&quot;</code>, <code>&quot;sda&quot;</code>, and <code>&quot;sparsediscrim&quot;</code> engines</li>
<li><code>logistic_reg(engine = &quot;LiblineaR&quot;)</code></li>
<li><code>mlp(engine = &quot;nnet&quot;)</code></li>
<li><code>multinom_reg(engine = &quot;nnet&quot;)</code></li>
<li><code>naive_Bayes(engine = &quot;klaR&quot;)</code> and <code>naive_Bayes(engine = &quot;naivebayes&quot;)</code></li>
<li><code>null_model()</code></li>
<li><code>pls(engine = &quot;mixOmics&quot;)</code></li>
<li><code>rule_fit(engine = &quot;h2o&quot;)</code></li>
<li><code>rule_fit(engine = &quot;xrf&quot;)</code></li>
<li><code>svm_linear(engine = &quot;kernlab&quot;)</code></li>
<li><code>svm_linear(engine = &quot;LiblineaR&quot;)</code></li>
</ul>
<p>One thing worth noting for models with &ldquo;h2o&rdquo; in their engine value, is that you need a running H2O cluster to build the orbital object.
Prediction and SQL generation from the orbital object stays dependency free.</p>
<p>Here is an example of linear discriminant analysis model,
which was not supported before this release,
fit on the penguins data and then run in DuckDB.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">tidymodels</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">discrim</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">orbital</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">duckdb</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">data</span><span class="p">(</span><span class="n">penguins</span><span class="p">,</span> <span class="n">package</span> <span class="o">=</span> <span class="s">&#34;modeldata&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">penguins</span> <span class="o">&lt;-</span> <span class="n">tidyr</span><span class="o">::</span><span class="nf">drop_na</span><span class="p">(</span><span class="n">penguins</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">rec_spec</span> <span class="o">&lt;-</span> <span class="nf">recipe</span><span class="p">(</span><span class="n">species</span> <span class="o">~</span> <span class="n">.,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">penguins</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">step_dummy</span><span class="p">(</span><span class="nf">all_nominal_predictors</span><span class="p">())</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">step_normalize</span><span class="p">(</span><span class="nf">all_numeric_predictors</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">mod_spec</span> <span class="o">&lt;-</span> <span class="nf">discrim_linear</span><span class="p">(</span><span class="n">engine</span> <span class="o">=</span> <span class="s">&#34;MASS&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">wf_spec</span> <span class="o">&lt;-</span> <span class="nf">workflow</span><span class="p">(</span><span class="n">rec_spec</span><span class="p">,</span> <span class="n">mod_spec</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">fit</span><span class="p">(</span><span class="n">penguins</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">orb</span> <span class="o">&lt;-</span> <span class="nf">orbital</span><span class="p">(</span><span class="n">wf_spec</span><span class="p">,</span> <span class="n">type</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="s">&#34;class&#34;</span><span class="p">,</span> <span class="s">&#34;prob&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">con</span> <span class="o">&lt;-</span> <span class="n">DBI</span><span class="o">::</span><span class="nf">dbConnect</span><span class="p">(</span><span class="n">duckdb</span><span class="o">::</span><span class="nf">duckdb</span><span class="p">())</span>
</span></span><span class="line"><span class="cl"><span class="n">penguins_db</span> <span class="o">&lt;-</span> <span class="n">dplyr</span><span class="o">::</span><span class="nf">copy_to</span><span class="p">(</span><span class="n">con</span><span class="p">,</span> <span class="n">penguins</span><span class="p">,</span> <span class="s">&#34;penguins&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">predict</span><span class="p">(</span><span class="n">orb</span><span class="p">,</span> <span class="n">penguins_db</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code># A query:  ?? x 4
# Database: DuckDB 1.5.5 [root@Darwin 25.6.0:R 4.6.1/:memory:]
   .pred_class .pred_Adelie .pred_Chinstrap .pred_Gentoo
   &lt;chr&gt;              &lt;dbl&gt;           &lt;dbl&gt;        &lt;dbl&gt;
 1 Adelie             1.000        6.43e- 9     4.64e-25
 2 Adelie             1.000        5.25e- 6     8.81e-16
 3 Adelie             1.000        1.97e- 4     2.61e-17
 4 Adelie             1.000        2.99e- 8     1.93e-23
 5 Adelie             1.000        8.34e- 9     4.42e-29
 6 Adelie             1.000        1.60e- 6     4.14e-20
 7 Adelie             1.000        6.03e-10     1.45e-18
 8 Adelie             0.999        6.47e- 4     4.16e-20
 9 Adelie             1.000        9.56e-10     1.97e-30
10 Adelie             1.000        6.73e-14     9.13e-27
# ℹ more rows
</code></pre>
<p>We have documented the <a href="https://orbital.tidymodels.org/articles/supported-models.html" target="_blank" rel="noopener">list of supported models</a>.
If there is a model you need that is still missing,
<a href="https://github.com/tidymodels/tidypredict/issues" target="_blank" rel="noopener">let us know</a> so we can prioritize it.</p>
<h2 id="when-a-model-has-no-probability">When a model has no probability
</h2>
<p>While we added these new methods we ran into a problem we didn&rsquo;t have before.
For many classification models you get both predicted probabilities and hard class predictions.
This release has added models where that is not the case.
A number of newly added models only produce hard class predictions.</p>
<p>Instead of trying to invent class probabilities,
we produce an informative error for the affected models.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">c5_wf</span> <span class="o">&lt;-</span> <span class="nf">workflow</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">species</span> <span class="o">~</span> <span class="n">.,</span>
</span></span><span class="line"><span class="cl">  <span class="nf">decision_tree</span><span class="p">(</span><span class="n">mode</span> <span class="o">=</span> <span class="s">&#34;classification&#34;</span><span class="p">,</span> <span class="n">engine</span> <span class="o">=</span> <span class="s">&#34;C5.0&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">fit</span><span class="p">(</span><span class="n">penguins</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">orbital</span><span class="p">(</span><span class="n">c5_wf</span><span class="p">,</span> <span class="n">type</span> <span class="o">=</span> <span class="s">&#34;prob&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code>Error in `orbital()`:
! &quot;prob&quot; predictions are not available for this model.
ℹ It predicts a class directly, with no probability behind it.
ℹ Use `type = &quot;class&quot;` instead.
</code></pre>
<h2 id="tidypredict-is-now-a-toolkit-not-just-a-function">tidypredict is now a toolkit, not just a function
</h2>
<p>We have talked a lot about orbital so far.
This is because we think that it is the ideal interface compared to tidypredict,
if your goal is to generate SQL expressions for a fitted model or workflow.</p>
<p>orbital and tidypredict work together to generate the expression that you need.
We have expanded tidypredict with a number of generics,
which are all developer focused.
With the goal that packages other than orbital can benefit from the work we have done.
Three of them describe what a model&rsquo;s fitted expressions compute:</p>
<ul>
<li><code>tidypredict_output_type()</code> returns <code>&quot;numeric&quot;</code>, <code>&quot;prob&quot;</code>, <code>&quot;decision&quot;</code>, or <code>&quot;class&quot;</code></li>
<li><code>tidypredict_outcome_levels()</code> returns the outcome levels in model order</li>
<li><code>tidypredict_normalized()</code> reports whether per-level probabilities already sum to one</li>
</ul>
<p>Below we see two of the generics in action.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">tidypredict</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">svm_fit</span> <span class="o">&lt;-</span> <span class="nf">svm_linear</span><span class="p">(</span><span class="n">mode</span> <span class="o">=</span> <span class="s">&#34;classification&#34;</span><span class="p">,</span> <span class="n">engine</span> <span class="o">=</span> <span class="s">&#34;LiblineaR&#34;</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">fit</span><span class="p">(</span><span class="n">sex</span> <span class="o">~</span> <span class="n">bill_length_mm</span> <span class="o">+</span> <span class="n">body_mass_g</span><span class="p">,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">penguins</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">tidypredict_output_type</span><span class="p">(</span><span class="n">svm_fit</span><span class="o">$</span><span class="n">fit</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code>[1] &quot;decision&quot;
</code></pre>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">tidypredict_outcome_levels</span><span class="p">(</span><span class="n">svm_fit</span><span class="o">$</span><span class="n">fit</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code>[1] &quot;female&quot; &quot;male&quot;  
</code></pre>
<p>The other five expose the per-tree pieces that <code>tidypredict_fit()</code> assembles,
so a package generating its own code can split an ensemble apart and put it back together:</p>
<ul>
<li><code>tidypredict_trees()</code> returns per-tree expressions</li>
<li><code>tidypredict_n_trees()</code> returns the number of trees</li>
<li><code>tidypredict_combine_trees()</code> turns per-tree expressions back into a prediction</li>
<li><code>tidypredict_class_trees()</code> returns per-tree expressions for each outcome level</li>
<li><code>tidypredict_class_exprs()</code> returns one finished expression per outcome level</li>
</ul>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">rf</span> <span class="o">&lt;-</span> <span class="nf">rand_forest</span><span class="p">(</span><span class="n">mode</span> <span class="o">=</span> <span class="s">&#34;regression&#34;</span><span class="p">,</span> <span class="n">trees</span> <span class="o">=</span> <span class="m">5</span><span class="p">,</span> <span class="n">engine</span> <span class="o">=</span> <span class="s">&#34;ranger&#34;</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">fit</span><span class="p">(</span><span class="n">body_mass_g</span> <span class="o">~</span> <span class="n">bill_length_mm</span> <span class="o">+</span> <span class="n">flipper_length_mm</span><span class="p">,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">penguins</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">tidypredict_n_trees</span><span class="p">(</span><span class="n">rf</span><span class="o">$</span><span class="n">fit</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code>[1] 5
</code></pre>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">tidypredict_combine_trees</span><span class="p">(</span><span class="n">rf</span><span class="o">$</span><span class="n">fit</span><span class="p">,</span> <span class="n">rlang</span><span class="o">::</span><span class="nf">syms</span><span class="p">(</span><span class="nf">paste0</span><span class="p">(</span><span class="s">&#34;tree_&#34;</span><span class="p">,</span> <span class="m">1</span><span class="o">:</span><span class="m">5</span><span class="p">)))</span></span></span></code></pre></div></div>
<pre><code>(tree_1 + tree_2 + tree_3 + tree_4 + tree_5)/5
</code></pre>
<p>A lot of this logic was hardcoded inside orbital on a model by model basis,
it is now formalized in such a way that you can easily use just the bits you need.</p>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>Many thanks to all the people who contributed to orbital and tidypredict since the last release!</p>
<p><a href="https://github.com/EmilHvitfeldt" target="_blank" rel="noopener">@EmilHvitfeldt</a>, <a href="https://github.com/jannikbx" target="_blank" rel="noopener">@jannikbx</a>, and <a href="https://github.com/RAMitchell" target="_blank" rel="noopener">@RAMitchell</a>.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-09-09_orbital-0-7-0/featured.jpg" length="402596" type="image/jpeg" />
    </item>
    <item>
      <title>cuda.ml 0.4.0: GPU-accelerated machine learning from R</title>
      <link>https://opensource.posit.co/blog/2026-08-21_cuda-ml-0-4-0/</link>
      <pubDate>Fri, 21 Aug 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-08-21_cuda-ml-0-4-0/</guid>
      <dc:creator>Tomasz Kalinowski</dc:creator><description><![CDATA[<p><a href="https://mlverse.github.io/cuda.ml/" target="_blank" rel="noopener">cuda.ml</a> is an R package for
running common data science and machine-learning operations on NVIDIA
GPUs. It provides high-level interfaces for fitting regression and
classification models, finding nearest neighbors, clustering
observations, reducing dimensions, and running predictions from tree
ensembles. You can use its direct R functions or work through parsnip
and tidymodels.</p>
<p>cuda.ml is for data scientists who work primarily in R and want to use a
GPU without moving their modeling workflow to Python or learning
low-level GPU APIs. Version 0.4.0 is a substantial update to the
package. It makes installation much simpler, expands tidymodels support,
adds more ways to run tree-ensemble models, and makes it straightforward
to save and restore supported fitted models.</p>
<h2 id="install-from-cran">Install from CRAN
</h2>
<p>For most users, setup is two commands:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;cuda.ml&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">cuda.ml</span><span class="o">::</span><span class="nf">cuda_ml_install</span><span class="p">()</span></span></span></code></pre></div></div>
<p>The package from CRAN is a regular, portable R package.
<code>cuda_ml_install()</code> downloads and verifies the matching compiled backend
and GPU libraries, then keeps them in a cache for later R sessions. On a
supported system, you do not need to compile cuda.ml from source,
configure a Python environment, or assemble the GPU libraries yourself.</p>
<p>The result is a familiar R package workflow: install the package,
prepare its supporting libraries once, and start an analysis. The extra
installation call is explicit because the GPU libraries are much larger
than the R package. Repeated calls reuse the completed cache, and
loading cuda.ml itself is quiet and does not initialize CUDA.</p>
<p>Prebuilt support is available for Linux x86_64 with glibc 2.28 or newer.
GPU operations require a supported NVIDIA GPU and driver 580 or newer.
On Windows, install and run R inside a compatible WSL2 Linux
distribution. See the <a href="https://mlverse.github.io/cuda.ml/articles/install-manage.html" target="_blank" rel="noopener">installation guide</a> for the complete system requirements and source-build options.</p>
<h2 id="use-familiar-modeling-interfaces">Use familiar modeling interfaces
</h2>
<p>cuda.ml registers parsnip engines for linear, logistic, and multinomial
regression, random forests, nearest neighbors, and radial, polynomial,
and linear support-vector machines. For example, this fits a
random-forest classifier on the GPU using the standard parsnip
interface:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">cuda.ml</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">parsnip</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">forest_spec</span> <span class="o">&lt;-</span> <span class="nf">rand_forest</span><span class="p">()</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">set_mode</span><span class="p">(</span><span class="s">&#34;classification&#34;</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">set_engine</span><span class="p">(</span><span class="s">&#34;cuda.ml&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">forest_fit</span> <span class="o">&lt;-</span> <span class="n">forest_spec</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">fit</span><span class="p">(</span><span class="n">class</span> <span class="o">~</span> <span class="n">.,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">modeldata</span><span class="o">::</span><span class="n">hpc_data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">predict</span><span class="p">(</span><span class="n">forest_fit</span><span class="p">,</span> <span class="n">modeldata</span><span class="o">::</span><span class="n">hpc_data[1</span><span class="o">:</span><span class="m">5</span><span class="p">,</span> <span class="n">]</span><span class="p">,</span> <span class="n">type</span> <span class="o">=</span> <span class="s">&#34;prob&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># A tibble: 5 × 4
</span></span><span class="line"><span class="cl">  .pred_VF .pred_F .pred_M  .pred_L
</span></span><span class="line"><span class="cl">     &lt;dbl&gt;   &lt;dbl&gt;   &lt;dbl&gt;    &lt;dbl&gt;
</span></span><span class="line"><span class="cl">1    0.309  0.597  0.0666  0.0275
</span></span><span class="line"><span class="cl">2    0.899  0.0838 0.0138  0.00335
</span></span><span class="line"><span class="cl">3    0.965  0.0261 0.00850 0.000883
</span></span><span class="line"><span class="cl">4    0.973  0.0228 0.00416 0.000352
</span></span><span class="line"><span class="cl">5    0.966  0.0298 0.00416 0.000352</span></span></code></pre></div></div>
<p><code>set_engine(&quot;cuda.ml&quot;)</code> selects the GPU-backed cuda.ml engine; the rest
is a standard parsnip workflow. Recipes can learn preprocessing on the
training data and carry it into resampling and prediction.</p>
<h2 id="work-directly-with-cudaml">Work directly with cuda.ml
</h2>
<p>cuda.ml also provides a direct R interface. This is useful when you
prefer a function-oriented workflow or want to use the package on its
own.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">cuda.ml</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">ggplot2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">clusters</span> <span class="o">&lt;-</span> <span class="nf">cuda_ml_kmeans</span><span class="p">(</span><span class="nf">scale</span><span class="p">(</span><span class="n">faithful</span><span class="p">),</span> <span class="n">k</span> <span class="o">=</span> <span class="m">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">faithful</span><span class="o">$</span><span class="n">cluster</span> <span class="o">&lt;-</span> <span class="nf">factor</span><span class="p">(</span><span class="n">clusters</span><span class="o">$</span><span class="n">labels</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">ggplot</span><span class="p">(</span><span class="n">faithful</span><span class="p">,</span> <span class="nf">aes</span><span class="p">(</span><span class="n">eruptions</span><span class="p">,</span> <span class="n">waiting</span><span class="p">,</span> <span class="n">color</span> <span class="o">=</span> <span class="n">cluster</span><span class="p">))</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">  <span class="nf">geom_point</span><span class="p">(</span><span class="n">size</span> <span class="o">=</span> <span class="m">2.5</span><span class="p">)</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">  <span class="nf">labs</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">x</span> <span class="o">=</span> <span class="s">&#34;Eruption duration (minutes)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">y</span> <span class="o">=</span> <span class="s">&#34;Waiting time (minutes)&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">  <span class="nf">theme_minimal</span><span class="p">()</span></span></span></code></pre></div></div>
<img src="https://opensource.posit.co/blog/2026-08-21_cuda-ml-0-4-0/index.markdown_strict_files/figure-markdown_strict/faithful-clusters-1.png" data-fig-align="center" data-fig-alt="Scatterplot of Old Faithful eruption durations and waiting times, colored by two clusters. Shorter eruptions have shorter waits, while longer eruptions have longer waits." width="768" />
<p>The direct API covers supervised models as well as clustering and
dimensionality reduction, including DBSCAN, k-means, PCA, tSVD, UMAP,
and t-SNE. It also includes stochastic-gradient-descent regression, the
hyperbolic-tangent SVM kernel, and external tree-ensemble inference.</p>
<h2 id="run-tree-ensembles-on-a-gpu-or-cpu">Run tree ensembles on a GPU or CPU
</h2>
<p>This release expands where and how you can make predictions with tree
ensembles. The new nvForest support powers prediction for random forests
trained with <code>cuda_ml_rand_forest()</code> and can load trained XGBoost
models, LightGBM text models, and Treelite checkpoints. Once a model is
loaded, the API provides standard prediction along with model
information, leaf identifiers, individual-tree predictions, and
checkpoint import and export.</p>
<p>GPU inference uses the complete cuda.ml installation. A deployment that
only needs CPU inference can prepare a smaller, CUDA-free backend:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">cuda.ml</span><span class="o">::</span><span class="nf">cuda_ml_install</span><span class="p">(</span><span class="n">device</span> <span class="o">=</span> <span class="s">&#34;cpu&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>The CPU backend can run a cuda.ml random forest trained on a GPU as well
as a supported external tree ensemble. The complete installation can
also run these models on a CPU, so the smaller backend is an optional
deployment choice.</p>
<h2 id="save-and-deploy-fitted-models">Save and deploy fitted models
</h2>
<p>cuda.ml 0.4.0 expands model persistence for training, analysis, and
deployment workflows. Supported fitted models can be saved to a
compressed file, restored in another R process, stored as raw bytes, or
wrapped with the bundle package.</p>
<p>The simplest file workflow passes a path directly:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">model</span> <span class="o">&lt;-</span> <span class="nf">cuda_ml_rand_forest</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">class</span> <span class="o">~</span> <span class="n">.,</span>
</span></span><span class="line"><span class="cl">  <span class="n">data</span> <span class="o">=</span> <span class="n">modeldata</span><span class="o">::</span><span class="n">hpc_data</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">trees</span> <span class="o">=</span> <span class="m">100</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">cuda_ml_serialize</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="s">&#34;hpc-runtime-forest.cuda-ml&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>In another R process or deployment environment, prepare cuda.ml and
restore the fitted model:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">cuda.ml</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">cuda_ml_install</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">model</span> <span class="o">&lt;-</span> <span class="nf">cuda_ml_unserialize</span><span class="p">(</span><span class="s">&#34;hpc-runtime-forest.cuda-ml&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">predict</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">modeldata</span><span class="o">::</span><span class="n">hpc_data[1</span><span class="o">:</span><span class="m">5</span><span class="p">,</span> <span class="n">]</span><span class="p">,</span> <span class="n">type</span> <span class="o">=</span> <span class="s">&#34;class&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>File paths use gzip compression. Passing <code>connection = NULL</code> instead
returns the state as an uncompressed raw vector of bytes, which is
convenient for object stores and database BLOB columns. The blob package
can represent raw vectors for database workflows, and the bundle package
is supported for teams that already use bundled model artifacts.</p>
<p>Persistence is available for linear models, logistic and multinomial
regression, PCA, SVC and SVR models, UMAP, random forests, and nvForest
models. cuda.ml checks the saved state and required backend before
restoring it. For an nvForest-backed model, the restore call can select
CPU or GPU inference.</p>
<h2 id="highlights-for-users-upgrading-from-cudaml-03">Highlights for users upgrading from cuda.ml 0.3
</h2>
<p>This is a breaking update to the earlier package. The most visible
changes are:</p>
<ul>
<li>The random-forest API now uses <code>mtry</code> for predictor sampling and
<code>sample_fraction</code> for row sampling. <code>trees</code> defaults to 100, and an
omitted <code>seed</code> draws from R&rsquo;s random-number generator, so <code>set.seed()</code>
controls the fit.</li>
<li>Linear, logistic, and multinomial regression now use numeric <code>penalty</code>
and <code>mixture</code> arguments that match parsnip. Logistic and multinomial
regression are unregularized by default.</li>
<li><code>normalize_input</code> was removed from the linear-model functions. It
previously requested GPU-side L2 normalization. Use explicit
preprocessing such as <code>recipes::step_normalize()</code> when centering and
scaling are appropriate; but please note, the two operations are not
numerically identical.</li>
<li><code>cuda_ml_sgd()</code> now fits squared-loss regression only. Its <code>loss</code>
argument was removed, and <code>n_iters_no_change</code> is now
<code>n_iter_no_change</code>.</li>
<li>The former FIL interface has been replaced by nvForest. Random
projection and the KNN IVFSQ index have no replacement in the pinned
upstream API.</li>
</ul>
<p>See the
<a href="https://mlverse.github.io/cuda.ml/news/index.html" target="_blank" rel="noopener">full changelog</a> for
the complete list of API changes.</p>
<p>The guides cover first steps and more complete examples:</p>
<ul>
<li><a href="https://mlverse.github.io/cuda.ml/articles/cuda-ml.html" target="_blank" rel="noopener">Get started with cuda.ml</a></li>
<li><a href="https://mlverse.github.io/cuda.ml/articles/tidymodels.html" target="_blank" rel="noopener">Use cuda.ml with tidymodels</a></li>
<li><a href="https://mlverse.github.io/cuda.ml/articles/model-persistence.html" target="_blank" rel="noopener">Save and restore models</a></li>
<li><a href="https://mlverse.github.io/cuda.ml/articles/nvforest.html" target="_blank" rel="noopener">nvForest inference and deployment</a></li>
</ul>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-08-21_cuda-ml-0-4-0/featured-alt.png" length="2702295" type="image/png" />
    </item>
    <item>
      <title>Neural networks in Orbital for Python 0.6.0: PyTorch straight to your database</title>
      <link>https://opensource.posit.co/blog/2026-08-17_pyorbital-0-6-0/</link>
      <pubDate>Mon, 17 Aug 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-08-17_pyorbital-0-6-0/</guid>
      <dc:creator>Alessandro Molina</dc:creator><description><![CDATA[<p>Over the past couple of months I&rsquo;ve been teaching Orbital to speak PyTorch.</p>
<p>Orbital&rsquo;s whole pitch is that a trained model becomes SQL, so a database can run predictions on its own, with no Python process anywhere near it. Until 0.6.0, &ldquo;trained model&rdquo; meant scikit-learn: pipelines, trees, linear models, all <code>.fit()</code> in Python and then turned into a <code>SELECT</code> statement. It never covered what a lot of teams are actually training now: PyTorch models, not scikit-learn pipelines.</p>
<p>Orbital 0.6.0 closes that gap. A <code>torch.nn.Sequential</code> network, trained exactly the way you already train it, now compiles to the same kind of SQL a linear regression would.</p>
<p>No ONNX Runtime. No model server. No separate inference service to keep alive next to the database. Just a query.</p>
<p>Why does that work at all, for a framework Orbital was never written for? Because of a decision made long before PyTorch was ever on the table.</p>
<h2 id="why-this-isnt-a-bolt-on">Why this isn&rsquo;t a bolt-on
</h2>
<p>Orbital was never really a scikit-learn tool. Underneath, it converts a scikit-learn pipeline to ONNX (Open Neural Network Exchange, a standard graph format for trained models) using the <code>skl2onnx</code> library, then walks that graph node by node to produce SQL. Scikit-learn was always one hop removed from what Orbital actually translates.</p>
<p>Adding PyTorch meant taking the same hop from a different starting point. <code>torch.onnx.export</code> turns a <code>torch.nn.Sequential</code> model into that same kind of graph. Feed it into the translator that already existed, and the translator doesn&rsquo;t know or care whether the graph came from PyTorch or scikit-learn.</p>
<p>The proof is in how little new code that took. The entire engine for running a feed-forward network in SQL is three small classes: a <code>Gemm</code> translator for <code>Linear</code> layers, <code>ReLU</code>, <code>Sigmoid</code>. Everything else (the translator base class, the optimizer, the per-dialect SQL compiler) already existed, built earlier for trees and linear models.</p>
<p>When I first thought of support for PyTorch, I put it this way: <em>&ldquo;the underlying translation works on ONNX graphs&hellip; the same value proposition orbital already provides for scikit-learn models can apply as it is to pytorch networks exported to ONNX.&rdquo;</em> That sentence turned out to be the whole implementation plan.</p>
<p>If you want the fuller picture of how a graph becomes SQL, parser, then translator, then optimizer, the <a href="https://posit-dev.github.io/orbital/learnmore/" target="_blank" rel="noopener">architecture docs</a> walk through all three stages in order.</p>
<p>That architecture is also why I keep calling this <strong>multiple frameworks</strong>, not two frameworks. <code>Relu</code>, <code>Sigmoid</code>, <code>Tanh</code>, and <code>Softmax</code> are single translators, not one per framework. Scikit-learn&rsquo;s <code>MLPClassifier</code> and <code>MLPRegressor</code> reach them through <code>MatMul</code> and <code>Add</code>, PyTorch&rsquo;s <code>nn.Sequential</code> reaches the exact same translators through <code>Gemm</code>. One implementation, two entry points, and no reason it has to stop at two.</p>
<p>Scikit-learn and PyTorch are both real and shipping today, which is enough on its own to call this &ldquo;multiple frameworks.&rdquo; But the dependency story is already moving that direction: <a href="https://github.com/posit-dev/orbital/issues/113" target="_blank" rel="noopener">issue #113</a> proposes turning scikit-learn itself into an optional dependency, the same way PyTorch already is, so the core stops assuming any particular framework at all.</p>
<h2 id="making-it-actually-usable">Making it actually usable
</h2>
<p>Neural networks are layered, and every neuron in layer two reads every output of layer one. If Orbital just inlines those outputs at each place they&rsquo;re read, instead of naming them once, that repetition compounds from one layer to the next. Two layers doubles the inlined text. Five layers is a different order of magnitude.</p>
<p>That&rsquo;s not theoretical: <a href="https://github.com/posit-dev/orbital/issues/115" target="_blank" rel="noopener">the issue that tracked the fix</a> measured scikit-learn&rsquo;s own default <code>MLPClassifier(hidden_layer_sizes=(100,))</code>, the first thing anyone reaches for, at 53MB of generated SQL and roughly 894 seconds just to generate it. A hundred neurons in one hidden layer, and the query was already unusable.</p>
<p>PyTorch&rsquo;s <code>Gemm</code> translator never had this problem. It has called <code>preserve()</code>, materializing its output as a real SQL column, since the day it was written. Scikit-learn&rsquo;s <code>MLPClassifier</code> and <code>MLPRegressor</code> compile through different ONNX ops though: <code>MatMul</code> then <code>Add</code>, because that&rsquo;s what <code>skl2onnx</code> emits, not <code>Gemm</code>. Neither of those translators called <code>preserve()</code> at all.</p>
<p>The fix, <code>Optimizer.preserve_referenced_outputs()</code>, runs after every single node in the translation loop, for every translator, not just <code>MatMul</code> and <code>Add</code>, and checks how many times that node&rsquo;s output is actually referenced downstream. Referenced more than once, it gets materialized as a named column. Referenced once or not at all, it stays inlined, no extra column, no extra noise.</p>
<p>None of this is new machinery either. Tree ensembles already lean on the same trick: <code>preserve()</code> materializes per-tree votes, or the whole ensemble&rsquo;s aggregated vote so it isn&rsquo;t re-emitted everywhere it&rsquo;s read, as real SQL columns. <code>preserve_referenced_outputs()</code> generalizes that same idea automatically, for every translator, whether it&rsquo;s part of an ordinary pipeline or a neural network.</p>
<p>Here&rsquo;s what that fix was worth, measured on three shapes while it was being built:</p>
<table>
  <thead>
      <tr>
          <th>Network</th>
          <th>Before</th>
          <th>After</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>20→64→64→1</code> (Orbital&rsquo;s own deep-network scaling test)</td>
          <td>12.4MB, ~185s to generate</td>
          <td>234KB</td>
      </tr>
      <tr>
          <td><code>MLPClassifier(hidden_layer_sizes=(100,))</code>, 3-class (scikit-learn&rsquo;s own default)</td>
          <td>53MB, ~894s to generate</td>
          <td>46.7KB</td>
      </tr>
      <tr>
          <td><code>MLPRegressor(hidden_layer_sizes=(32, 32))</code></td>
          <td>1.75MB, ~21s to generate</td>
          <td>57.6KB</td>
      </tr>
  </tbody>
</table>
<p>Generation time collapsed just as hard: the <code>20→64→64→1</code> network above went from about 185 seconds to about 3.6 seconds. Running the resulting SQL got faster too, if less dramatically: on 200,000 rows, an <code>MLP(32,32)</code> query dropped from 0.43s to 0.35s, and an <code>MLP(100,100)</code> from 3.58s to 3.31s.</p>
<p>Same hyperparameters. Same defaults everyone actually reaches for. The difference is entirely in how the SQL gets built, not in what the network computes.</p>
<h2 id="what-it-can-do-today">What it can do today
</h2>
<p>Neural network support in 0.6.0 covers binary classification, multiclass classification, and regression, for both scikit-learn and PyTorch. Five new or updated examples in the repo prove it out: <code>pytorch_fraud_detector.py</code>, <code>pytorch_maintenance_classifier.py</code>, <code>pytorch_demand_regressor.py</code>, <code>pipeline_mlp_classifier.py</code>, <code>pipeline_mlp_regressor.py</code>.</p>
<p>The one worth walking through is the fraud detector, since it&rsquo;s the shape most teams actually need: a handful of numeric features, a binary &ldquo;is this fraud&rdquo; output, trained the same way this kind of model always is.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">FEATURES</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;amount&#34;</span><span class="p">:</span> <span class="n">orbital</span><span class="o">.</span><span class="n">types</span><span class="o">.</span><span class="n">DoubleColumnType</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;hour&#34;</span><span class="p">:</span> <span class="n">orbital</span><span class="o">.</span><span class="n">types</span><span class="o">.</span><span class="n">DoubleColumnType</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;v1&#34;</span><span class="p">:</span> <span class="n">orbital</span><span class="o">.</span><span class="n">types</span><span class="o">.</span><span class="n">DoubleColumnType</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;v2&#34;</span><span class="p">:</span> <span class="n">orbital</span><span class="o">.</span><span class="n">types</span><span class="o">.</span><span class="n">DoubleColumnType</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">model</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">Sequential</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">Linear</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">FEATURES</span><span class="p">),</span> <span class="mi">16</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">ReLU</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">    <span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">Linear</span><span class="p">(</span><span class="mi">16</span><span class="p">,</span> <span class="mi">8</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">ReLU</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">    <span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">Linear</span><span class="p">(</span><span class="mi">8</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">Sigmoid</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ... train model normally: Adam, BCELoss, a plain training loop ...</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">orbital_pipeline</span> <span class="o">=</span> <span class="n">orbital</span><span class="o">.</span><span class="n">parse_pytorch_model</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">FEATURES</span><span class="p">)</span></span></span></code></pre></div></div>
<p>From that one <code>orbital_pipeline</code>, two engines:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">duckdb_sql</span> <span class="o">=</span> <span class="n">orbital</span><span class="o">.</span><span class="n">export_sql</span><span class="p">(</span><span class="s2">&#34;transactions&#34;</span><span class="p">,</span> <span class="n">orbital_pipeline</span><span class="p">,</span> <span class="n">dialect</span><span class="o">=</span><span class="s2">&#34;duckdb&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">postgres_sql</span> <span class="o">=</span> <span class="n">orbital</span><span class="o">.</span><span class="n">export_sql</span><span class="p">(</span><span class="s2">&#34;transactions&#34;</span><span class="p">,</span> <span class="n">orbital_pipeline</span><span class="p">,</span> <span class="n">dialect</span><span class="o">=</span><span class="s2">&#34;postgres&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>I ran both, against a real DuckDB and a real Postgres, not just read the generated text. Same four test transactions, three ways to compute a prediction (PyTorch itself, the DuckDB query, the Postgres query), and all three agree to within 1e-5. Both queries land around 10KB, not the tens of megabytes a naive translation would have produced before 0.6.0&rsquo;s optimizer fix.</p>
<p>That works because <code>Sigmoid</code> and <code>ReLU</code> both compile to plain arithmetic: <code>EXP</code>, a division, a <code>CASE WHEN</code>. Every SQL engine has those. It&rsquo;s not an accident which activation this example uses, either: <code>Tanh</code> compiles to a native <code>TANH()</code> call instead, and not every dialect implements that the same way, so it&rsquo;s the one activation in Orbital&rsquo;s NN support with an actual portability caveat attached.</p>
<p>DuckDB and Postgres are two of the three dialects <a href="https://posit-dev.github.io/orbital/learnmore/" target="_blank" rel="noopener">Orbital actively tests in CI</a>. SQLite is the third. But that&rsquo;s a testing choice, not an architecture boundary: Orbital doesn&rsquo;t write dialect-specific SQL at all. Translation ends at ibis. <code>export_sql</code> just hands the finished expression to whichever of ibis&rsquo;s own backend compilers matches the dialect you ask for, and ibis ships about twenty of those: Snowflake, BigQuery, Trino, MySQL, and so on.</p>
<h2 id="limits-honestly">Limits, honestly
</h2>
<p>What Orbital 0.6.0 handles is feed-forward, fully connected networks: stacks of <code>Linear</code> layers with <code>ReLU</code>, <code>Sigmoid</code>, <code>Tanh</code>, or <code>Softmax</code> in between. That already covers real use cases people put into production: fraud scoring, churn, demand forecasting, risk models. None of those need a CNN or a transformer.</p>
<p>What it doesn&rsquo;t do yet is exactly what that shape excludes: no convolutions, no recurrence, no attention, no embedding layers for categorical features. If your model needs any of those, Orbital isn&rsquo;t there yet.</p>
<p>SQL size still grows with the network. A few hidden layers of 64 to 128 neurons land comfortably in the KB range. Wider or deeper than that, it&rsquo;s worth checking the generated SQL against whatever statement-size limit your engine has, before you deploy it.</p>
<p>That headroom exists at all thanks to <code>Optimizer.preserve_referenced_outputs()</code>, and that mechanism helps every translator in Orbital, not just neural networks. It&rsquo;s a big enough story on its own that I&rsquo;ll get back to in a future post.</p>
<h2 id="try-it">Try it
</h2>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install orbital<span class="o">[</span>pytorch<span class="o">]</span></span></span></code></pre></div></div>
<p>From there, the <a href="https://posit-dev.github.io/orbital/getstarted/" target="_blank" rel="noopener">getting-started guide</a> walks through this same fraud-detector shape end to end, and the <a href="https://github.com/posit-dev/orbital/tree/main/examples" target="_blank" rel="noopener">examples directory</a> has five more, covering classification and regression in both frameworks.</p>
<p>Orbital speaks PyTorch now. Scikit-learn too. Same query either way.</p>
<p>There are more frameworks already in the works. I won&rsquo;t name them here, only that the architecture was built for exactly this.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-08-17_pyorbital-0-6-0/featured.png" length="1070756" type="image/png" />
    </item>
    <item>
      <title>A memory model for production async R: mirai 2.7.0 and mori 0.2.0</title>
      <link>https://opensource.posit.co/blog/2026-05-12_production-async-r/</link>
      <pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-05-12_production-async-r/</guid>
      <dc:creator>Charlie Gao</dc:creator><description><![CDATA[<p>Async R has always come with tradeoffs. Parallel batch compute meant duplicated data and an unbounded queue that could push your session out of memory, while Shiny gave you an event loop but background-worker submissions had no way to tell you when to slow down.</p>
<p>Shared memory across processes is standard in other programming languages. In R it&rsquo;s been either platform-specific (<code>fork</code> on Unix-alikes, unsafe from GUIs like Positron or with multi-threaded code) or file-format-specific (memory-mapped Parquet).</p>
<p>Two CRAN releases address those constraints directly:</p>
<ul>
<li><strong><a href="https://mirai.r-lib.org/" target="_blank" rel="noopener">mirai 2.7.0</a></strong> — <code>daemons(memory = ...)</code> puts a hard byte-level cap on the dispatcher queue. <code>try_mirai()</code> returns <code>NULL</code> immediately when that cap is hit, instead of blocking your session. The dispatcher itself runs as an in-process thread now; and through <a href="https://nanonext.r-lib.org/" target="_blank" rel="noopener">nanonext 1.9.0</a> underneath, peak memory during serialized sends has halved.</li>
<li><strong><a href="https://shikokuchuo.net/mori/" target="_blank" rel="noopener">mori 0.2.0</a></strong> — graduates from experimental status, with a stable wire format underneath. Sub-lists and extracted elements now expose stable path-form names (<code>/mori_abc[2,3]</code>) that any process can attach to via <code>map_shared()</code> directly, without going through the parent.</li>
</ul>
<h2 id="building-out-the-foundations">Building out the foundations
</h2>
<p>mirai already shipped a lot of what production async needs: microsecond round-trip latency, FIFO scheduling, task cancellation, OpenTelemetry tracing, reproducible parallel RNG, custom serialization for torch / Arrow / polars, an event-driven promises method for Shiny ExtendedTask, and deployment across SSH / Slurm / Posit Workbench.</p>
<p>What these releases add is more foundational. R hasn&rsquo;t really had a built-in memory model for asynchronous work — no way to size a task queue in bytes, and no portable way to share data across processes without paying for a full copy. mirai 2.7.0 introduces the first: a bounded queue built into the dispatcher, with <code>try_mirai()</code> exposing non-blocking submission under this constraint. mori 0.2.0 brings the second to a stable footing: a self-describing shared-memory format, addressable down to individual list elements by name, that any process on the machine can attach to.</p>
<p>Together they extend what R can credibly be used for — streaming pipelines, multi-stage data flows, and event-loop applications that previously had to reach outside the language.</p>
<h2 id="mirai-270--bounded-queues-and-try_mirai">mirai 2.7.0 — bounded queues and <code>try_mirai()</code>
</h2>
<h3 id="daemonsmemory--"><code>daemons(memory = ...)</code>
</h3>
<p>The dispatcher is the single point in your session where every <code>mirai()</code> call lands before being routed to a daemon. Each call carries its serialized arguments — the queued payload — and that payload sits in memory until a daemon is ready for it. By default the queue is unbounded, which is fine until it isn&rsquo;t.</p>
<p><code>daemons()</code> now takes a <code>memory</code> argument that caps the approximate total payload of queued tasks at the dispatcher, in MB:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">mirai</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">daemons</span><span class="p">(</span><span class="m">4</span><span class="p">,</span> <span class="n">memory</span> <span class="o">=</span> <span class="m">100</span><span class="p">)</span>  <span class="c1"># 100 MB queue capacity</span></span></span></code></pre></div></div>
<p>When the queued bytes are below the cap, <code>mirai()</code> calls submit normally. When the cap is reached, <code>mirai()</code> blocks the calling R thread until daemons drain the queue enough for the new payload to fit. Existing code keeps working — the only visible difference is that your session can no longer be pushed out of memory by a runaway producer.</p>
<p><code>status()</code> surfaces current and peak usage:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">status</span><span class="p">()</span><span class="o">$</span><span class="n">memory</span>
</span></span><span class="line"><span class="cl"><span class="c1">#     used     peak capacity </span>
</span></span><span class="line"><span class="cl"><span class="c1">#     12.4     87.3    100.0 </span></span></span></code></pre></div></div>
<p><code>peak</code> is the high-watermark queued payload over the lifetime of the dispatcher, which gives you an empirical way to size the cap. The recommended workflow is to run a representative workload with <code>memory = NULL</code> first (where <code>capacity</code> reports <code>NA</code>), observe <code>peak</code>, then set <code>memory</code> at or above that watermark.</p>
<p>If profiling isn&rsquo;t practical, the local machine has to fit your session, the dispatcher queue, and <code>n</code> daemon processes (each a full R session holding an in-flight payload while executing). Half of currently available RAM (<code>ps::ps_system_memory()[[&quot;avail&quot;]] / 2e6</code>, in MB) is a reasonable starting <code>memory</code> cap, revised down for large <code>n</code> or payloads. Remote daemons don&rsquo;t draw on the local budget, so when most workers are remote, the cap can be more generous.</p>
<p>For <code>n</code> itself, one less than your number of CPU cores is the right starting point for CPU-bound work (one core left for your session), while I/O-bound work (API calls, database queries, file reads) can comfortably exceed core count since idle daemons cost little.</p>
<p>The capacity is measured in <em>bytes</em>, not <em>task count</em>. A queue capped at 100 tasks can hold either 100 small tasks or 100 huge ones, and only the second runs out of memory. A queue capped at 100 MB never runs your session out of memory regardless of the mix.</p>
<h3 id="try_mirai"><code>try_mirai()</code>
</h3>
<p>A blocking submit is fine in a batch script, but not an event loop: a Shiny session that calls <code>mirai()</code> from inside an ExtendedTask can&rsquo;t afford to block while the queue drains, because that same session is also driving the UI for all users.</p>
<p><code>try_mirai()</code> returns immediately when the queue is full, instead of blocking. The semantics are simple:</p>
<ul>
<li>If the queue is below capacity, behaves identically to <code>mirai()</code> and returns a mirai.</li>
<li>If the queue is at capacity, returns <code>NULL</code> immediately without blocking, and leaves the caller to decide what to do next.</li>
</ul>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">m</span> <span class="o">&lt;-</span> <span class="nf">try_mirai</span><span class="p">({</span> <span class="nf">slow_thing</span><span class="p">()</span> <span class="p">})</span> <span class="o">%||%</span> <span class="nf">stop</span><span class="p">(</span><span class="s">&#34;queue full&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>The three response strategies are: drop the task (best when the work is idempotent and frequent), retry later (queue the request behind a <code>later::later()</code> call), or propagate backpressure upstream — as in the example above, by raising a condition. Which is right is application-specific — <code>try_mirai()</code> hands you the flexibility.</p>
<p>If <code>memory</code> isn&rsquo;t set, or the daemon configuration has no dispatcher, <code>try_mirai()</code> always returns a mirai and is identical to <code>mirai()</code> — so the function is safe to use unconditionally; it only does anything different in the bounded-queue case.</p>
<p>The two halves — <code>daemons(memory = ...)</code> and <code>try_mirai()</code> — are the right combination for any event-loop integration. Set the cap to reflect what your session can actually afford to hold, submit through <code>try_mirai()</code>, and the application gracefully adapts to load instead of locking up or crashing.</p>
<h3 id="under-the-hood">Under the hood
</h3>
<p>A few transport-layer changes ship alongside 2.7.0 — invisible most of the time, important when payloads are large or throughput is high:</p>
<ul>
<li><strong>Dispatcher as a thread.</strong> The dispatcher is no longer a separate R process; it runs as an in-process thread, courtesy of a C-level reimplementation in nanonext. mirai round-trip latency is now in the tens of microseconds.</li>
<li><strong>Halved peak memory on sends.</strong> <a href="https://nanonext.r-lib.org/" target="_blank" rel="noopener">nanonext 1.9.0</a>, the transport underneath mirai, now writes serialized sends directly instead of going through an intermediate buffer. A 500 MB argument going to a daemon used to need ~1 GB of momentary headroom in your session; now it needs ~500 MB. The dispatcher&rsquo;s actual resident memory tracks the <code>daemons(memory = ...)</code> cap more accurately as a result.</li>
<li><strong>&gt;2 GB payloads on macOS and Windows.</strong> Transfers above 2 GB previously failed silently near the <code>INT_MAX</code>-byte boundary on those platforms. They now go through cleanly. Linux was already fine.</li>
</ul>
<h2 id="mori-020--out-of-experimental-status">mori 0.2.0 — out of experimental status
</h2>
<p>mori is a younger package — its <a href="https://opensource.posit.co/blog/2026-04-23_mori-0-1-0/">first CRAN release</a> was just last month — and its job is the other half of memory pressure: the data itself, rather than the queue. R processes don&rsquo;t share memory, and data crosses between them through message-passing. When eight workers each need the same 200 MB data frame, that&rsquo;s 1.6 GB of serialize / transfer / deserialize, plus eight separate copies of the data resident in RAM.</p>
<p><code>mori::share()</code> writes the object once into OS-level shared memory and returns an ALTREP (alternative representation) wrapper. Multiple processes on the same machine map the same physical pages — zero-copy, lazy, and managed by R&rsquo;s garbage collector. Pass the wrapper to a daemon and only the shared memory identifier (hundreds of bytes) crosses the wire; the daemon attaches and reads the pages directly.</p>
<p>We implemented this in mori 0.1.0, but 0.2.0 takes mori out of experimental status, landing a stable wire format. A key consequence of this is that every sub-list and extracted element of a shared region now has a stable identifier of its own. <code>shared_name()</code> emits path-form names (<code>/mori_abc_1[2]</code> for the second element of region <code>/mori_abc_1</code>, <code>/mori_abc_1[2,3]</code> for nested addressing), and <code>map_shared()</code> accepts the path form directly.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">mori</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">mirai</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">lst</span> <span class="o">&lt;-</span> <span class="nf">share</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="nf">list</span><span class="p">(</span><span class="n">weights</span> <span class="o">=</span> <span class="nf">rnorm</span><span class="p">(</span><span class="m">1e6</span><span class="p">),</span> <span class="n">targets</span> <span class="o">=</span> <span class="nf">rnorm</span><span class="p">(</span><span class="m">1e6</span><span class="p">),</span> <span class="n">inputs</span> <span class="o">=</span> <span class="nf">rnorm</span><span class="p">(</span><span class="m">1e6</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Sub-elements have their own path-form names</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">shared_name</span><span class="p">(</span><span class="n">lst</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; [1] &#34;/mori_abc_1&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nf">shared_name</span><span class="p">(</span><span class="n">lst[[1]]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; [1] &#34;/mori_abc_1[1]&#34;</span></span></span></code></pre></div></div>
<p>Pass the shared object directly to a daemon — the ALTREP wrapper serializes as just the name, and the daemon attaches on deserialize:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">mirai</span><span class="p">(</span><span class="nf">length</span><span class="p">(</span><span class="n">weights</span><span class="p">),</span> <span class="n">weights</span> <span class="o">=</span> <span class="n">lst[[1]]</span><span class="p">)</span><span class="n">[]</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; [1] 1000000</span></span></span></code></pre></div></div>
<p>A consumer in a separate session that only has the name as a string — from a config file, message queue, or HTTP response — attaches with <code>map_shared()</code>:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># In a separate R session</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">weights</span> <span class="o">&lt;-</span> <span class="n">mori</span><span class="o">::</span><span class="nf">map_shared</span><span class="p">(</span><span class="s">&#34;/mori_abc_1[1]&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">length</span><span class="p">(</span><span class="n">weights</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; [1] 1000000</span></span></span></code></pre></div></div>
<p>A path-form name is a small thing on the wire – just a string – but it enables a lot:</p>
<ul>
<li><strong>Attach by name.</strong> Path-form identifiers are plain strings, so they fit anywhere a string can go: a queue payload, a config entry, an HTTP response, a database row. A consumer reads the name, calls <code>map_shared()</code>, and gets exactly that slice — the parent region never has to enter their session.</li>
<li><strong>Decoupled pipelines.</strong> Stage A shares a 100-column frame and tells stage B the names of the three columns to process. Stage B passes the name of its output to stage C, and so on. No stage holds more in memory than it actually uses, and no stage needs to know how its inputs were produced — only the names.</li>
<li><strong>Code symmetry.</strong> <code>map_shared(shared_name(x))</code> returns an equivalent of <code>x</code> whether <code>x</code> is a root region or a single leaf element, so consumer code doesn&rsquo;t need a special branch for sub-objects.</li>
<li><strong>Lifetime through one chain.</strong> Holding a reference to a leaf keeps the whole parent region alive automatically — the garbage collector walks the chain of dependencies — so a consumer never has to manually pin the root to stay safe.</li>
</ul>
<h3 id="composing-with-daemonsmemory--">Composing with <code>daemons(memory = ...)</code>
</h3>
<img src="https://opensource.posit.co/blog/2026-05-12_production-async-r/composition-diagram.svg" data-fig-alt="Diagram: the single call try_mirai({ f(y) }, y = share(x)) forks into two effects — submission to a bounded dispatcher queue (memory = 100 MB) and writing the data once into a shared-memory region (/mori_abc_1). Both feed a single daemon: the queue carries only the name, and the daemon attaches to shared memory by name via mmap." />
<p>The two memory tools compose. <code>mori::share()</code> shrinks each task&rsquo;s <em>queued payload</em> from megabytes to hundreds of bytes, which means the dispatcher&rsquo;s <code>memory</code> cap rarely fills up at all when workers are reading shared data. The pieces are complementary:</p>
<ul>
<li><code>mori::share()</code> removes the per-worker copy of large data.</li>
<li><code>daemons(memory = ...)</code> caps the dispatcher backlog when something <em>is</em> being sent.</li>
<li><code>try_mirai()</code> makes hitting that cap a non-event in an event loop.</li>
</ul>
<h2 id="putting-r-on-the-concurrency-map">Putting R on the concurrency map
</h2>
<p>The design of <code>daemons(memory = ...)</code>, <code>try_mirai()</code>, and <code>mori::share()</code> draws on how async runtimes in other languages handle the same problems. Here&rsquo;s the comparison:</p>
<p><strong>Byte-budgeted backpressure.</strong> Async frameworks elsewhere — Python&rsquo;s <code>asyncio.Queue</code>, Ray&rsquo;s <code>max_pending_tasks</code>, Tokio&rsquo;s bounded <code>mpsc</code> channels — measure queue capacity by task count. <code>daemons(memory = ...)</code> caps by <em>bytes</em> instead, which tracks what actually drives memory pressure.</p>
<p><strong>Non-blocking submission on a full queue.</strong> Tokio&rsquo;s <code>try_send</code> is the closest direct analogue: it returns immediately when the channel is full instead of blocking. <code>try_mirai()</code> provides the same semantics for R submission, with the additional advantage that the cap itself is byte-aware.</p>
<p><strong>Integrated cross-process zero-copy.</strong> Ray&rsquo;s Plasma object store offers cross-process zero-copy, but it&rsquo;s a heavyweight component — you run a Ray cluster to use it, not a function. <code>mori::share()</code> is just a function call, producing an ALTREP wrapper that consumer processes attach to via lazy reads, down to individual columns by name.</p>
<p>Combined with mirai&rsquo;s microsecond round-trip latency and event-driven C-level transport, these three pieces give R a concurrency story that stands on its own: byte-aware backpressure, non-blocking submission, and integrated zero-copy sharing.</p>
<h2 id="try-it">Try it
</h2>
<p>Install from CRAN:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="nf">c</span><span class="p">(</span><span class="s">&#34;mirai&#34;</span><span class="p">,</span> <span class="s">&#34;mori&#34;</span><span class="p">))</span></span></span></code></pre></div></div>
<p>Pointers from here:</p>
<ul>
<li><a href="https://mirai.r-lib.org/articles/v01-reference.html" target="_blank" rel="noopener">mirai reference vignette</a> — the memory-management section walks through <code>daemons(memory = ...)</code>, <code>status()$memory</code>, <code>try_mirai()</code>, and shared-memory composition.</li>
<li><a href="https://mirai.r-lib.org/articles/v02-promises.html" target="_blank" rel="noopener">mirai promises vignette</a> — Shiny ExtendedTask and event-driven async.</li>
<li><a href="https://shikokuchuo.net/mori/" target="_blank" rel="noopener">mori package site</a> — <code>share()</code>, <code>shared_name()</code> and <code>map_shared()</code>.</li>
<li><a href="https://nanonext.r-lib.org/" target="_blank" rel="noopener">nanonext package site</a> — the transport layer and concurrency primitives.</li>
<li>The <code>r-lib</code> agent skill in the <a href="https://github.com/posit-dev/skills" target="_blank" rel="noopener"><code>posit-dev-skills</code></a> plugin includes an LLM-optimised mirai guide for AI coding assistants.</li>
</ul>
<p>Issues, feedback, and questions are very welcome on GitHub: <a href="https://github.com/r-lib/mirai/issues" target="_blank" rel="noopener">r-lib/mirai</a>, <a href="https://github.com/r-lib/nanonext/issues" target="_blank" rel="noopener">r-lib/nanonext</a>, <a href="https://github.com/shikokuchuo/mori/issues" target="_blank" rel="noopener">shikokuchuo/mori</a>.</p>
<h2 id="acknowledgments">Acknowledgments
</h2>
<p>A big thanks to everyone who contributed to mirai 2.7.0, mori 0.2.0, and nanonext 1.9.0 through their issues, pull requests, and discussions: <a href="https://github.com/HenrikBengtsson" target="_blank" rel="noopener">@HenrikBengtsson</a>, <a href="https://github.com/kentqin-cve" target="_blank" rel="noopener">@kentqin-cve</a>, <a href="https://github.com/king-of-poppk" target="_blank" rel="noopener">@king-of-poppk</a>, <a href="https://github.com/manforkr" target="_blank" rel="noopener">@manforkr</a>, <a href="https://github.com/mcol" target="_blank" rel="noopener">@mcol</a>, <a href="https://github.com/michaelmayer2" target="_blank" rel="noopener">@michaelmayer2</a>, <a href="https://github.com/pmac0451" target="_blank" rel="noopener">@pmac0451</a>, <a href="https://github.com/t-kalinowski" target="_blank" rel="noopener">@t-kalinowski</a>, and <a href="https://github.com/wlandau" target="_blank" rel="noopener">@wlandau</a>.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-05-12_production-async-r/featured.jpg" length="261721" type="image/jpeg" />
    </item>
    <item>
      <title>Bringing OpenTelemetry to R in production</title>
      <link>https://opensource.posit.co/blog/2026-05-07_opentelemetry/</link>
      <pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-05-07_opentelemetry/</guid>
      <dc:creator>Charlie Gao</dc:creator>
      <dc:creator>Aaron Jacobs</dc:creator>
      <dc:creator>Barret Schloerke</dc:creator>
      <dc:creator>Gábor Csárdi</dc:creator><description><![CDATA[<p>We&rsquo;re bringing <a href="https://opentelemetry.io/" target="_blank" rel="noopener">OpenTelemetry</a> to R. As a Posit-wide initiative across our open source packages, we&rsquo;ve instrumented some of the most widely-used R packages for production workloads &ndash; <a href="https://shiny.posit.co/" target="_blank" rel="noopener">Shiny</a>, <a href="https://plumber2.posit.co/" target="_blank" rel="noopener">plumber2</a>, <a href="https://mirai.r-lib.org" target="_blank" rel="noopener">mirai</a>, <a href="https://httr2.r-lib.org" target="_blank" rel="noopener">httr2</a>, <a href="https://ellmer.tidyverse.org" target="_blank" rel="noopener">ellmer</a>, <a href="https://pkg.yihui.org/knitr/" target="_blank" rel="noopener">knitr</a>, <a href="https://testthat.r-lib.org" target="_blank" rel="noopener">testthat</a> and <a href="https://dbi.r-dbi.org" target="_blank" rel="noopener">DBI</a>. You can add observability to your R applications with <strong>no code changes</strong>: set a few environment variables and you get traces, logs, and metrics flowing to the backend of your choice.</p>
<p>This is part of our commitment to R in production. As R applications scale &ndash; more users, more processes, more machines &ndash; you need tools to understand what&rsquo;s happening across your entire system. That&rsquo;s what OpenTelemetry is built for, and it&rsquo;s now available for R.</p>
<h2 id="what-is-opentelemetry">What is OpenTelemetry?
</h2>
<p><a href="https://opentelemetry.io/" target="_blank" rel="noopener">OpenTelemetry</a> (OTel) is a vendor-neutral, open source observability framework backed by the <a href="https://www.cncf.io/" target="_blank" rel="noopener">Cloud Native Computing Foundation</a>.
It has broad industry support across languages and platforms, and is already the standard in the Python, Java, JavaScript, and Go ecosystems. Now it&rsquo;s available for R.</p>
<p>OpenTelemetry defines a standard for collecting telemetry data:</p>
<ul>
<li><strong>Traces</strong> follow an operation as it moves through your system, showing exactly which functions ran, in what order, and how long each took.</li>
<li><strong>Metrics</strong> capture numerical measurements over time &ndash; things like request counts, response latencies, or memory usage.</li>
<li><strong>Logs</strong> record detailed events as they happen, providing context when you need to investigate a specific moment.</li>
</ul>
<p>The instrumented packages described in this post all use the <a href="https://otel.r-lib.org" target="_blank" rel="noopener">otel</a> package under the hood, and focus on traces, which provide the most immediate value for understanding production behavior. You can also use otel directly to add your own metrics and logs to your application code.</p>
<h2 id="why-observability-matters-for-r-in-production">Why observability matters for R in production
</h2>
<p>When you&rsquo;re developing interactively in RStudio or Positron, debugging is straightforward &ndash; you can step through code, inspect objects, and add print statements. But when your R code runs in production &ndash; a Shiny app serving hundreds of users, a plumber2 API handling thousands of requests, a batch pipeline running across a cluster &ndash; the picture changes.</p>
<p>The core concept in OpenTelemetry is a <strong>trace</strong>: the full path of a request through your system. Each trace is made up of <strong>spans</strong> &ndash; individual units of work with a name and a duration. Spans nest inside each other, so you can follow how each operation triggered the next:</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-05-07_opentelemetry/otel-layers.svg"
      alt="A trace is made up of spans, each representing a unit of work. Nested spans show how operations cascade through your system." 
      loading="lazy"
    >
  </figure></div>
</p>
<p>This structure gives you four things that are hard to get any other way:</p>
<ul>
<li><strong>Performance</strong>: Which part of a request is slow? Span durations pinpoint where time is spent &ndash; and nesting reveals unnecessary overhead.</li>
<li><strong>Errors</strong>: In development and testing, you know where errors are &ndash; you wrote the test, you control the inputs. In production, errors surface far from their root cause, across process boundaries and async operations, triggered by conditions you never anticipated. Traces show you the full chain of real operations that led to each failure, in the context where it actually happened.</li>
<li><strong>Centralized view</strong>: When your application extends across multiple R processes or machines &ndash; a Shiny app with mirai workers, or a plumber2 API behind a load balancer &ndash; traces are aggregated into a single view across all of them.</li>
<li><strong>Real-time monitoring</strong>: OTel is designed to be left on in production, not just enabled during testing or staging. With low overhead, it runs continuously &ndash; so you see what&rsquo;s happening as it happens, and dashboards and alerts catch problems before users report them.</li>
</ul>
<h2 id="instrumented-packages">Instrumented packages
</h2>
<p>We&rsquo;ve worked across teams to add OpenTelemetry instrumentation to the R packages where it matters most:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Package</th>
          <th style="text-align: left">Version</th>
          <th style="text-align: left">What it traces</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><a href="https://shiny.posit.co/" target="_blank" rel="noopener">Shiny</a></td>
          <td style="text-align: left">≥ 1.12.0</td>
          <td style="text-align: left">Session lifecycle, reactive updates, reactive expressions, background tasks</td>
      </tr>
      <tr>
          <td style="text-align: left"><a href="https://plumber2.posit.co/" target="_blank" rel="noopener">plumber2</a></td>
          <td style="text-align: left">≥ 0.2.0</td>
          <td style="text-align: left">API request handling, routing, endpoint execution</td>
      </tr>
      <tr>
          <td style="text-align: left"><a href="https://mirai.r-lib.org" target="_blank" rel="noopener">mirai</a></td>
          <td style="text-align: left">≥ 2.5.0</td>
          <td style="text-align: left">Task dispatch, daemon execution, results</td>
      </tr>
      <tr>
          <td style="text-align: left"><a href="https://httr2.r-lib.org" target="_blank" rel="noopener">httr2</a></td>
          <td style="text-align: left">≥ 1.2.2</td>
          <td style="text-align: left">HTTP requests and responses</td>
      </tr>
      <tr>
          <td style="text-align: left"><a href="https://ellmer.tidyverse.org" target="_blank" rel="noopener">ellmer</a></td>
          <td style="text-align: left">≥ 0.4.1</td>
          <td style="text-align: left">LLM API calls, tool execution, token usage</td>
      </tr>
      <tr>
          <td style="text-align: left"><a href="https://pkg.yihui.org/knitr/" target="_blank" rel="noopener">knitr</a></td>
          <td style="text-align: left">≥ 1.51</td>
          <td style="text-align: left">Document rendering, chunk evaluation</td>
      </tr>
      <tr>
          <td style="text-align: left"><a href="https://testthat.r-lib.org" target="_blank" rel="noopener">testthat</a></td>
          <td style="text-align: left">≥ 3.3.2</td>
          <td style="text-align: left">Test execution</td>
      </tr>
      <tr>
          <td style="text-align: left"><a href="https://dbi.r-dbi.org" target="_blank" rel="noopener">DBI</a></td>
          <td style="text-align: left">≥ 1.3.0</td>
          <td style="text-align: left">Database queries and connections</td>
      </tr>
  </tbody>
</table>
<p>Together, these packages cover the most common production R workloads: web applications, APIs, parallel computing, HTTP clients, AI/LLM tools, report rendering pipelines, CI test runs, and database access. Because the instrumentation is built into the packages themselves, you benefit from it automatically &ndash; no wrapper functions, nor modifications to existing code. If you already have a Shiny app or plumber2 API, your app will generate traces as soon as you enable OpenTelemetry. Your application code stays exactly as it is.</p>
<h2 id="seeing-it-in-action">Seeing it in action
</h2>
<p>To make this concrete, let&rsquo;s look at a Shiny chat app built with <a href="https://posit-dev.github.io/shinychat/" target="_blank" rel="noopener">shinychat</a> and <a href="https://ellmer.tidyverse.org" target="_blank" rel="noopener">ellmer</a> that fetches weather forecasts. It uses mirai for async execution and httr2 for weather API requests.</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-05-07_opentelemetry/chat-demo.png"
      alt="The weather chat app uses Shiny, shinychat, ellmer, mirai, and httr2 &ndash; all instrumented with OpenTelemetry." 
      loading="lazy"
    >
  </figure></div>
</p>
<p>A user asks about the weather in Atlanta and Newcastle:</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-05-07_opentelemetry/chat-screenshot.png"
      alt="The running chat application, showing a successful weather query for Atlanta and a failed one for Newcastle." 
      loading="lazy"
    >
  </figure></div>
</p>
<p>With OpenTelemetry enabled, every step is captured automatically. Here are the traces from those two queries:</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-05-07_opentelemetry/otel-traces.png"
      alt="Traces from the weather chat app, showing the full chain of operations from Shiny session through to HTTP requests. The second request shows an error (red) that&rsquo;s immediately visible in the trace." 
      loading="lazy"
    >
  </figure></div>
</p>
<p>The trace reveals the full chain of operations from user input through to the HTTP request, across process boundaries, with no manual logging. The nesting shows how each step triggered the next, and the durations show where time was spent.</p>
<p>The second query failed. Without tracing, you&rsquo;d see an error in your logs and start investigating. Here, the failure is immediately visible &ndash; the red span pinpoints where it occurred and the surrounding context shows why. In a production system with many concurrent users, that&rsquo;s the difference between minutes and seconds of debugging.</p>
<h2 id="getting-started">Getting started
</h2>
<p>Getting started requires the otelsdk package and a few environment variables. No changes to your application code.</p>
<p>Here&rsquo;s how the pieces fit together: the instrumented R packages generate telemetry data as they run. The <a href="https://otelsdk.r-lib.org" target="_blank" rel="noopener">otelsdk</a> package collects and exports this data over HTTP to a <strong>backend</strong> &ndash; a service that stores your traces and provides a web dashboard where you can search, filter, and visualize them (like the trace screenshots above).</p>
<h3 id="step-1-install-otelsdk">Step 1: Install otelsdk
</h3>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;otelsdk&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<h3 id="step-2-choose-a-backend">Step 2: Choose a backend
</h3>
<p>OpenTelemetry is vendor-neutral, so you can send your data to any compatible backend:</p>
<ul>
<li><strong>Cloud services</strong>: <a href="https://logfire.pydantic.dev/" target="_blank" rel="noopener">Logfire</a>, <a href="https://grafana.com/products/cloud/" target="_blank" rel="noopener">Grafana Cloud</a>, <a href="https://langfuse.com/" target="_blank" rel="noopener">Langfuse</a></li>
<li><strong>Self-hosted</strong>: <a href="https://www.jaegertracing.io/" target="_blank" rel="noopener">Jaeger</a>, <a href="https://zipkin.io/" target="_blank" rel="noopener">Zipkin</a>, <a href="https://prometheus.io/" target="_blank" rel="noopener">Prometheus</a></li>
</ul>
<p>During development, you can also run an <a href="https://opentelemetry.io/docs/collector/" target="_blank" rel="noopener">OpenTelemetry Collector</a> on your own machine and inspect traces directly, without sending data anywhere else.</p>
<p>Each backend will give you an endpoint URL and an authentication token. The <a href="https://otelsdk.r-lib.org/reference/collecting.html#setup" target="_blank" rel="noopener">otelsdk collecting telemetry data guide</a> has examples for some common backends.</p>
<h3 id="step-3-set-environment-variables">Step 3: Set environment variables
</h3>
<p>Add these to your <code>.Renviron</code> file (use <code>usethis::edit_r_environ()</code> to open it), replacing the endpoint and token with the values from your chosen backend. This example uses <a href="https://logfire.pydantic.dev/" target="_blank" rel="noopener">Logfire</a>, which offers a free tier to get started:</p>
<div class="code-block"><pre tabindex="0"><code>OTEL_TRACES_EXPORTER=&#34;http&#34;
OTEL_EXPORTER_OTLP_ENDPOINT=&#34;https://logfire-eu.pydantic.dev&#34;
OTEL_EXPORTER_OTLP_HEADERS=&#34;Authorization=&lt;YOUR-WRITE-TOKEN&gt;&#34;</code></pre></div>
<p>If you&rsquo;re deploying content to Posit Connect, refer to <a href="https://docs.posit.co/connect/user/content-settings/#setting-env-vars" target="_blank" rel="noopener">how to set environment variables on Posit Connect</a>.</p>
<h3 id="step-4-run-your-app">Step 4: Run your app
</h3>
<p>That&rsquo;s it. Restart R, then run your Shiny app, plumber2 API, or any code that uses the instrumented packages. Traces will flow to your backend automatically. Open your backend&rsquo;s web dashboard to see them &ndash; you&rsquo;ll see a view like the trace screenshots shown above, with each span representing an operation in your application.</p>
<p>You can verify that tracing is active at any time:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">otel</span><span class="o">::</span><span class="nf">is_tracing_enabled</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; [1] TRUE</span></span></span></code></pre></div></div>
<p>OpenTelemetry is designed to be safe in production. If anything goes wrong in the telemetry code itself, it will never crash your application &ndash; errors are silently suppressed so your app keeps running.</p>
<h2 id="zero-code-instrumentation">Zero-code instrumentation
</h2>
<p>Beyond the packages that ship with built-in instrumentation, otel supports <strong>zero-code instrumentation</strong> for any R package. Set the <code>OTEL_R_INSTRUMENT_PKGS</code> environment variable to a comma-separated list of package names, and otel will automatically create spans for their exported functions:</p>
<div class="code-block"><pre tabindex="0"><code>OTEL_R_INSTRUMENT_PKGS=dplyr,tidyr</code></pre></div>
<p>You can also fine-tune which functions are instrumented using include and exclude filters:</p>
<div class="code-block"><pre tabindex="0"><code>OTEL_R_INSTRUMENT_PKGS_DPLYR_INCLUDE=mutate,filter,select</code></pre></div>
<p>This is useful for adding visibility to any package in your stack, even those without built-in OTel support. See the <a href="https://otel.r-lib.org/reference/zci.html" target="_blank" rel="noopener">otel documentation</a> for full details.</p>
<h2 id="configuration-options">Configuration options
</h2>
<p>The otelsdk package is configured entirely through environment variables, following OpenTelemetry conventions:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Variable</th>
          <th style="text-align: left">Purpose</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><code>OTEL_TRACES_EXPORTER</code></td>
          <td style="text-align: left">Exporter type for traces (e.g. <code>&quot;http&quot;</code>)</td>
      </tr>
      <tr>
          <td style="text-align: left"><code>OTEL_LOGS_EXPORTER</code></td>
          <td style="text-align: left">Exporter type for logs</td>
      </tr>
      <tr>
          <td style="text-align: left"><code>OTEL_METRICS_EXPORTER</code></td>
          <td style="text-align: left">Exporter type for metrics</td>
      </tr>
      <tr>
          <td style="text-align: left"><code>OTEL_EXPORTER_OTLP_ENDPOINT</code></td>
          <td style="text-align: left">URL of the OTLP-compatible backend</td>
      </tr>
      <tr>
          <td style="text-align: left"><code>OTEL_EXPORTER_OTLP_HEADERS</code></td>
          <td style="text-align: left">Authentication headers for the backend</td>
      </tr>
      <tr>
          <td style="text-align: left"><code>OTEL_R_INSTRUMENT_PKGS</code></td>
          <td style="text-align: left">Packages for zero-code instrumentation</td>
      </tr>
      <tr>
          <td style="text-align: left"><code>OTEL_R_EMIT_SCOPES</code></td>
          <td style="text-align: left">Restrict telemetry to specific packages</td>
      </tr>
      <tr>
          <td style="text-align: left"><code>OTEL_R_SUPPRESS_SCOPES</code></td>
          <td style="text-align: left">Exclude specific packages from telemetry</td>
      </tr>
  </tbody>
</table>
<p>See the <a href="https://otelsdk.r-lib.org/reference/environmentvariables.html" target="_blank" rel="noopener">otelsdk environment variables reference</a> for the complete list.</p>
<h2 id="looking-ahead">Looking ahead
</h2>
<p>With instrumentation built into the packages that R users already rely on, observability becomes something you can turn on, not something you have to build.</p>
<p>We&rsquo;re also integrating an OpenTelemetry collector into <a href="https://posit.co/products/enterprise/connect/" target="_blank" rel="noopener">Posit Connect</a>, giving you an end-to-end observability solution you can simply turn on or off.</p>
<p>OTel support across the ecosystem continues to expand. If you&rsquo;d like to learn more:</p>
<ul>
<li><a href="https://otel.r-lib.org" target="_blank" rel="noopener">otel package documentation</a> &ndash; the instrumentation API</li>
<li><a href="https://otelsdk.r-lib.org" target="_blank" rel="noopener">otelsdk package documentation</a> &ndash; the SDK for collecting and exporting telemetry</li>
<li><a href="https://opensource.posit.co/blog/2025-12-10_shiny-r-1.12/">Shiny 1.12 OTel blog post</a> &ndash; deep dive into Shiny&rsquo;s OpenTelemetry support</li>
<li><a href="https://opentelemetry.io/" target="_blank" rel="noopener">OpenTelemetry project</a> &ndash; the upstream standard</li>
</ul>
<p>We&rsquo;re excited about what this opens up for the R community. Whether you&rsquo;re running a Shiny dashboard for a small team, a plumber2 API serving thousands of requests, or a data pipeline distributed across a cluster &ndash; you now have the tools to see what&rsquo;s happening, in real time, with no code changes required.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-05-07_opentelemetry/featured.jpg" length="489929" type="image/jpeg" />
    </item>
    <item>
      <title>Positron Server available for academic use via JupyterHub</title>
      <link>https://opensource.posit.co/blog/2026-04-06_positron-server-jupyterhub/</link>
      <pubDate>Mon, 06 Apr 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-04-06_positron-server-jupyterhub/</guid>
      <dc:creator>Isabel Zimmerman</dc:creator><description><![CDATA[<p>Academic institutions can now offer Positron directly within their existing JupyterHub environments, giving students a robust data science IDE without needing a local install or new infrastructure.
With a free teaching license, institutions can provide Positron Server to currently enrolled students for use in coursework.
This makes it easy to deliver a consistent, fully featured data science environment to students without requiring local installation or setup.</p>
<p>Students can launch Positron the same way they would open JupyterLab or a notebook.
Just select it from the JupyterHub launcher and start working.</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-04-06_positron-server-jupyterhub/images/jupyter-positron.gif"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
<p>Once launched, Positron provides the full IDE experience, including:</p>
<ul>
<li>Rich Python and R support</li>
<li>Access to the OpenVSX extension marketplace</li>
<li>Built in data viewer and variables explorer</li>
<li>Integrated help pane, debugger, version control and <a href="https://positron.posit.co/features.html" target="_blank" rel="noopener">other features</a> to help students level up when they&rsquo;re ready</li>
</ul>
<h2 id="how-it-works">How it works
</h2>
<p>Positron Server is designed to integrate directly with existing JupyterHub deployments.
It&rsquo;s compatible with JupyterHub environments running JupyterLab 4 and Python 3.9+.</p>
<p>It&rsquo;s installed via the <a href="https://github.com/posit-dev/jupyter-positron-server" target="_blank" rel="noopener"><code>jupyter-positron-server</code> Python package</a>, built on Jupyter Server Proxy.
If you&rsquo;ve configured similar services before, setup will feel familiar.
This is not a standalone desktop install.
Rather, it lets you bring Positron into an existing JupyterHub setup.</p>
<h2 id="who-can-use-it">Who can use it?
</h2>
<p>This offering is available to academic institutions using Positron for teaching.
Under a free license, institutions can provide access to enrolled students, course participants, or staff involved in the delivery or receipt of educational programming.</p>
<p>Full eligibility details are available in the <a href="https://github.com/posit-dev/positron/blob/main/LICENSE.txt" target="_blank" rel="noopener">Positron Education License Rider</a>.</p>
<h2 id="getting-started">Getting started
</h2>
<p>Hosting Positron for teaching purposes requires a free license key.
To get set up:</p>
<ol>
<li>Review the eligibility criteria in the <a href="https://github.com/posit-dev/positron/blob/main/LICENSE.txt" target="_blank" rel="noopener">Positron Education License Rider</a>.</li>
<li>Email <a href="mailto:academic-licenses@posit.co">academic-licenses@posit.co</a> to request a teaching license.</li>
<li>Once your license is confirmed, follow the <a href="https://posit-dev.github.io/jupyter-positron-server/" target="_blank" rel="noopener"><code>jupyter-positron-server</code> documentation</a> to complete setup in your JupyterHub environment.</li>
</ol>
<div class="callout callout-tip" role="note" aria-label="Tip">
<div class="callout-header">
<span class="callout-title">Get in touch</span>
</div>
<div class="callout-body">
<p>Have questions or want to learn more?</p>
<p>Reach out to <a href="mailto:academic-licenses@posit.co">academic-licenses@posit.co</a> and let us know you&rsquo;re interested in Positron.
We&rsquo;ll help you navigate next steps!</p>
</div>
</div>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-04-06_positron-server-jupyterhub/images/social.png" length="129271" type="image/png" />
    </item>
    <item>
      <title>orbital 0.5.0</title>
      <link>https://opensource.posit.co/blog/2026-03-13_orbital-0-5-0/</link>
      <pubDate>Fri, 13 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-03-13_orbital-0-5-0/</guid>
      <dc:creator>Emil Hvitfeldt</dc:creator><description><![CDATA[<!--
TODO:
* [x] Look over / edit the post's title in the yaml
* [x] Edit (or delete) the description; note this appears in the Twitter card
* [x] Pick category and tags (see existing with [`hugodown::tidy_show_meta()`](https://rdrr.io/pkg/hugodown/man/use_tidy_post.html))
* [x] Find photo & update yaml metadata
* [x] Create `thumbnail-sq.jpg`; height and width should be equal
* [x] Create `thumbnail-wd.jpg`; width should be >5x height
* [x] [`hugodown::use_tidy_thumbnails()`](https://rdrr.io/pkg/hugodown/man/use_tidy_post.html)
* [x] Add intro sentence, e.g. the standard tagline for the package
* [x] [`usethis::use_tidy_thanks()`](https://usethis.r-lib.org/reference/use_tidy_thanks.html)
-->
<p>We&rsquo;re over the moon to announce the release of <a href="https://orbital.tidymodels.org/" target="_blank" rel="noopener">orbital</a> 0.5.0. orbital lets you predict in databases using tidymodels workflows. orbital uses <a href="https://tidypredict.tidymodels.org/" target="_blank" rel="noopener">tidypredict</a> under the hood to translate fitted models into expressions. This post will also cover things from tidypredict&rsquo;s 1.1.0 release.</p>
<p>This blogpost is about the R orbital package, but there&rsquo;s also a <a href="https://posit-dev.github.io/orbital/" target="_blank" rel="noopener">python version</a> that works on scikit-learn models.</p>
<p>You can install both from CRAN with:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://rdrr.io/r/utils/install.packages.html'>install.packages</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"orbital"</span>, <span class='s'>"tidypredict"</span><span class='o'>)</span><span class='o'>)</span></span></code></pre>
</div>
<p>This blog post will cover the highlights, which are support for more models, faster performance, and more vignettes.</p>
<p>You can see a full list of changes in the <a href="https://orbital.tidymodels.org/news/index.html#orbital-050" target="_blank" rel="noopener">orbital release notes</a> and <a href="https://tidypredict.tidymodels.org/news/index.html#tidypredict-110" target="_blank" rel="noopener">tidypredict release notes</a>.</p>
<h2 id="newly-supported-models">Newly supported models
</h2>
<p>We have added support for new models as well as more prediction types for existing supported models.</p>
<p>The newly supported models are.</p>
<ul>
<li><code>decision_tree(engine = &quot;rpart&quot;)</code></li>
<li><code>boost_tree(engine = &quot;lightgbm&quot;)</code></li>
<li><code>boost_tree(engine = &quot;catboost&quot;)</code> (More on this soon)</li>
</ul>
<p>All of which support regression, classification and probability estimates.</p>
<p>The following models now also support classification and probability estimates in addition to regression.</p>
<ul>
<li><code>mars(engine = &quot;earth&quot;)</code></li>
<li><code>multinom_reg(engine = &quot;glmnet&quot;)</code></li>
<li><code>rand_forest(engine = &quot;randomForest&quot;)</code></li>
<li><code>rand_forest(engine = &quot;ranger&quot;)</code></li>
</ul>
<p>If there is a model type you specifically need <a href="https://github.com/tidymodels/tidypredict/issues/232" target="_blank" rel="noopener">please let us know</a> so we can prioritize new additions.</p>
<h2 id="nested-case_when-support">Nested <code>case_when()</code> support
</h2>
<p>All tree based models were previously implemented as a flat <code>case_when()</code> statement. This means that a small tree with 3 leaves would look like this.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">case_when</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">x</span> <span class="o">&lt;=</span> <span class="m">5</span> <span class="o">&amp;</span> <span class="n">y</span> <span class="o">&lt;=</span> <span class="m">3</span> <span class="o">~</span> <span class="s">&#34;low&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">x</span> <span class="o">&lt;=</span> <span class="m">5</span> <span class="o">&amp;</span> <span class="n">y</span> <span class="o">&gt;</span> <span class="m">3</span>  <span class="o">~</span> <span class="s">&#34;med&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">x</span> <span class="o">&gt;</span> <span class="m">5</span>           <span class="o">~</span> <span class="s">&#34;high&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p>And while this works, it comes with a number of downsides. In this example we have to calculate <code>x &lt;= 5</code> more than once. This might not be that big of a deal in this sized tree but it compounds very fast as the tree grows deeper.</p>
<p>We are also not using the information effectively. Since trees are exhaustive we shouldn&rsquo;t have to calculate the last condition as all other choices have been ruled out. With these considerations in mind we have switched all trees to be expressed as nested <code>case_when()</code> statements.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">case_when</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">x</span> <span class="o">&lt;=</span> <span class="m">5</span> <span class="o">~</span> <span class="nf">case_when</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">y</span> <span class="o">&lt;=</span> <span class="m">3</span> <span class="o">~</span> <span class="s">&#34;low&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">.default</span> <span class="o">=</span> <span class="s">&#34;med&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">.default</span> <span class="o">=</span> <span class="s">&#34;high&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p>This <code>case_when()</code> evaluates exactly the same as the previous flat <code>case_when()</code> statement. While this might be a little harder to read it provides a lot of benefit in terms of performance. Each condition is evaluated at most 1 time. This has a really big influence on the computational speed.</p>
<p>This also means that the R version of orbital now matches what the <a href="https://posit-dev.github.io/orbital/" target="_blank" rel="noopener">python version of orbital</a> does when creating a tree.</p>
<h2 id="new-separate_trees-argument">New <code>separate_trees</code> argument
</h2>
<p>Some models like the ensemble tree models can be represented as a combination of multiple smaller models.This typically manifests as a single massive expression in the following format:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">.pred</span> <span class="o">=</span> <span class="s">&#34;(tree1) + (tree2) + (tree3) + ... + (tree100)&#34;</span></span></span></code></pre></div></div>
<p>This can create trouble for two main reasons. The first one is that this can cause us to hit expression nesting depth when trying to execute these in a database if we have too many trees or have too deep trees. The second related issue is that databases will not be able to recognize that these trees could be calculated in parallel and combined afterwards.</p>
<p>This is where the new <code>separate_trees</code> argument comes in. When setting <code>separate_trees = TRUE</code> in <a href="https://orbital.tidymodels.org/reference/orbital.html" target="_blank" rel="noopener"><code>orbital()</code></a> you change the internal representation of the orbital object to not have a single massive expression for <code>.pred</code> and instead split them out into multiple expressions like so.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">.pred_tree_001</span> <span class="o">=</span> <span class="s">&#34;case_when(...)&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">.pred_tree_002</span> <span class="o">=</span> <span class="s">&#34;case_when(...)&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">.pred_tree_003</span> <span class="o">=</span> <span class="s">&#34;case_when(...)&#34;</span>
</span></span><span class="line"><span class="cl"><span class="kc">...</span>
</span></span><span class="line"><span class="cl"><span class="n">.pred</span> <span class="o">=</span> <span class="s">&#34;.pred_tree_001 + .pred_tree_002 + .pred_tree_003 + ...&#34;</span></span></span></code></pre></div></div>
<p>This representation allows the database query optimizer to potentially evaluate trees in parallel, since each intermediate column is independent.</p>
<p>The <code>separate_trees</code> argument works for the following engines.</p>
<ul>
<li>xgboost</li>
<li>lightgbm</li>
<li>catboost</li>
<li>ranger</li>
<li>randomForest</li>
</ul>
<p>This change alone allows us to work with model types previously not possible with orbital. Together with the nested tree support you can now productionize some of the most popular machine learning models.</p>
<h2 id="splines-support">splines support
</h2>
<p>Spline transformations are commonly used in preprocessing to capture non-linear relationships between predictors and the outcome.</p>
<p>With this release, orbital now supports <code>step_spline_b()</code>, <code>step_spline_convex()</code>, <code>step_spline_monotone()</code>, <code>step_spline_natural()</code>, and <code>step_spline_nonnegative()</code> from the recipes package. Under the hood, splines are translated into piecewise polynomial expressions that can be evaluated directly in SQL.</p>
<h2 id="more-vignettes">More vignettes
</h2>
<p>We have added a handful of new vignettes as well in this release.</p>
<ul>
<li>
<p><a href="https://orbital.tidymodels.org/articles/sql-size.html" target="_blank" rel="noopener">SQL expression sizes</a>: Goes over how different hyperparameters in models affect SQL sizes. This is useful information especially when working with boosted trees as there are many different combinations of hyperparameters that produce similar performance at different SQL expression sizes. With a little effort you could pick a model that runs 10-100 times faster with minimal loss in predictive performance.</p>
</li>
<li>
<p><a href="https://orbital.tidymodels.org/articles/separate-trees.html" target="_blank" rel="noopener">Parallel tree evaluation in databases</a>: A more in-depth look at how the <code>separate_trees</code> argument works. Also includes a section on why and when you should consider using it.</p>
</li>
<li>
<p><a href="https://orbital.tidymodels.org/articles/database-deployment.html" target="_blank" rel="noopener">Database deployment</a>: Shows examples of how we can deploy an orbital model using tables and views.</p>
</li>
<li>
<p><a href="https://tidypredict.tidymodels.org/articles/float-precision.html" target="_blank" rel="noopener">Float precision at split boundaries</a>: Some models like xgboost and Cubist models operate on 32-bit doubles instead of on 64-bit doubles like we have in R. This can cause some problems where predictions don&rsquo;t match exactly. If you use any of these models you should read this vignette to see if this issue is a dealbreaker for you or not.</p>
</li>
</ul>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>A special thanks to <a href="https://github.com/emilyriederer" target="_blank" rel="noopener">Emily Riederer</a> who helped workshop and benchmark these new features.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-03-13_orbital-0-5-0/thumbnail-wd.jpg" length="96543" type="image/jpeg" />
    </item>
    <item>
      <title>Outgrowing your laptop with R and Positron</title>
      <link>https://opensource.posit.co/blog/2026-03-05_outgrow-your-laptop/</link>
      <pubDate>Thu, 05 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-03-05_outgrow-your-laptop/</guid>
      <dc:creator>Julia Silge</dc:creator><description><![CDATA[<h2 id="my-data-is-too-big-for-my-laptop">My data is too big for my laptop!
</h2>
<p>Last week, I had the pleasure of giving a talk to <a href="https://www.meetup.com/rladies-abuja/" target="_blank" rel="noopener">R-Ladies Abuja</a> about how Positron can grow with you as you work on data that is too large for your laptop. The talk was recorded, and you can find it on YouTube here:</p>
<div class="w-full aspect-video">
      <iframe
        src="https://www.youtube.com/embed/sPZsH0eaUpQ"
        class="w-full h-full"
        
        frameborder="0"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
        allowfullscreen></iframe>
    </div>
<p>I opened this talk discussing how I first learned about these &ldquo;beyond your laptop&rdquo; technologies, typically in a organization where these technologies were already in use and seemed specific to infrastructure there. I later came to understand that these technologies are actually related to each other and understanding one can really help when you need to pick up another one. I pointed out some of the Positron features that are designed to make it easier to work with these technologies:</p>
<ul>
<li><a href="https://positron.posit.co/data-explorer.html" target="_blank" rel="noopener">Data Explorer</a></li>
<li><a href="https://positron.posit.co/connections-pane.html" target="_blank" rel="noopener">Connections Pane</a></li>
<li><a href="https://positron.posit.co/remote-ssh.html" target="_blank" rel="noopener">Remote SSH sessions</a></li>
</ul>
<h2 id="check-out-my-slides">Check out my slides
</h2>
<p>If you&rsquo;d like to check out my slides, they are <a href="https://juliasilge.github.io/rladies-abuja/" target="_blank" rel="noopener">available as well</a>:</p>
<iframe src="https://juliasilge.github.io/rladies-abuja/" width="100%" style="aspect-ratio: 16/9; border: none;">
</iframe>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-03-05_outgrow-your-laptop/images/outgrowing.jpg" length="100933" type="image/jpeg" />
    </item>
    <item>
      <title>News from the sparkly-verse</title>
      <link>https://opensource.posit.co/blog/2024-04-22_sparklyr-updates-q1-2024/</link>
      <pubDate>Mon, 22 Apr 2024 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2024-04-22_sparklyr-updates-q1-2024/</guid>
      <dc:creator>Edgar Ruiz</dc:creator><description><![CDATA[<h2 id="highlights">Highlights
</h2>
<p><code>sparklyr</code> and friends have been getting some important updates in the past few
months, here are some highlights:</p>
<ul>
<li>
<p><code>spark_apply()</code> now works on Databricks Connect v2</p>
</li>
<li>
<p><code>sparkxgb</code> is coming back to life</p>
</li>
<li>
<p>Support for Spark 2.3 and below has ended</p>
</li>
</ul>
<h2 id="pysparklyr-014">pysparklyr 0.1.4
</h2>
<p><code>spark_apply()</code> now works on Databricks Connect v2. The latest <code>pysparklyr</code>
release uses the <code>rpy2</code> Python library as the backbone of the integration.</p>
<p>Databricks Connect v2, is based on Spark Connect. At this time, it supports
Python user-defined functions (UDFs), but not R user-defined functions.
Using <code>rpy2</code> circumvents this limitation. As shown in the diagram, <code>sparklyr</code>
sends the the R code to the locally installed <code>rpy2</code>, which in turn sends it
to Spark. Then the <code>rpy2</code> installed in the remote Databricks cluster will run
the R code.</p>
<figure>
<img src="https://opensource.posit.co/blog/2024-04-22_sparklyr-updates-q1-2024/images/r-udfs.png" data-fig-alt="Diagram that shows how sparklyr transmits the R code via the rpy2 python package, and how Spark uses it to run the R code" width="600" alt="R code via rpy2" />
<figcaption aria-hidden="true">R code via rpy2</figcaption>
</figure>
<p>A big advantage of this approach, is that <code>rpy2</code> supports Arrow. In fact it
is the recommended Python library to use when integrating <a href="https://arrow.apache.org/docs/python/integration/python_r.html" target="_blank" rel="noopener">Spark, Arrow and
R</a>.
This means that the data exchange between the three environments will be much
faster!</p>
<p>As in its original implementation, schema inferring works, and as with the
original implementation, it has a performance cost. But unlike the original,
this implementation will return a &lsquo;columns&rsquo; specification that you can use
for the next time you run the call.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">spark_apply</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">tbl_mtcars</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">nrow</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">group_by</span> <span class="o">=</span> <span class="s">&#34;am&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; To increase performance, use the following schema:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; columns = &#34;am double, x long&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; # Source:   table&lt;`sparklyr_tmp_table_b84460ea_b1d3_471b_9cef_b13f339819b6`&gt; [2 x 2]</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; # Database: spark_connection</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;      am     x</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   &lt;dbl&gt; &lt;dbl&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1     0    19</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 2     1    13</span></span></span></code></pre></div></div>
<p>A full article about this new capability is available here:
<a href="https://spark.posit.co/deployment/databricks-connect-udfs.html" target="_blank" rel="noopener">Run R inside Databricks Connect</a></p>
<h2 id="sparkxgb">sparkxgb
</h2>
<p>The <code>sparkxgb</code> is an extension of <code>sparklyr</code>. It enables integration with
<a href="https://xgboost.readthedocs.io/en/stable/" target="_blank" rel="noopener">XGBoost</a>. The current CRAN release
does not support the latest versions of XGBoost. This limitation has recently
prompted a full refresh of <code>sparkxgb</code>. Here is a summary of the improvements,
which are currently in the <a href="https://github.com/rstudio/sparkxgb" target="_blank" rel="noopener">development version of the package</a>:</p>
<ul>
<li>
<p>The <code>xgboost_classifier()</code> and <code>xgboost_regressor()</code> functions no longer
pass values of two arguments. These were deprecated by XGBoost and
cause an error if used. In the R function, the arguments will remain for
backwards compatibility, but will generate an informative error if not left <code>NULL</code>:</p>
<ul>
<li><code>sketch_eps</code> - As of <a href="https://github.com/dmlc/xgboost/blob/59d7b8dc72df7ed942885676964ea0a681d09590/NEWS.md?plain=1#L494" target="_blank" rel="noopener">XGBoost version 1.6.0</a>
<code>sketch_eps</code> was replaced by <code>max_bins</code></li>
<li><code>timeout_request_workers</code> - Removed in <a href="https://github.com/dmlc/xgboost/blob/59d7b8dc72df7ed942885676964ea0a681d09590/NEWS.md?plain=1#L398" target="_blank" rel="noopener">XGBoost version 1.7.0</a>
because it was no longer needed when XGBoost added barrier support</li>
</ul>
</li>
<li>
<p>Updates the JVM version used during the Spark session. It now uses <a href="https://central.sonatype.com/artifact/ml.dmlc/xgboost4j-spark_2.12" target="_blank" rel="noopener">xgboost4j-spark
version 2.0.3</a>,
instead of 0.8.1. This gives us access to XGboost&rsquo;s most recent Spark code.</p>
</li>
<li>
<p>Updates code that used deprecated functions from upstream R dependencies. It
also stops using an un-maintained package as a dependency (<code>forge</code>). This
eliminated all of the warnings that were happening when fitting a model.</p>
</li>
<li>
<p>Major improvements to package testing. Unit tests were updated and expanded,
the way <code>sparkxgb</code> automatically starts and stops the Spark session for testing
was modernized, and the continuous integration tests were restored. This will
ensure the package&rsquo;s health going forward.</p>
</li>
</ul>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">remotes</span><span class="o">::</span><span class="nf">install_github</span><span class="p">(</span><span class="s">&#34;rstudio/sparkxgb&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">sparkxgb</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">sparklyr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">sc</span> <span class="o">&lt;-</span> <span class="nf">spark_connect</span><span class="p">(</span><span class="n">master</span> <span class="o">=</span> <span class="s">&#34;local&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">iris_tbl</span> <span class="o">&lt;-</span> <span class="nf">copy_to</span><span class="p">(</span><span class="n">sc</span><span class="p">,</span> <span class="n">iris</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">xgb_model</span> <span class="o">&lt;-</span> <span class="nf">xgboost_classifier</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">iris_tbl</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">Species</span> <span class="o">~</span> <span class="n">.,</span>
</span></span><span class="line"><span class="cl">  <span class="n">num_class</span> <span class="o">=</span> <span class="m">3</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">num_round</span> <span class="o">=</span> <span class="m">50</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">max_depth</span> <span class="o">=</span> <span class="m">4</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">xgb_model</span> <span class="o">%&gt;%</span> 
</span></span><span class="line"><span class="cl">  <span class="nf">ml_predict</span><span class="p">(</span><span class="n">iris_tbl</span><span class="p">)</span> <span class="o">%&gt;%</span> 
</span></span><span class="line"><span class="cl">  <span class="nf">select</span><span class="p">(</span><span class="n">Species</span><span class="p">,</span> <span class="n">predicted_label</span><span class="p">,</span> <span class="nf">starts_with</span><span class="p">(</span><span class="s">&#34;probability_&#34;</span><span class="p">))</span> <span class="o">%&gt;%</span> 
</span></span><span class="line"><span class="cl">  <span class="n">dplyr</span><span class="o">::</span><span class="nf">glimpse</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Rows: ??</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Columns: 5</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Database: spark_connection</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; $ Species                &lt;chr&gt; &#34;setosa&#34;, &#34;setosa&#34;, &#34;setosa&#34;, &#34;setosa&#34;, &#34;setosa…</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; $ predicted_label        &lt;chr&gt; &#34;setosa&#34;, &#34;setosa&#34;, &#34;setosa&#34;, &#34;setosa&#34;, &#34;setosa…</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; $ probability_setosa     &lt;dbl&gt; 0.9971547, 0.9948581, 0.9968392, 0.9968392, 0.9…</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; $ probability_versicolor &lt;dbl&gt; 0.002097376, 0.003301427, 0.002284616, 0.002284…</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; $ probability_virginica  &lt;dbl&gt; 0.0007479066, 0.0018403779, 0.0008762418, 0.000…</span></span></span></code></pre></div></div>
<h2 id="sparklyr-185">sparklyr 1.8.5
</h2>
<p>The new version of <code>sparklyr</code> does not have user facing improvements. But
internally, it has crossed an important milestone. Support for Spark version 2.3
and below has effectively ended. The Scala
code needed to do so is no longer part of the package. As per Spark&rsquo;s versioning
policy, <a href="https://spark.apache.org/versioning-policy.html" target="_blank" rel="noopener">found here</a>,
Spark 2.3 was &rsquo;end-of-life&rsquo; in 2018.</p>
<p>This is part of a larger, and ongoing effort to make the immense code-base of
<code>sparklyr</code> a little easier to maintain, and hence reduce the risk of failures.
As part of the same effort, the number of upstream packages that <code>sparklyr</code>
depends on have been reduced. This has been happening across multiple CRAN
releases, and in this latest release <code>tibble</code>, and <code>rappdirs</code> are no longer
imported by <code>sparklyr</code>.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2024-04-22_sparklyr-updates-q1-2024/thumbnail.png" length="314789" type="image/png" />
    </item>
    <item>
      <title>Announcing bundle</title>
      <link>https://opensource.posit.co/blog/2022-09-16_bundle-0-1-0/</link>
      <pubDate>Fri, 16 Sep 2022 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2022-09-16_bundle-0-1-0/</guid>
      <dc:creator>Julia Silge</dc:creator><description><![CDATA[<p>We&rsquo;re thrilled to announce the first release of <a href="https://rstudio.github.io/bundle/" target="_blank" rel="noopener">bundle</a>. The bundle package provides a consistent interface to capture all information needed to serialize a model, situate that information within a portable object, and restore it for use in new settings.</p>
<p>You can install it from CRAN with:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;bundle&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>Let&rsquo;s walk through what bundle does, and when you might need to use it.</p>
<h2 id="saving-things-is-hard">Saving things is hard
</h2>
<p>We often think of a trained model as a self-contained R object. The model exists in memory in R and if we have some new data, the model object can generate predictions on its own:</p>
<img src="https://opensource.posit.co/blog/2022-09-16_bundle-0-1-0/diagram_01.png" alt="A diagram showing a rectangle, labeled model object, and another rectangle, labeled predictions. The two are connected by an arrow from model object to predictions, with the label predict." width="100%" />
<p>In reality, model objects sometimes also make use of <em>references</em> to generate predictions. A reference is a piece of information that a model object refers to that isn&rsquo;t part of the object itself; this could be something like a connection to a server, a file on disk, or an internal function in the package used to train the model. When we call <code>predict()</code>, model objects know where to look to retrieve that information:</p>
<img src="https://opensource.posit.co/blog/2022-09-16_bundle-0-1-0/diagram_02.png" alt="A diagram showing the same pair of rectangles as before, connected by the arrow labeled predict. This time, though, we introduce two boxes labeled reference. These two boxes are connected to the arrow labeled predict with dotted arrows, to show that, most of the time, we don't need to think about including them in our workflow." width="100%" />
<p>Saving model objects can sometimes disrupt those references. Thus, if we want to train a model, save it, re-load it in a production setting, and generate predictions with it, we may run into issues:</p>
<img src="https://opensource.posit.co/blog/2022-09-16_bundle-0-1-0/diagram_03.png" alt="A diagram showing the same set of rectangles, representing a prediction problem, as before. This version of the diagram adds two boxes, labeled R Session numbe r one, and R session number two. In R session number two, we have a new rectangle labeled standalone model object. In focus is the arrow from the model object, in R Session number one, to the standalone model object in R session number two." width="100%" />
<p>We need some way to preserve access to those references. This new package provides a consistent interface for <em>bundling</em> model objects with their references so that they can be safely saved and re-loaded in production:</p>
<img src="https://opensource.posit.co/blog/2022-09-16_bundle-0-1-0/diagram_04.png" alt="A replica of the previous diagram, where the arrow previously connecting the model object in R session one and the standalone model object in R session two is connected by a verb called bundle. The bundle function outputs an object called a bundle." width="100%" />
<h2 id="when-to-bundle-your-model">When to bundle your model
</h2>
<p>Let&rsquo;s walk through building a couple of models using data on <a href="https://modeldata.tidymodels.org/reference/cells.html" target="_blank" rel="noopener">cell body segmentation</a>.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">tidymodels</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">data</span><span class="p">(</span><span class="n">cells</span><span class="p">,</span> <span class="n">package</span> <span class="o">=</span> <span class="s">&#34;modeldata&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">set.seed</span><span class="p">(</span><span class="m">123</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">cell_split</span> <span class="o">&lt;-</span> <span class="n">cells</span> <span class="o">%&gt;%</span> 
</span></span><span class="line"><span class="cl">  <span class="nf">select</span><span class="p">(</span><span class="o">-</span><span class="n">case</span><span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">initial_split</span><span class="p">(</span><span class="n">strata</span> <span class="o">=</span> <span class="n">class</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">cell_train</span> <span class="o">&lt;-</span> <span class="nf">training</span><span class="p">(</span><span class="n">cell_split</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">cell_test</span>  <span class="o">&lt;-</span> <span class="nf">testing</span><span class="p">(</span><span class="n">cell_split</span><span class="p">)</span></span></span></code></pre></div></div>
<p>First, let&rsquo;s train a logistic regression model:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">glm_fit</span> <span class="o">&lt;-</span> <span class="nf">glm</span><span class="p">(</span><span class="n">class</span> <span class="o">~</span> <span class="n">.,</span> <span class="n">family</span> <span class="o">=</span> <span class="s">&#34;binomial&#34;</span><span class="p">,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">cell_train</span><span class="p">)</span></span></span></code></pre></div></div>
<p>If we&rsquo;re satisfied with this model and think it is ready for production, we might want to deploy it somewhere, maybe as a REST API or as a Shiny app. A typical approach would be to:</p>
<ul>
<li>save our model object</li>
<li>start up a new R session</li>
<li>load the model object into the new session</li>
<li>predict on new data with the loaded model object</li>
</ul>
<p>The <a href="https://callr.r-lib.org/" target="_blank" rel="noopener">callr</a> package is helpful for demonstrating this kind of situation; it allows us to start up a fresh R session and pass a few objects in.</p>
<p>We&rsquo;ll just make use of two of the arguments to the function <code>r()</code>:</p>
<ul>
<li><code>func</code>: A function that, given a model object and some new data, will generate predictions, and</li>
<li><code>args</code>: A named list, giving the arguments to the above function.</li>
</ul>
<p>Let&rsquo;s save our model object to a temporary file and pass it to a fresh R session for prediction, like if we had deployed the model somewhere.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">callr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">temp_file</span> <span class="o">&lt;-</span> <span class="nf">tempfile</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="nf">saveRDS</span><span class="p">(</span><span class="n">glm_fit</span><span class="p">,</span> <span class="n">file</span> <span class="o">=</span> <span class="n">temp_file</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">r</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="kr">function</span><span class="p">(</span><span class="n">temp_file</span><span class="p">,</span> <span class="n">new_data</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">model_object</span> <span class="o">&lt;-</span> <span class="nf">readRDS</span><span class="p">(</span><span class="n">file</span> <span class="o">=</span> <span class="n">temp_file</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nf">predict</span><span class="p">(</span><span class="n">model_object</span><span class="p">,</span> <span class="n">new_data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="n">args</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">temp_file</span> <span class="o">=</span> <span class="n">temp_file</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">new_data</span> <span class="o">=</span> <span class="nf">head</span><span class="p">(</span><span class="n">cell_test</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<div class="code-block"><pre tabindex="0"><code>##          1          2          3          4          5          6 
## -4.8706401 -1.8143956  2.3386470 -1.2735249 -0.3586448  2.7865270</code></pre></div>
<p>Nice! 😀</p>
<p>What if instead we wanted to train a neural network using tidymodels, with keras as the modeling engine?</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">cell_rec</span> <span class="o">&lt;-</span> 
</span></span><span class="line"><span class="cl">  <span class="nf">recipe</span><span class="p">(</span><span class="n">class</span> <span class="o">~</span> <span class="n">.,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">cell_train</span><span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">step_YeoJohnson</span><span class="p">(</span><span class="nf">all_numeric_predictors</span><span class="p">())</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">step_normalize</span><span class="p">(</span><span class="nf">all_numeric_predictors</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">keras_spec</span> <span class="o">&lt;-</span> 
</span></span><span class="line"><span class="cl">  <span class="nf">mlp</span><span class="p">(</span><span class="n">penalty</span> <span class="o">=</span> <span class="m">0</span><span class="p">,</span> <span class="n">epochs</span> <span class="o">=</span> <span class="m">10</span><span class="p">)</span> <span class="o">%&gt;%</span> 
</span></span><span class="line"><span class="cl">  <span class="nf">set_mode</span><span class="p">(</span><span class="s">&#34;classification&#34;</span><span class="p">)</span> <span class="o">%&gt;%</span> 
</span></span><span class="line"><span class="cl">  <span class="nf">set_engine</span><span class="p">(</span><span class="s">&#34;keras&#34;</span><span class="p">,</span> <span class="n">verbose</span> <span class="o">=</span> <span class="m">0</span><span class="p">)</span> 
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">keras_fit</span> <span class="o">&lt;-</span> 
</span></span><span class="line"><span class="cl">  <span class="nf">workflow</span><span class="p">(</span><span class="n">cell_rec</span><span class="p">,</span> <span class="n">keras_spec</span><span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">fit</span><span class="p">(</span><span class="n">data</span> <span class="o">=</span> <span class="n">cell_train</span><span class="p">)</span></span></span></code></pre></div></div>
<p>Let&rsquo;s try to save this to disk and then reload it in a new session.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">temp_file</span> <span class="o">&lt;-</span> <span class="nf">tempfile</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="nf">saveRDS</span><span class="p">(</span><span class="n">keras_fit</span><span class="p">,</span> <span class="n">file</span> <span class="o">=</span> <span class="n">temp_file</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">r</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="kr">function</span><span class="p">(</span><span class="n">temp_file</span><span class="p">,</span> <span class="n">new_data</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">library</span><span class="p">(</span><span class="n">workflows</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">model_object</span> <span class="o">&lt;-</span> <span class="nf">readRDS</span><span class="p">(</span><span class="n">file</span> <span class="o">=</span> <span class="n">temp_file</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nf">predict</span><span class="p">(</span><span class="n">model_object</span><span class="p">,</span> <span class="n">new_data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="n">args</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">temp_file</span> <span class="o">=</span> <span class="n">temp_file</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">new_data</span> <span class="o">=</span> <span class="nf">head</span><span class="p">(</span><span class="n">cell_test</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<div class="code-block"><pre tabindex="0"><code>## Error: ! error in callr subprocess
## Caused by error in `do.call(object$predict, args)`:
## ! &#39;what&#39; must be a function or character string</code></pre></div>
<p>Oh no! 😱</p>
<p>It turns out that keras models <a href="https://tensorflow.rstudio.com/guides/keras/serialization_and_saving.html" target="_blank" rel="noopener">need to be saved in a special way</a>. This is true of a handful of models, like XGBoost, and even some preprocessing steps, like UMAP. These special ways to save objects, like the ones that keras provide, are often referred to as <em>native serialization</em>. Methods for native serialization know which references need to be brought along in order for an object to effectively do its thing in a new environment, but they are different for each model.</p>
<p>The bundle package provides a consistent way to deal with all these kinds of special serialization. The package provides two functions, <code>bundle()</code> and <code>unbundle()</code>, that take care of all of the minutae of preparing to save and load R objects effectively. You <code>bundle()</code> your model before you save it:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">bundle</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">temp_file</span> <span class="o">&lt;-</span> <span class="nf">tempfile</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">keras_bundle</span> <span class="o">&lt;-</span> <span class="nf">bundle</span><span class="p">(</span><span class="n">keras_fit</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">saveRDS</span><span class="p">(</span><span class="n">keras_bundle</span><span class="p">,</span> <span class="n">file</span> <span class="o">=</span> <span class="n">temp_file</span><span class="p">)</span></span></span></code></pre></div></div>
<p>And then you <code>unbundle()</code> after you read the object in a new session:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">r</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="kr">function</span><span class="p">(</span><span class="n">temp_file</span><span class="p">,</span> <span class="n">new_data</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">library</span><span class="p">(</span><span class="n">bundle</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nf">library</span><span class="p">(</span><span class="n">workflows</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">model_bundle</span> <span class="o">&lt;-</span> <span class="nf">readRDS</span><span class="p">(</span><span class="n">file</span> <span class="o">=</span> <span class="n">temp_file</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">model_object</span> <span class="o">&lt;-</span> <span class="nf">unbundle</span><span class="p">(</span><span class="n">model_bundle</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nf">predict</span><span class="p">(</span><span class="n">model_object</span><span class="p">,</span> <span class="n">new_data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="n">args</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">temp_file</span> <span class="o">=</span> <span class="n">temp_file</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">new_data</span> <span class="o">=</span> <span class="nf">head</span><span class="p">(</span><span class="n">cell_test</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<div class="code-block"><pre tabindex="0"><code>## # A tibble: 6 × 1
##   .pred_class
##   &lt;fct&gt;      
## 1 PS         
## 2 PS         
## 3 WS         
## 4 PS         
## 5 PS         
## 6 WS</code></pre></div>
<p>Hooray! 🎉</p>
<p>We have support in bundle for a <a href="https://rstudio.github.io/bundle/reference/" target="_blank" rel="noopener">wide variety</a> of models that require (or <em>sometimes</em> require) special handling for serialization, from <a href="https://h2o.ai/" target="_blank" rel="noopener">H2O</a> to <a href="https://mlverse.github.io/luz/" target="_blank" rel="noopener">torch luz models</a>. Soon bundle will be integrated into <a href="https://vetiver.rstudio.com/" target="_blank" rel="noopener">vetiver</a>, for better and more robust deployment options. If you use a model that needs special serialization and is not yet supported, <a href="https://github.com/rstudio/bundle/issues" target="_blank" rel="noopener">let us know</a> in an issue.</p>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>Thank you so much to everyone who contributed to this first release: <a href="https://github.com/dfalbel" target="_blank" rel="noopener">@dfalbel</a>, <a href="https://github.com/juliasilge" target="_blank" rel="noopener">@juliasilge</a>, <a href="https://github.com/qiushiyan" target="_blank" rel="noopener">@qiushiyan</a>, and <a href="https://github.com/simonpcouch" target="_blank" rel="noopener">@simonpcouch</a>. I would especially like to highlight Simon&rsquo;s contributions, which have been central to bundle getting off the ground!</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2022-09-16_bundle-0-1-0/thumbnail-wd.jpg" length="185242" type="image/jpeg" />
    </item>
    <item>
      <title>Announcing vetiver for MLOps in R and Python</title>
      <link>https://opensource.posit.co/blog/2022-06-09_announce-vetiver/</link>
      <pubDate>Thu, 09 Jun 2022 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2022-06-09_announce-vetiver/</guid>
      <dc:creator>Julia Silge</dc:creator><description><![CDATA[<!--
TODO:
* [ ] Look over / edit the post's title in the yaml
* [ ] Edit (or delete) the description; note this appears in the Twitter card
* [ ] Pick category and tags (see existing with `hugodown::tidy_show_meta()`)
* [ ] Find photo & update yaml metadata
* [ ] Create `thumbnail-sq.jpg`; height and width should be equal
* [ ] Create `thumbnail-wd.jpg`; width should be >5x height
* [ ] `hugodown::use_tidy_thumbnails()`
* [ ] Add intro sentence, e.g. the standard tagline for the package
* [ ] `usethis::use_tidy_thanks()`
-->
<p>We are thrilled to announce the release of <a href="https://vetiver.rstudio.com/" target="_blank" rel="noopener">vetiver</a>, a framework for MLOps tasks in R and Python! The goal of vetiver is to provide fluent tooling to <strong>version</strong>, <strong>share</strong>, <strong>deploy</strong>, and <strong>monitor</strong> a trained model. If you like perfume or candles, you may recognize this name; vetiver, also known as the &ldquo;oil of tranquility&rdquo;, is used as a stabilizing ingredient in perfumery to preserve more volatile fragrances.</p>
<p>You can install the released version of vetiver for R from <a href="https://cran.r-project.org/package=vetiver" target="_blank" rel="noopener">CRAN</a>:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;vetiver&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>You can install the released version of vetiver for Python from <a href="https://pypi.org/project/vetiver/" target="_blank" rel="noopener">PyPI</a>:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">pip</span> <span class="n">install</span> <span class="n">vetiver</span></span></span></code></pre></div></div>
<p>We are sharing more about what vetiver is and how it works over <a href="https://www.rstudio.com/blog/announce-vetiver/" target="_blank" rel="noopener">on the RStudio blog</a> so check that out, but we want to share here as well!</p>
<h2 id="train-a-model">Train a model
</h2>
<p>For this example, let’s work with data on everyone&rsquo;s favorite dataset on fuel efficiency for cars to predict miles per gallon. In R, we can train a decision tree model to predict miles per gallon using a <a href="https://www.tidymodels.org/" target="_blank" rel="noopener">tidymodels</a> workflow:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">tidymodels</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">car_mod</span> <span class="o">&lt;-</span>
</span></span><span class="line"><span class="cl">    <span class="nf">workflow</span><span class="p">(</span><span class="n">mpg</span> <span class="o">~</span> <span class="n">.,</span> <span class="nf">decision_tree</span><span class="p">(</span><span class="n">mode</span> <span class="o">=</span> <span class="s">&#34;regression&#34;</span><span class="p">))</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">    <span class="nf">fit</span><span class="p">(</span><span class="n">mtcars</span><span class="p">)</span></span></span></code></pre></div></div>
<p>In Python, we can train the same kind of model using <a href="https://scikit-learn.org/" target="_blank" rel="noopener">scikit-learn</a>:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">vetiver.data</span> <span class="kn">import</span> <span class="n">mtcars</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">sklearn</span> <span class="kn">import</span> <span class="n">tree</span>
</span></span><span class="line"><span class="cl"><span class="n">car_mod</span> <span class="o">=</span> <span class="n">tree</span><span class="o">.</span><span class="n">DecisionTreeRegressor</span><span class="p">()</span><span class="o">.</span><span class="n">fit</span><span class="p">(</span><span class="n">mtcars</span><span class="p">,</span> <span class="n">mtcars</span><span class="p">[</span><span class="s2">&#34;mpg&#34;</span><span class="p">])</span></span></span></code></pre></div></div>
<p>For both R and Python, the <code>car_mod</code> object is a fitted model, with parameters estimated using our training data <code>mtcars</code>.</p>
<h2 id="create-a-vetiver-model">Create a vetiver model
</h2>
<p>We can create a <code>vetiver_model()</code> in R or <code>VetiverModel()</code> in Python from the trained model; a vetiver model object collects the information needed to store, version, and deploy a trained model.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">vetiver</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">v</span> <span class="o">&lt;-</span> <span class="nf">vetiver_model</span><span class="p">(</span><span class="n">car_mod</span><span class="p">,</span> <span class="s">&#34;cars_mpg&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">v</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ── cars_mpg ─ &lt;butchered_workflow&gt; model for deployment </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; A rpart regression modeling workflow using 10 features</span></span></span></code></pre></div></div>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">vetiver</span> <span class="kn">import</span> <span class="n">VetiverModel</span>
</span></span><span class="line"><span class="cl"><span class="n">v</span> <span class="o">=</span> <span class="n">VetiverModel</span><span class="p">(</span><span class="n">car_mod</span><span class="p">,</span> <span class="n">model_name</span> <span class="o">=</span> <span class="s2">&#34;cars_mpg&#34;</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">                 <span class="n">save_ptype</span> <span class="o">=</span> <span class="kc">True</span><span class="p">,</span> <span class="n">ptype_data</span> <span class="o">=</span> <span class="n">mtcars</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">v</span><span class="o">.</span><span class="n">description</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; &#34;Scikit-learn &lt;class &#39;sklearn.tree._classes.DecisionTreeRegressor&#39;&gt; model&#34;</span></span></span></code></pre></div></div>
<p>See our documentation for how to use these deployable model objects and:</p>
<ul>
<li><a href="https://vetiver.rstudio.com/get-started/version.html" target="_blank" rel="noopener">publish and version your model</a></li>
<li><a href="https://vetiver.rstudio.com/get-started/deploy.html" target="_blank" rel="noopener">deploy your model as a REST API</a></li>
</ul>
<p>Be sure to also read more <a href="https://www.rstudio.com/blog/announce-vetiver/" target="_blank" rel="noopener">on the RStudio blog</a>.</p>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>We&rsquo;d like to extend our thanks to all of the contributors who helped make these initial releases of vetiver for R and Python possible!</p>
<ul>
<li>
<p>R package: <a href="https://github.com/cderv" target="_blank" rel="noopener">@cderv</a>, <a href="https://github.com/ggpinto" target="_blank" rel="noopener">@ggpinto</a>, <a href="https://github.com/isabelizimm" target="_blank" rel="noopener">@isabelizimm</a>, <a href="https://github.com/juliasilge" target="_blank" rel="noopener">@juliasilge</a>, and <a href="https://github.com/mfansler" target="_blank" rel="noopener">@mfansler</a></p>
</li>
<li>
<p>Python package: <a href="https://github.com/has2k1" target="_blank" rel="noopener">@has2k1</a>, and <a href="https://github.com/isabelizimm" target="_blank" rel="noopener">@isabelizimm</a></p>
</li>
</ul>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2022-06-09_announce-vetiver/thumbnail-wd.jpg" length="152876" type="image/jpeg" />
    </item>
    <item>
      <title>Integrating Dynamic R and Python Models in Tableau Using plumbertableau</title>
      <link>https://opensource.posit.co/blog/2021-12-20_dynamic-r-and-python-models-in-tableau-using-plumbertableau/</link>
      <pubDate>Mon, 20 Dec 2021 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2021-12-20_dynamic-r-and-python-models-in-tableau-using-plumbertableau/</guid>
      <dc:creator>Isabella Velásquez</dc:creator><description><![CDATA[<p>RStudio believes that you can attain greater business intelligence with interoperable tools that <a href="https://www.rstudio.com/solutions/interoperability/" target = "_blank">take the full advantage of open-source data science</a>. Your organization may rely on Tableau for reporting purposes, but how can you ensure that you&rsquo;re using the full power of your data science team&rsquo;s R and Python models in your dashboards?</p>
<p>With the <a href="https://rstudio.github.io/plumbertableau/index.html" target = "_blank">plumbertableau</a> package (and its corresponding Python package, <a href="https://rstudio.github.io/fastapitableau/" target = "_blank">fastapitableau</a>), you can use functions or models created in R or Python from Tableau through an API. These packages allow you to showcase cutting-edge data science results in your organization’s preferred dashboard tool.</p>
<p>While this post mentions R, anything possible with R and plumbertableau is also doable with Python and fastapitableau.</p>
<h2 id="foster-data-analytics-capabilities-with-plumbertableau">Foster Data Analytics Capabilities With plumbertableau
</h2>
<p>With plumbertableau, you can fully develop your model with code-first data science. The package uses <a href="https://www.rplumber.io/" target = "_blank">plumber</a> to create an API directly from your code. Since your model is fully developed in your data science editor, it can use all the packages and complex calculations it needs.</p>
<p>You can extract the best data science results using R&rsquo;s capabilities as your model will not be constrained by Tableau&rsquo;s environment.</p>
<h2 id="improve-data-quality-with-apis-for-continuous-use">Improve Data Quality With APIs for Continuous Use
</h2>
<p>Seamless integration between analytic platforms prevents issues like using outdated, inaccurate, or incomplete data. Rather than depending on a manual process, data scientists can depend on their data pipelines to ensure data integrity.</p>
<p>With plumbertableau, your tools are integrated through an API. The Tableau dashboard displays results without any intermediate manipulation like copy-and-pasting code or uploading datasets. You can work in confidence knowing your results are synchronized, accurate, and reproducible.</p>
<h2 id="increase-deliverability-by-streamlining-data-pipelines">Increase Deliverability by Streamlining Data Pipelines
</h2>
<p>If your model has many dependencies or versioning requirements, it can be difficult to handle them outside of the development environment. Debugging is even more time-consuming when you need to work in separate environments to figure out what went wrong.</p>
<p>With <a href="https://connect.rstudioservices.com/connect/" target = "_blank">RStudio Connect</a>, you can publish directly plumbertableau extensions directly from the RStudio IDE. RStudio Connect automatically manages your API&rsquo;s dependent packages and files to recreate an environment closely mimicking your local development environment. And since all your R code remains in R, you can use your usual data science techniques to efficiently resolve issues.</p>
<p>Read more on the <a href="https://www.rplumber.io/articles/hosting.html/" target = "_blank">Hosting</a> page of the plumber package.</p>
<h2 id="how-to-use-plumbertableau-xgboost-with-dynamic-model-output-example">How to Use plumbertableau: XGBoost with Dynamic Model Output Example
</h2>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2021-12-20_dynamic-r-and-python-models-in-tableau-using-plumbertableau/img/gif4.gif"
      alt="Showing predictive values in Tableau dashboard" 
      loading="lazy"
    >
  </figure></div>
</p>
<p>In this walkthrough, we will be using data from the <a href="https://data.seattle.gov/" target = "_blank">Seattle Open Data Portal</a> to predict the paid parking occupancy percentage in various areas around the city. We will run an XGBoost model in RStudio, create a plumbertableau extension to embed into Tableau, and visualize and interact with the model in a Tableau dashboard. The code is here for reproducibility purposes; however, it will <strong>require</strong> an RStudio Connect account to complete.</p>
<p>The plumbertableau and fastapi packages have wonderful documentation. Be sure to read them for more information on:</p>
<ul>
<li>The anatomy of the extensions</li>
<li>Details on setting up RStudio Connect and Tableau</li>
<li>Other examples to try out in your Tableau dashboards</li>
</ul>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2021-12-20_dynamic-r-and-python-models-in-tableau-using-plumbertableau/img/img2.png"
      alt="Displaying dynamic model output in Tableau steps" 
      loading="lazy"
    >
  </figure></div>
</p>
<h3 id="1-build-the-model">1. Build the model
</h3>
<p>First, we need to build a model. This walkthrough won’t be covering how to create, tune, or validate a model. If you&rsquo;d like to learn more on models and machine learning, check out the <a href="https://www.tidymodels.org/" target = "_blank">tidymodels</a> website and Julia Silge&rsquo;s fantastic <a href="https://juliasilge.com/category/tidymodels/" target = "_blank">screencasts and tutorials</a>.</p>
<p><strong>Load Libraries</strong></p>
<div class="code-block"><pre tabindex="0"><code>library(tidyverse)
library(RSocrata)
library(lubridate)
library(usemodels)
library(tidymodels)</code></pre></div>
<p><strong>Download and Clean Data</strong></p>
<p>The Seattle Open Data Portal uses <a href="https://www.tylertech.com/products/socrata" target = "_blank">Socrata</a>, a data management tool, for its APIs. We can use the <a href="https://cran.r-project.org/web/packages/RSocrata/index.html" target = "_blank">RSocrata</a> package to download the data.</p>
<div class="code-block"><pre tabindex="0"><code>parking_data &lt;-
  RSocrata::read.socrata(
    &#34;https://data.seattle.gov/resource/rke9-rsvs.json?$where=sourceelementkey &lt;= 1020&#34;
  )

parking_id &lt;-
  parking_data %&gt;%
  group_by(blockfacename, location.coordinates) %&gt;%
  mutate(id = cur_group_id()) %&gt;%
  ungroup()

parking_clean &lt;-
  parking_id %&gt;%
  mutate(across(c(parkingspacecount, paidoccupancy), as.numeric),
         occupancy_pct = paidoccupancy / parkingspacecount) %&gt;%
  group_by(
    id = id,
    hour = as.numeric(hour(occupancydatetime)),
    month = as.numeric(month(occupancydatetime)),
    dow = as.numeric(wday(occupancydatetime)),
    date = date(occupancydatetime)
  ) %&gt;%
  summarize(occupancy_pct = mean(occupancy_pct, na.rm = TRUE)) %&gt;%
  drop_na() %&gt;%
  ungroup()</code></pre></div>
<p>We will also need information on the city blocks, so let&rsquo;s create that dataset.</p>
<div class="code-block"><pre tabindex="0"><code>parking_information &lt;-
  parking_id %&gt;%
  mutate(loc = location.coordinates) %&gt;%
  select(id, blockfacename, loc) %&gt;%
  distinct(id, blockfacename, loc) %&gt;%
  unnest_wider(loc, c(&#39;loc1&#39;, &#39;loc2&#39;))</code></pre></div>
<p><strong>Create Training Data</strong></p>
<p>Now, let&rsquo;s create the training set from our original data.</p>
<div class="code-block"><pre tabindex="0"><code>parking_split &lt;-
  parking_clean %&gt;%
  arrange(date) %&gt;%
  select(-date) %&gt;%
  initial_time_split(prop = 0.75)</code></pre></div>
<p><strong>Train and Tune the Model</strong></p>
<p>Here, we train and tune the model. We select the model with the best RSME to use in our dashboard.</p>
<div class="code-block"><pre tabindex="0"><code>xgboost_recipe &lt;-
  recipe(formula = occupancy_pct ~ ., data = parking_clean) %&gt;%
  step_zv(all_predictors())  %&gt;%
  prep()

xgboost_folds &lt;-
  recipes::bake(xgboost_recipe,
                new_data = training(parking_split)) %&gt;%
  rsample::vfold_cv(v = 5)

xgboost_model &lt;-
  boost_tree(
    mode = &#34;regression&#34;,
    trees = 1000,
    min_n = tune(),
    tree_depth = tune(),
    learn_rate = tune(),
    loss_reduction = tune()
  ) %&gt;%
  set_engine(&#34;xgboost&#34;, objective = &#34;reg:squarederror&#34;)

xgboost_params &lt;-
  parameters(min_n(),
             tree_depth(),
             learn_rate(),
             loss_reduction())

xgboost_grid &lt;-
  grid_max_entropy(xgboost_params,
                   size = 5)

xgboost_wf &lt;-
  workflows::workflow() %&gt;%
  add_model(xgboost_model) %&gt;%
  add_formula(occupancy_pct ~ .)

xgboost_tuned &lt;- tune::tune_grid(
  object = xgboost_wf,
  resamples = xgboost_folds,
  grid = xgboost_grid,
  metrics = yardstick::metric_set(rmse, rsq, mae),
  control = tune::control_grid(verbose = TRUE)
)

xgboost_best &lt;-
  xgboost_tuned %&gt;%
  tune::select_best(&#34;rmse&#34;)

xgboost_final &lt;-
  xgboost_model %&gt;%
  finalize_model(xgboost_best)</code></pre></div>
<p>We bundle the recipe and fitted model in an object so we can use it later.</p>
<div class="code-block"><pre tabindex="0"><code>train_processed &lt;-
  bake(xgboost_recipe, new_data = training(parking_split))

prediction_fit &lt;-
  xgboost_final %&gt;%
  fit(formula = occupancy_pct ~ .,
      data    = train_processed)

model_details &lt;- list(model = xgboost_final,
                      recipe = xgboost_recipe,
                      prediction_fit = prediction_fit)</code></pre></div>
<p><strong>Save Objects for the plumbertableau Extension</strong></p>
<p>We&rsquo;ll want to save our data and our model so that we can use them in the extension. If you have an RStudio Connect account, the <a href="https://pins.rstudio.com/" target = "_blank">pins</a> package is a great choice for saving these objects.</p>
<div class="code-block"><pre tabindex="0"><code>rsc &lt;-
  pins::board_rsconnect(server = Sys.getenv(&#34;CONNECT_SERVER&#34;),
                        key = Sys.getenv(&#34;CONNECT_API_KEY&#34;))

pins::pin_write(
  board = rsc,
  x = model_details,
  name = &#34;seattle_parking_model&#34;,
  description = &#34;Seattle Occupancy Percentage XGBoost Model&#34;,
  type = &#34;rds&#34;
)

pins::pin_write(
  board = rsc,
  x = parking_information,
  name = &#34;seattle_parking_info&#34;,
  description = &#34;Seattle Parking Information&#34;,
  type = &#34;rds&#34;
)</code></pre></div>
<h3 id="2-create-a-plumbertableau-extension">2. Create a plumbertableau Extension
</h3>
<p>Next, we will use our model to create a plumbertableau extension. As noted previously, the plumbertableau extension is a Plumber API with some special annotations.</p>
<p>Create an R script called <code>plumber.R</code>. At the top, we list the libraries we&rsquo;ll need.</p>
<div class="code-block"><pre tabindex="0"><code>library(plumber)
library(pins)
library(tibble)
library(xgboost)
library(lubridate)
library(dplyr)
library(tidyr)
library(tidymodels)
library(plumbertableau)</code></pre></div>
<p>We want to bring in our model details and our data. If you pinned your data, you&rsquo;ll change the name of the pin below.</p>
<div class="code-block"><pre tabindex="0"><code>rsc &lt;-
  pins::board_rsconnect(
    server = Sys.getenv(&#34;CONNECT_SERVER&#34;),
    key = Sys.getenv(&#34;CONNECT_API_KEY&#34;)
  )

xgboost_model &lt;-
  pins::pin_read(&#34;isabella.velasquez/seattle_parking_model&#34;, board = rsc)</code></pre></div>
<p>Now, we add our <a href="https://www.rplumber.io/articles/annotations.htm" target = "_blank">annotations</a>. Note that we use plumbertableau annotations, which are slightly different than the ones from plumber.</p>
<ul>
<li>We use <code>tableauArg</code> rather than <code>params</code>.</li>
<li>We specify what is returned to Tableau with <code>tableauReturn</code>.</li>
<li>We must use <code>post</code> for what is being returned.</li>
</ul>
<div class="code-block"><pre tabindex="0"><code>#* @apiTitle Seattle Parking Occupancy Percentage Prediction API
#* @apiDescription Return the predicted occupancy percentage at various Seattle locations

#* @tableauArg block_id:integer numeric block ID
#* @tableauArg ndays:integer number of days in the future for the prediction

#* @tableauReturn [numeric] Predicted occupancy rate
#* @post /pred</code></pre></div>
<p>Now, we create our function with the arguments <code>station_id</code> and <code>ndays</code>. These will have corresponding arguments in Tableau. The function will output our predicted occupancy percentage, which will be what we visualize and interact with in the dashboard.</p>
<p>This function takes the city block and number of days in the future to give us the predicted occupancy percentage at that time.</p>
<div class="code-block"><pre tabindex="0"><code>function(block_id, ndays) {
  times &lt;- Sys.time() + lubridate::ddays(ndays)
  
  current_time &lt;-
    tibble::tibble(times = times,
                   id = block_id)
  
  current_prediction  &lt;-
    current_time %&gt;%
    transmute(
      id = id,
      hour = hour(times),
      month = month(times),
      dow = wday(times),
      occupancy_pct = NA
    ) %&gt;%
    bake(xgboost_model$recipe, .)
  
  parking_prediction &lt;-
    xgboost_model$prediction_fit %&gt;%
    predict(new_data = current_prediction)
  
  predictions &lt;-
    parking_prediction$.pred
  
  predictions[[1]]
  
}</code></pre></div>
<p>Finally, we finish off our script with the extension footer needed for plumbertableau extensions.</p>
<div class="code-block"><pre tabindex="0"><code>#* @plumber
tableau_extension</code></pre></div>
<p>Here is the full <code>plumber.R</code> script:</p>
<div class="code-block"><pre tabindex="0"><code>library(plumber)
library(pins)
library(tibble)
library(xgboost)
library(lubridate)
library(dplyr)
library(tidyr)
library(tidymodels)
library(plumbertableau)

rsc &lt;-
  pins::board_rsconnect(server = Sys.getenv(&#34;CONNECT_SERVER&#34;),
                        key = Sys.getenv(&#34;CONNECT_API_KEY&#34;))

xgboost_model &lt;-
  pins::pin_read(&#34;isabella.velasquez/seattle_parking_model&#34;, board = rsc)

#* @apiTitle Seattle Parking Occupancy Percentage Prediction API
#* @apiDescription Return the predicted occupancy percentage at various Seattle locations

#* @tableauArg block_id:integer numeric block ID
#* @tableauArg ndays:integer number of days in the future for the prediction

#* @tableauReturn [numeric] Predicted occupancy rate
#* @post /pred

function(block_id, ndays) {
  times &lt;- Sys.time() + lubridate::ddays(ndays)
  
  current_time &lt;-
    tibble::tibble(times = times,
                   id = block_id)
  
  current_prediction  &lt;-
    current_time %&gt;%
    transmute(
      id = id,
      hour = hour(times),
      month = month(times),
      dow = wday(times),
      occupancy_pct = NA
    ) %&gt;%
    bake(xgboost_model$recipe, .)
  
  parking_prediction &lt;-
    xgboost_model$prediction_fit %&gt;%
    predict(new_data = current_prediction)
  
  predictions &lt;-
    parking_prediction$.pred
  
  predictions[[1]]
  
}

#* @plumber
tableau_extension</code></pre></div>
<h3 id="3-host-your-api">3. Host your API
</h3>
<p>We have to host our API so that it can be accessed in Tableau. In our case, we publish it to RStudio Connect.</p>
<p>Once hosted, plumbertableau automatically generates a documentation page. Notice that the <code>SCRIPT_*</code> value is not R code. This is a Tableau command that we will use to connect our extension and Tableau.</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2021-12-20_dynamic-r-and-python-models-in-tableau-using-plumbertableau/img/img3.png"
      alt="Automatically generated plumbertableau documentation page" 
      loading="lazy"
    >
  </figure></div>
</p>
<caption><center><i>Automatically generated plumbertableau documentation page</center></i></caption>
<h3 id="4-create-a-calculated-field-in-tableau">4. Create a calculated field in Tableau
</h3>
<p>There are a few steps you need to take so that Tableau can use your plumbertableau extension. If you are using RStudio Connect, read the documentation on how to <a href="https://docs.rstudio.com/rsc/integration/tableau/" target = "_blank">configure RStudio Connect as an analytic extension</a>.</p>
<p>Create a new workbook and upload the <code>station_information</code> file. Under Analysis, turn off Aggregate Measures. Drop <code>Lat</code> into Rows and <code>Lon</code> into Columns, which will create a map. Save the workbook.</p>
<p>Make sure your workbook knows to connect to RStudio Connect by going to Analysis &gt; Manage Analytic Extensions Connection &gt; Choose a Connection. Then, select your Connect account.</p>
<p>Drag <code>Id</code> into the &ldquo;Detail&rdquo; mark. Create a parameter called &ldquo;Days in the Future&rdquo;. We&rsquo;re using our model to predict parking occupancy percentage for that date. Show the parameter on the worksheet.</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2021-12-20_dynamic-r-and-python-models-in-tableau-using-plumbertableau/img/gif1.gif"
      alt="Creating a parameter in Tableau" 
      loading="lazy"
    >
  </figure></div>
</p>
<p>Create a calculated field using the <code>SCRIPT</code> from the plumbertableau documentation page:</p>
<div class="code-block"><pre tabindex="0"><code>SCRIPT_REAL(&#34;/plumbertableau-xgboost-example/pred&#34;, block_id, ndays) </code></pre></div>
<p>For each <code>tableauArg</code> we have listed in the extension, we will replace it with its corresponding Tableau value. If you&rsquo;re following along, this means <code>block_id</code> will become <code>ATTR([Id])</code> and <code>ndays</code> will become <code>ATTR([Days in the Future])</code>.</p>
<div class="code-block"><pre tabindex="0"><code>SCRIPT_REAL(&#34;/plumbertableau-xgboost-example/pred&#34;, ATTR([Id]), ATTR([Days in the Future]))</code></pre></div>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2021-12-20_dynamic-r-and-python-models-in-tableau-using-plumbertableau/img/gif2.gif"
      alt="Creating a calculated field from a plumbertableau extension" 
      loading="lazy"
    >
  </figure></div>
</p>
<h3 id="5-run-model-and-visualize-results-in-tableau">5. Run model and visualize results in Tableau
</h3>
<p>That&rsquo;s it! Once you embed your extension in Tableau’s calculated fields, you can use your model&rsquo;s results in your Tableau dashboard like any other measure or dimension.</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2021-12-20_dynamic-r-and-python-models-in-tableau-using-plumbertableau/img/gif3.gif"
      alt="Showing predictive results in Tableau" 
      loading="lazy"
    >
  </figure></div>
</p>
<p>We can change the <code>ndays</code> argument to get new predictions from our XGBoost model and display them on our Tableau dashboard.</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2021-12-20_dynamic-r-and-python-models-in-tableau-using-plumbertableau/img/gif5.gif"
      alt="Showing predictive results in Tableau dashboard by changing the number of days in the future" 
      loading="lazy"
    >
  </figure></div>
</p>
<p>You can style your Tableau dashboard and then provide your users something that is not only aesthetically pleasing, but is dynamically calculating predictions based on a model you have created in R.</p>
<h2 id="conclusion">Conclusion
</h2>
<p>With plumbertableau, you can showcase sophisticated model results that are easy to integrate, debug, and reproduce. Your work will be at the forefront of data science while being visualized in Tableau&rsquo;s easy, point-and-click interface.</p>
<h2 id="learn-more">Learn More
</h2>
<p>Watch James Blair showcase plumbertableau in Leveraging R &amp; Python in Tableau with RStudio Connect:</p>
<script src="https://fast.wistia.com/embed/medias/hl37qvfnml.jsonp" async></script><script src="https://fast.wistia.com/assets/external/E-v1.js" async></script><div class="wistia_responsive_padding" style="padding:56.25% 0 0 0;position:relative;"><div class="wistia_responsive_wrapper" style="height:100%;left:0;position:absolute;top:0;width:100%;"><div class="wistia_embed wistia_async_hl37qvfnml videoFoam=true" style="height:100%;position:relative;width:100%"><div class="wistia_swatch" style="height:100%;left:0;opacity:0;overflow:hidden;position:absolute;top:0;transition:opacity 200ms;width:100%;"><img src="https://fast.wistia.com/embed/medias/hl37qvfnml/swatch" style="filter:blur(5px);height:100%;object-fit:contain;width:100%;" alt="" aria-hidden="true" onload="this.parentNode.style.opacity=1;" /></div></div></div></div>
<p>More on how RStudio supports interoperability across tools can be found on our <a href="https://www.rstudio.com/solutions/bi-and-data-science/" target = "_blank">BI and Data Science Overview Page</a>.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2021-12-20_dynamic-r-and-python-models-in-tableau-using-plumbertableau/thumbnail.png" length="97818" type="image/png" />
    </item>
    <item>
      <title>Sharing Data With the pins Package</title>
      <link>https://opensource.posit.co/blog/2021-12-15_sharing-data-with-the-pins-package/</link>
      <pubDate>Wed, 15 Dec 2021 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2021-12-15_sharing-data-with-the-pins-package/</guid>
      <dc:creator>Katie Masiello</dc:creator>
      <dc:creator>Isabella Velásquez</dc:creator><description><![CDATA[<caption>
Photo by <a href="https://unsplash.com/@universaleye?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Universal Eye</a> on <a href="https://unsplash.com/@ivelasq/likes?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>
</caption>
<p>Teams often need access to key data to do their work, but have you ever opened your coworker&rsquo;s script to see:</p>
<div class="code-block"><pre tabindex="0"><code>dat &lt;-  
 read_csv(&#34;C://Users/someone_else/data/dataset.csv&#34;)

more_dat &lt;- 
 read_csv(&#34;S://Path_to_mapped_drive_that_you_dont_have/dataset.csv&#34;)</code></pre></div>
<p>Yikes! How will you get these files? Let&rsquo;s hope you can reach your coworker before they’ve logged off for the day.</p>
<p>How can your code be reproducible if you have to manually change the file paths? <em>Shudder</em>.</p>
<p>What if you need to make edits to the data, will you have to keep copying CSVs and emailing files forever? <em>Double shudder.</em></p>
<p>What if your coworker accidentally forwards your email to someone who is not supposed to have access? <em>Oh no.</em></p>
<p>We can struggle to share data assets easily and safely, relying on emailed files to keep our analyses up to date. This makes it difficult to keep current or know what version of the data we’re using. If you&rsquo;ve ever experienced any of the scenarios above, consider <a href="https://www.rstudio.com/blog/pins-1-0-0/" target = "_blank">pins</a> as a solution that can help you share your data assets.</p>
<h2 id="what-is-a-pin-anyway">What <em>is</em> a pin, anyway?
</h2>
<p>Pins, from the <a href="https://pins.rstudio.com/" target = "_blank">R package of the same name</a>, are a versatile way to publish R objects on a virtual corkboard so you can share them across projects and people.</p>
<p>Good pins are data or assets that are a few hundred megabytes or smaller. You can pin just about any object: data, models, JSON files, feather files from the Arrow package, and more. One of the most frequent use cases is pinning small data sets — often ephemeral data or reference tables that don&rsquo;t quite merit being in a database, but seemingly don&rsquo;t have a good home elsewhere (until now).</p>
<p>Pins get published to a board, which can be an <a href="https://www.rstudio.com/products/connect/" target = "_blank">RStudio Connect</a> server, an AWS S3 bucket or Azure Blob Storage, a shared drive like Dropbox or Sharepoint, or a <a href="https://pins.rstudio.com/reference/index.html#section-boards" target = "_blank">variety of other options</a>. Try it out for yourself — read in this data set we’ve pinned for you on RStudio Connect!</p>
<div class="code-block"><pre tabindex="0"><code># Install the latest pins from CRAN
install.packages(&#34;pins&#34;)

library(pins)

# Identify the board
board &lt;-
  board_url(c(&#34;penguins&#34; = &#34;https://colorado.rstudio.com/rsc/example_pin/&#34;))

# Read the shared data
board %&gt;%
  pin_read(&#34;penguins&#34;)</code></pre></div>
<p>In short, if you’ve ever wondered where to put an R object that you or your colleague will need to use again, you might just want to pin it.</p>
<h2 id="pins-for-sharing-across-projects-and-teams">Pins for Sharing Across Projects and Teams
</h2>
<p>One of the greatest strengths of pins is how your pin becomes accessible directly from your R scripts  <em>and</em> the R scripts of anyone else to whom you’ve given access. Different projects can include code that reads the same pin without creating more copies of the data:</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2021-12-15_sharing-data-with-the-pins-package/images/image1.png"
      alt="Three projects using the same pin to download data" 
      loading="lazy"
    >
  </figure></div>
</p>
<p>It&rsquo;s easier (and safer) to share a pin across multiple projects or people than to email files around. Pins respect the access controls of the board. Say you’ve pinned to RStudio Connect: you can control who gets to use the pin, just like any other piece of content.</p>
<h2 id="pins-for-updating-and-versioning">Pins for Updating and Versioning
</h2>
<p>You may be wondering why use pins if you already have a shared drive with your teammates. But what happens if you need to replace the dataset with a new one? Do you email everybody to let them know? Is it dataFINALv2.csv? Or dataFINALfinal.csv?</p>
<p>The pins package retrieves the newest version of the pin by default. That means pin users never have to worry about getting a stale version of the pin. If you need to update your pin regularly, a scheduled R Markdown on RStudio Connect can handle this task for you, so your pin stays fresh.</p>
<p>But you’re not locked into losing old versions of a pin. You can version pins so that writing to an existing pin adds a new copy rather than replacing the existing data.</p>
<p>Here&rsquo;s what versioning looks like using a temporary board:</p>
<div class="code-block"><pre tabindex="0"><code>library(pins)


board2 &lt;- board_temp(versioned = TRUE)

board2 %&gt;% pin_write(1:5, name = &#34;x&#34;, type = &#34;rds&#34;)
#&gt; Creating new version &#39;20210304T050607Z-ab444&#39;
#&gt; Writing to pin &#39;x&#39;

board2 %&gt;% pin_write(2:6, name = &#34;x&#34;, type = &#34;rds&#34;)
#&gt; Creating new version &#39;20210304T050607Z-a077a&#39;
#&gt; Writing to pin &#39;x&#39;

board2 %&gt;% pin_write(3:7, name = &#34;x&#34;, type = &#34;rds&#34;)
#&gt; Creating new version &#39;20210304T050607Z-0a284&#39;
#&gt; Writing to pin &#39;x&#39;

# see all versions
board2 %&gt;% pin_versions(&#34;x&#34;)
#&gt; # A tibble: 3 × 3
#&gt;   version                created             hash 
#&gt;   &lt;chr&gt;                  &lt;dttm&gt;              &lt;chr&gt;
#&gt; 1 20210304T050607Z-0a284 2021-03-04 05:06:00 0a284
#&gt; 2 20210304T050607Z-a077a 2021-03-04 05:06:00 a077a
#&gt; 3 20210304T050607Z-ab444 2021-03-04 05:06:00 ab444</code></pre></div>
<h2 id="learn-more">Learn More
</h2>
<p>With pins, you and your teammates can know where your important data assets are, how to access them, and whether they are the correct version. You can work with confidence knowing you’re using the right asset, your work is reproducible, and you’re following good practices for data management.</p>
<p>There’s more to explore with pins. We’re excited to share how you can adopt them into your workflow.</p>
<p>Learn more about how and when to use pins:</p>
<ul>
<li><a href="https://pins.rstudio.com/" target = "_blank">The pins package documentation</a></li>
<li><a href="https://docs.rstudio.com/how-to-guides/users/pro-tips/pins/" target = "_blank">RStudio Pro Tips: Creating Efficient Workflows with <code>pins</code> and RStudio Connect</a></li>
</ul>
<p>See pins in action:</p>
<ul>
<li>Pins can pull intensive ETL processes out of your apps, improve performance, and save you the hassle of redeploying whenever the underlying data changes.
<ul>
<li>Watch: <a href="https://www.rstudio.com/resources/rstudioconf-2020/deploying-end-to-end-data-science-with-shiny-plumber-and-pins/" target = "_blank">Deploying End-To-End Data Science with Shiny, Plumber, and Pins</a></li>
</ul>
</li>
<li>Pins can play a key role in MLOps, publishing versioned models, and monitoring model metrics.
<ul>
<li>Read: <a href="https://www.rstudio.com/blog/model-monitoring-with-r-markdown/" target = "_blank">Model Monitoring with R Markdown, pins, and RStudio Connect</a></li>
</ul>
</li>
</ul>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2021-12-15_sharing-data-with-the-pins-package/thumbnail.png" length="392754" type="image/png" />
    </item>
    <item>
      <title>sparklyr.sedona: A sparklyr extension for analyzing geospatial data</title>
      <link>https://opensource.posit.co/blog/2021-07-07_sparklyr-sedona/</link>
      <pubDate>Wed, 07 Jul 2021 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2021-07-07_sparklyr-sedona/</guid>
      <dc:creator>Yitao Li</dc:creator><description><![CDATA[<p><a href="https://github.com/apache/incubator-sedona/tree/master/R/sparklyr.sedona" target="_blank" rel="noopener"><code>sparklyr.sedona</code></a> is now available
as the <code>sparklyr</code>-based R interface for <a href="https://sedona.apache.org/" target="_blank" rel="noopener">Apache Sedona</a>.</p>
<p>To install <code>sparklyr.sedona</code> from GitHub using
the <a href="https://cran.r-project.org/web/packages/remotes/index.html" target="_blank" rel="noopener"><code>remotes</code></a> package
<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>, run</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">remotes</span><span class="o">::</span><span class="nf">install_github</span><span class="p">(</span><span class="n">repo</span> <span class="o">=</span> <span class="s">&#34;apache/incubator-sedona&#34;</span><span class="p">,</span> <span class="n">subdir</span> <span class="o">=</span> <span class="s">&#34;R/sparklyr.sedona&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>In this blog post, we will provide a quick introduction to <code>sparklyr.sedona</code>, outlining the motivation behind
this <code>sparklyr</code> extension, and presenting some example <code>sparklyr.sedona</code> use cases involving Spark spatial RDDs,
Spark dataframes, and visualizations.</p>
<h2 id="motivation-for-sparklyrsedona">Motivation for <code>sparklyr.sedona</code>
</h2>
<p>A suggestion from the
<a href="https://opensource.posit.co/blog/2021-02-17_keydanasurvey2021/">mlverse survey results</a> earlier
this year mentioned the need for up-to-date R interfaces for Spark-based GIS frameworks.
While looking into this suggestion, we learned about
<a href="https://sedona.apache.org/" target="_blank" rel="noopener">Apache Sedona</a>, a geospatial data system powered by Spark
that is modern, efficient, and easy to use. We also realized that while our friends from the
Spark open-source community had developed a
<a href="https://github.com/harryprince/geospark" target="_blank" rel="noopener"><code>sparklyr</code> extension</a> for GeoSpark, the
predecessor of Apache Sedona, there was no similar extension making more recent Sedona
functionalities easily accessible from R yet.
We therefore decided to work on <code>sparklyr.sedona</code>, which aims to bridge the gap between
Sedona and R.</p>
<h2 id="the-lay-of-the-land">The lay of the land<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>
</h2>
<p>We hope you are ready for a quick tour through some of the RDD-based and
Spark-dataframe-based functionalities in <code>sparklyr.sedona</code>, and also, some bedazzling
visualizations derived from geospatial data in Spark.</p>
<p>In Apache Sedona,
<a href="https://sedona.apache.org/api/javadoc/core/org/apache/sedona/core/spatialRDD/SpatialRDD.html" target="_blank" rel="noopener">Spatial Resilient Distributed Datasets</a>(SRDDs)
are basic building blocks of distributed spatial data encapsulating
&ldquo;vanilla&rdquo; <a href="https://spark.apache.org/docs/latest/api/java/index.html?org/apache/spark/api/java/JavaRDD.html" target="_blank" rel="noopener">RDD</a>s of
geometrical objects and indexes. SRDDs support low-level operations such as Coordinate Reference System (CRS)
transformations, spatial partitioning, and spatial indexing. For example, with <code>sparklyr.sedona</code>, SRDD-based operations we can perform include the following:</p>
<ul>
<li>Importing some external data source into a SRDD:</li>
</ul>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">sparklyr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">sparklyr.sedona</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">sedona_git_repo</span> <span class="o">&lt;-</span> <span class="nf">normalizePath</span><span class="p">(</span><span class="s">&#34;~/incubator-sedona&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">data_dir</span> <span class="o">&lt;-</span> <span class="nf">file.path</span><span class="p">(</span><span class="n">sedona_git_repo</span><span class="p">,</span> <span class="s">&#34;core&#34;</span><span class="p">,</span> <span class="s">&#34;src&#34;</span><span class="p">,</span> <span class="s">&#34;test&#34;</span><span class="p">,</span> <span class="s">&#34;resources&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">sc</span> <span class="o">&lt;-</span> <span class="nf">spark_connect</span><span class="p">(</span><span class="n">master</span> <span class="o">=</span> <span class="s">&#34;local&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">pt_rdd</span> <span class="o">&lt;-</span> <span class="nf">sedona_read_dsv_to_typed_rdd</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">sc</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">location</span> <span class="o">=</span> <span class="nf">file.path</span><span class="p">(</span><span class="n">data_dir</span><span class="p">,</span> <span class="s">&#34;arealm.csv&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">type</span> <span class="o">=</span> <span class="s">&#34;point&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<ul>
<li>Applying spatial partitioning to all data points:</li>
</ul>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">sedona_apply_spatial_partitioner</span><span class="p">(</span><span class="n">pt_rdd</span><span class="p">,</span> <span class="n">partitioner</span> <span class="o">=</span> <span class="s">&#34;kdbtree&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<ul>
<li>Building spatial index on each partition:</li>
</ul>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">sedona_build_index</span><span class="p">(</span><span class="n">pt_rdd</span><span class="p">,</span> <span class="n">type</span> <span class="o">=</span> <span class="s">&#34;quadtree&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<ul>
<li>Joining one spatial data set with another using &ldquo;contain&rdquo; or &ldquo;overlap&rdquo; as the join predicate:</li>
</ul>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">polygon_rdd</span> <span class="o">&lt;-</span> <span class="nf">sedona_read_dsv_to_typed_rdd</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">sc</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">location</span> <span class="o">=</span> <span class="nf">file.path</span><span class="p">(</span><span class="n">data_dir</span><span class="p">,</span> <span class="s">&#34;primaryroads-polygon.csv&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">type</span> <span class="o">=</span> <span class="s">&#34;polygon&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">pts_per_region_rdd</span> <span class="o">&lt;-</span> <span class="nf">sedona_spatial_join_count_by_key</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">pt_rdd</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">polygon_rdd</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">join_type</span> <span class="o">=</span> <span class="s">&#34;contain&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">partitioner</span> <span class="o">=</span> <span class="s">&#34;kdbtree&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p>It is worth mentioning that <code>sedona_spatial_join()</code> will perform spatial partitioning
and indexing on the inputs using the <code>partitioner</code> and <code>index_type</code> only if the inputs
are not partitioned or indexed as specified already.</p>
<p>From the examples above, one can see that SRDDs are great for spatial operations requiring
fine-grained control, e.g., for ensuring a spatial join query is executed as efficiently
as possible with the right types of spatial partitioning and indexing.</p>
<p>Finally, we can try visualizing the join result above, using a choropleth map:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">sedona_render_choropleth_map</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">pts_per_region_rdd</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">resolution_x</span> <span class="o">=</span> <span class="m">1000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">resolution_y</span> <span class="o">=</span> <span class="m">600</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">output_location</span> <span class="o">=</span> <span class="nf">tempfile</span><span class="p">(</span><span class="s">&#34;choropleth-map-&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">boundary</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="m">-126.790180</span><span class="p">,</span> <span class="m">-64.630926</span><span class="p">,</span> <span class="m">24.863836</span><span class="p">,</span> <span class="m">50.000</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">base_color</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="m">63</span><span class="p">,</span> <span class="m">127</span><span class="p">,</span> <span class="m">255</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p>which gives us the following:</p>
<figure>
<img src="https://opensource.posit.co/blog/2021-07-07_sparklyr-sedona/images/choropleth-map.png" alt="Example choropleth map output" />
<figcaption aria-hidden="true">Example choropleth map output</figcaption>
</figure>
<p>Wait, but something seems amiss. To make the visualization above look nicer, we can
overlay it with the contour of each polygonal region:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">contours</span> <span class="o">&lt;-</span> <span class="nf">sedona_render_scatter_plot</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">polygon_rdd</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">resolution_x</span> <span class="o">=</span> <span class="m">1000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">resolution_y</span> <span class="o">=</span> <span class="m">600</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">output_location</span> <span class="o">=</span> <span class="nf">tempfile</span><span class="p">(</span><span class="s">&#34;scatter-plot-&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">boundary</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="m">-126.790180</span><span class="p">,</span> <span class="m">-64.630926</span><span class="p">,</span> <span class="m">24.863836</span><span class="p">,</span> <span class="m">50.000</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">base_color</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="m">255</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="m">0</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">browse</span> <span class="o">=</span> <span class="kc">FALSE</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">sedona_render_choropleth_map</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">pts_per_region_rdd</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">resolution_x</span> <span class="o">=</span> <span class="m">1000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">resolution_y</span> <span class="o">=</span> <span class="m">600</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">output_location</span> <span class="o">=</span> <span class="nf">tempfile</span><span class="p">(</span><span class="s">&#34;choropleth-map-&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">boundary</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="m">-126.790180</span><span class="p">,</span> <span class="m">-64.630926</span><span class="p">,</span> <span class="m">24.863836</span><span class="p">,</span> <span class="m">50.000</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">base_color</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="m">63</span><span class="p">,</span> <span class="m">127</span><span class="p">,</span> <span class="m">255</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">overlay</span> <span class="o">=</span> <span class="n">contours</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p>which gives us the following:</p>
<figure>
<img src="https://opensource.posit.co/blog/2021-07-07_sparklyr-sedona/images/choropleth-map-with-overlay.png" alt="Choropleth map with overlay" />
<figcaption aria-hidden="true">Choropleth map with overlay</figcaption>
</figure>
<p>With some low-level spatial operations taken care of using the SRDD API and
the right spatial partitioning and indexing data structures, we can then
import the results from SRDDs to Spark dataframes. When working with spatial
objects within Spark dataframes, we can write high-level, declarative queries
on these objects using <code>dplyr</code> verbs in conjunction with Sedona
<a href="https://sedona.apache.org/api/sql/Function/" target="_blank" rel="noopener">spatial UDFs</a>, e.g.
<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>
, the
following query tells us whether each of the <code>8</code> nearest polygons to the
query point contains that point, and also, the convex hull of each polygon.</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">tbl</span> <span class="o">&lt;-</span> <span class="n">DBI</span><span class="o">::</span><span class="nf">dbGetQuery</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">sc</span><span class="p">,</span> <span class="s">&#34;SELECT ST_GeomFromText(\&#34;POINT(-66.3 18)\&#34;) AS `pt`&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">pt</span> <span class="o">&lt;-</span> <span class="n">tbl</span><span class="o">$</span><span class="n">pt[[1]]</span>
</span></span><span class="line"><span class="cl"><span class="n">knn_rdd</span> <span class="o">&lt;-</span> <span class="nf">sedona_knn_query</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">polygon_rdd</span><span class="p">,</span> <span class="n">x</span> <span class="o">=</span> <span class="n">pt</span><span class="p">,</span> <span class="n">k</span> <span class="o">=</span> <span class="m">8</span><span class="p">,</span> <span class="n">index_type</span> <span class="o">=</span> <span class="s">&#34;rtree&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">knn_sdf</span> <span class="o">&lt;-</span> <span class="n">knn_rdd</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">sdf_register</span><span class="p">()</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="n">dplyr</span><span class="o">::</span><span class="nf">mutate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">contains_pt</span> <span class="o">=</span> <span class="nf">ST_contains</span><span class="p">(</span><span class="n">geometry</span><span class="p">,</span> <span class="nf">ST_Point</span><span class="p">(</span><span class="m">-66.3</span><span class="p">,</span> <span class="m">18</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">    <span class="n">convex_hull</span> <span class="o">=</span> <span class="nf">ST_ConvexHull</span><span class="p">(</span><span class="n">geometry</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">knn_sdf</span> <span class="o">%&gt;%</span> <span class="nf">print</span><span class="p">()</span></span></span></code></pre></div></div>
<pre><code># Source: spark&lt;?&gt; [?? x 3]
  geometry                         contains_pt convex_hull
  &lt;list&gt;                           &lt;lgl&gt;       &lt;list&gt;
1 &lt;POLYGON ((-66.335674 17.986328… TRUE        &lt;POLYGON ((-66.335674 17.986328,…
2 &lt;POLYGON ((-66.335432 17.986626… TRUE        &lt;POLYGON ((-66.335432 17.986626,…
3 &lt;POLYGON ((-66.335432 17.986626… TRUE        &lt;POLYGON ((-66.335432 17.986626,…
4 &lt;POLYGON ((-66.335674 17.986328… TRUE        &lt;POLYGON ((-66.335674 17.986328,…
5 &lt;POLYGON ((-66.242489 17.988637… FALSE       &lt;POLYGON ((-66.242489 17.988637,…
6 &lt;POLYGON ((-66.242489 17.988637… FALSE       &lt;POLYGON ((-66.242489 17.988637,…
7 &lt;POLYGON ((-66.24221 17.988799,… FALSE       &lt;POLYGON ((-66.24221 17.988799, …
8 &lt;POLYGON ((-66.24221 17.988799,… FALSE       &lt;POLYGON ((-66.24221 17.988799, …
</code></pre>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>The author of this blog post would like to thank <a href="https://github.com/jiayuasu" target="_blank" rel="noopener">Jia Yu</a>,
the creator of Apache Sedona, and <a href="https://github.com/lorenzwalthert" target="_blank" rel="noopener">Lorenz Walthert</a> for
their suggestion to contribute <code>sparklyr.sedona</code> to the upstream
<a href="https://github.com/apache/incubator-sedona" target="_blank" rel="noopener">incubator-sedona</a> repository. Jia has provided
extensive code-review feedback to ensure <code>sparklyr.sedona</code> complies with coding standards
and best practices of the Apache Sedona project, and has also been very helpful in the
instrumentation of CI workflows verifying <code>sparklyr.sedona</code> works as expected with snapshot
versions of Sedona libraries from development branches.</p>
<p>The author is also grateful for his colleague <a href="https://github.com/skeydan" target="_blank" rel="noopener">Sigrid Keydana</a>
for valuable editorial suggestions on this blog post.</p>
<p>That&rsquo;s all. Thank you for reading!</p>
<p>Photo by <a href="https://unsplash.com/@nasa" target="_blank" rel="noopener">NASA</a> on <a href="https://unsplash.com/" target="_blank" rel="noopener">Unsplash</a></p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><code>sparklyr.sedona</code> was not released to CRAN yet at the time of writing.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Yes, pun intended&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>This demo requires sparklyr 1.7 or above to generate the required Spark SQL type casts for <code>ST_Point()</code> automatically.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2021-07-07_sparklyr-sedona/thumbnail.jpg" length="374380" type="image/jpeg" />
    </item>
    <item>
      <title>sparklyr 1.7: New data sources and spark_apply() capabilities, better interfaces for sparklyr extensions, and more!</title>
      <link>https://opensource.posit.co/blog/2021-07-06_sparklyr-1.7/</link>
      <pubDate>Tue, 06 Jul 2021 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2021-07-06_sparklyr-1.7/</guid>
      <dc:creator>Yitao Li</dc:creator><description><![CDATA[<p><a href="https://sparklyr.ai" target="_blank" rel="noopener"><code>Sparklyr</code></a> 1.7 is now available on <a href="https://cran.r-project.org/web/packages/sparklyr/index.html" target="_blank" rel="noopener">CRAN</a>!</p>
<p>To install <code>sparklyr</code> 1.7 from CRAN, run</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;sparklyr&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>In this blog post, we wish to present the following highlights from the <code>sparklyr</code> 1.7 release:</p>
<ul>
<li><a href="#image-and-binary-data-sources">Image and binary data sources</a></li>
<li><a href="#new-spark_apply-capabilities">New spark_apply() capabilities</a></li>
<li><a href="#better-integration-with-sparklyr-extensions">Better integration with sparklyr extensions</a></li>
<li><a href="#other-exciting-news">Other exciting news</a></li>
</ul>
<h2 id="image-and-binary-data-sources">Image and binary data sources
</h2>
<p>As a unified analytics engine for large-scale data processing, <a href="https://spark.apache.org" target="_blank" rel="noopener">Apache Spark</a>
is well-known for its ability to tackle challenges associated with the volume, velocity, and last but
not least, the variety of big data. Therefore it is hardly surprising to see that &ndash; in response to recent
advances in deep learning frameworks &ndash; Apache Spark has introduced built-in support for
<a href="https://issues.apache.org/jira/browse/SPARK-22666" target="_blank" rel="noopener">image data sources</a>
and <a href="https://issues.apache.org/jira/browse/SPARK-25348" target="_blank" rel="noopener">binary data sources</a> (in releases 2.4 and 3.0, respectively).
The corresponding R interfaces for both data sources, namely,
<a href="https://spark.rstudio.com/reference/spark_read_image.html" target="_blank" rel="noopener"><code>spark_read_image()</code></a> and
<a href="https://spark.rstudio.com/reference/spark_read_binary.html" target="_blank" rel="noopener"><code>spark_read_binary()</code></a>, were shipped
recently as part of <code>sparklyr</code> 1.7.</p>
<p>The usefulness of data source functionalities such as <code>spark_read_image()</code> is perhaps best illustrated
by a quick demo below, where <code>spark_read_image()</code>, through the standard Apache Spark
<a href="https://spark.apache.org/docs/latest/api/java/org/apache/spark/ml/image/ImageSchema.html" target="_blank" rel="noopener"><code>ImageSchema</code></a>,
helps connecting raw image inputs to a sophisticated feature extractor and a classifier, forming a powerful
Spark application for image classifications.</p>
<h3 id="the-demo">The demo
</h3>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2021-07-06_sparklyr-1.7/images/photo-1571324524859-899fbd151860.jpeg"
      alt="" 
      loading="lazy"
    >
  </figure></div>

Photo by <a href="https://unsplash.com/@danieltuttle" target="_blank" rel="noopener">Daniel Tuttle</a> on
<a href="https://unsplash.com/?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText" target="_blank" rel="noopener">Unsplash</a></p>
<p>In this demo, we shall construct a scalable Spark ML pipeline capable of classifying images of cats and dogs
accurately and efficiently, using <code>spark_read_image()</code> and a pre-trained convolutional neural network
code-named <code>Inception</code> (Szegedy et al. (2015)).</p>
<p>The first step to building such a demo with maximum portability and repeatability is to create a
<a href="https://spark.rstudio.com/extensions/" target="_blank" rel="noopener">sparklyr extension</a> that accomplishes the following:</p>
<ul>
<li>Specifying the required MVN dependencies of this demo (namely, the
<a href="https://spark-packages.org/package/databricks/spark-deep-learning" target="_blank" rel="noopener">Spark Deep Learning library</a>
(Databricks, Inc. (2019)), which contains an <code>Inception</code>-V3-based image feature extractor accessible through
the <a href="https://spark.apache.org/docs/latest/ml-pipeline.html#transformers" target="_blank" rel="noopener">Spark ML Transformer interface</a>)</li>
<li>Bundling with itself two <a href="https://xkcd.com/221" target="_blank" rel="noopener">randomly selected</a>
<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> and disjoint subsets of the
dogs-vs-cats dataset (Elson et al. (2007)) as train and test data, which are stored in the <code>extdata/{train,test}</code> sub
directories of the package)</li>
</ul>
<p>A reference implementation of such a <code>sparklyr</code> extension can be found in
<a href="https://github.com/mlverse/sparklyr-image-classification-demo" target="_blank" rel="noopener">here</a>.</p>
<p>The second step, of course, is to make use of the above-mentioned <code>sparklyr</code> extension to perform some feature
engineering. We will see very high-level features being extracted intelligently from each cat/dog image based
on what the pre-built <code>Inception</code>-V3 convolutional neural network has already learned from classifying a much
broader collection of images:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">sparklyr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">sparklyr.deeperer</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># NOTE: the correct spark_home path to use depends on the configuration of the</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Spark cluster you are working with.</span>
</span></span><span class="line"><span class="cl"><span class="n">spark_home</span> <span class="o">&lt;-</span> <span class="s">&#34;/usr/lib/spark&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">sc</span> <span class="o">&lt;-</span> <span class="nf">spark_connect</span><span class="p">(</span><span class="n">master</span> <span class="o">=</span> <span class="s">&#34;yarn&#34;</span><span class="p">,</span> <span class="n">spark_home</span> <span class="o">=</span> <span class="n">spark_home</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">data_dir</span> <span class="o">&lt;-</span> <span class="nf">copy_images_to_hdfs</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># extract features from train- and test-data</span>
</span></span><span class="line"><span class="cl"><span class="n">image_data</span> <span class="o">&lt;-</span> <span class="nf">list</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="kr">for</span> <span class="p">(</span><span class="n">x</span> <span class="kr">in</span> <span class="nf">c</span><span class="p">(</span><span class="s">&#34;train&#34;</span><span class="p">,</span> <span class="s">&#34;test&#34;</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># import</span>
</span></span><span class="line"><span class="cl">  <span class="n">image_data[[x]]</span> <span class="o">&lt;-</span> <span class="nf">c</span><span class="p">(</span><span class="s">&#34;dogs&#34;</span><span class="p">,</span> <span class="s">&#34;cats&#34;</span><span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">    <span class="nf">lapply</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="kr">function</span><span class="p">(</span><span class="n">label</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">numeric_label</span> <span class="o">&lt;-</span> <span class="nf">ifelse</span><span class="p">(</span><span class="nf">identical</span><span class="p">(</span><span class="n">label</span><span class="p">,</span> <span class="s">&#34;dogs&#34;</span><span class="p">),</span> <span class="m">1L</span><span class="p">,</span> <span class="m">0L</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nf">spark_read_image</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="n">sc</span><span class="p">,</span> <span class="n">dir</span> <span class="o">=</span> <span class="nf">file.path</span><span class="p">(</span><span class="n">data_dir</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">label</span><span class="p">,</span> <span class="n">fsep</span> <span class="o">=</span> <span class="s">&#34;/&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">          <span class="n">dplyr</span><span class="o">::</span><span class="nf">mutate</span><span class="p">(</span><span class="n">label</span> <span class="o">=</span> <span class="n">numeric_label</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">      <span class="nf">do.call</span><span class="p">(</span><span class="n">sdf_bind_rows</span><span class="p">,</span> <span class="n">.)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">dl_featurizer</span> <span class="o">&lt;-</span> <span class="nf">invoke_new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">sc</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s">&#34;com.databricks.sparkdl.DeepImageFeaturizer&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nf">random_string</span><span class="p">(</span><span class="s">&#34;dl_featurizer&#34;</span><span class="p">)</span> <span class="c1"># uid</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">    <span class="nf">invoke</span><span class="p">(</span><span class="s">&#34;setModelName&#34;</span><span class="p">,</span> <span class="s">&#34;InceptionV3&#34;</span><span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">    <span class="nf">invoke</span><span class="p">(</span><span class="s">&#34;setInputCol&#34;</span><span class="p">,</span> <span class="s">&#34;image&#34;</span><span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">    <span class="nf">invoke</span><span class="p">(</span><span class="s">&#34;setOutputCol&#34;</span><span class="p">,</span> <span class="s">&#34;features&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">image_data[[x]]</span> <span class="o">&lt;-</span>
</span></span><span class="line"><span class="cl">    <span class="n">dl_featurizer</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">    <span class="nf">invoke</span><span class="p">(</span><span class="s">&#34;transform&#34;</span><span class="p">,</span> <span class="nf">spark_dataframe</span><span class="p">(</span><span class="n">image_data[[x]]</span><span class="p">))</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">    <span class="nf">sdf_register</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>Third step: equipped with features that summarize the content of each image well, we can
build a Spark ML pipeline that recognizes cats and dogs using only logistic regression
<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">label_col</span> <span class="o">&lt;-</span> <span class="s">&#34;label&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">prediction_col</span> <span class="o">&lt;-</span> <span class="s">&#34;prediction&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">pipeline</span> <span class="o">&lt;-</span> <span class="nf">ml_pipeline</span><span class="p">(</span><span class="n">sc</span><span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">ml_logistic_regression</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">features_col</span> <span class="o">=</span> <span class="s">&#34;features&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">label_col</span> <span class="o">=</span> <span class="n">label_col</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">prediction_col</span> <span class="o">=</span> <span class="n">prediction_col</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">model</span> <span class="o">&lt;-</span> <span class="n">pipeline</span> <span class="o">%&gt;%</span> <span class="nf">ml_fit</span><span class="p">(</span><span class="n">image_data</span><span class="o">$</span><span class="n">train</span><span class="p">)</span></span></span></code></pre></div></div>
<p>Finally, we can evaluate the accuracy of this model on the test images:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">predictions</span> <span class="o">&lt;-</span> <span class="n">model</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">ml_transform</span><span class="p">(</span><span class="n">image_data</span><span class="o">$</span><span class="n">test</span><span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="n">dplyr</span><span class="o">::</span><span class="nf">compute</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">cat</span><span class="p">(</span><span class="s">&#34;Predictions vs. labels:\n&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">predictions</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="n">dplyr</span><span class="o">::</span><span class="nf">select</span><span class="p">(</span><span class="o">!!</span><span class="n">label_col</span><span class="p">,</span> <span class="o">!!</span><span class="n">prediction_col</span><span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">print</span><span class="p">(</span><span class="n">n</span> <span class="o">=</span> <span class="nf">sdf_nrow</span><span class="p">(</span><span class="n">predictions</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">cat</span><span class="p">(</span><span class="s">&#34;\nAccuracy of predictions:\n&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">predictions</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">ml_multiclass_classification_evaluator</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">label_col</span> <span class="o">=</span> <span class="n">label_col</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">prediction_col</span> <span class="o">=</span> <span class="n">prediction_col</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">metric_name</span> <span class="o">=</span> <span class="s">&#34;accuracy&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">    <span class="nf">print</span><span class="p">()</span></span></span></code></pre></div></div>
<pre><code>## Predictions vs. labels:
## # Source: spark&lt;?&gt; [?? x 2]
##    label prediction
##    &lt;int&gt;      &lt;dbl&gt;
##  1     1          1
##  2     1          1
##  3     1          1
##  4     1          1
##  5     1          1
##  6     1          1
##  7     1          1
##  8     1          1
##  9     1          1
## 10     1          1
## 11     0          0
## 12     0          0
## 13     0          0
## 14     0          0
## 15     0          0
## 16     0          0
## 17     0          0
## 18     0          0
## 19     0          0
## 20     0          0
##
## Accuracy of predictions:
## [1] 1
</code></pre>
<h2 id="new-spark_apply-capabilities">New <code>spark_apply()</code> capabilities
</h2>
<h3 id="optimizations--custom-serializers">Optimizations &amp; custom serializers
</h3>
<p>Many <code>sparklyr</code> users who have tried to run
<a href="https://spark.rstudio.com/reference/spark_apply.html" target="_blank" rel="noopener"><code>spark_apply()</code></a> or
<a href="https://opensource.posit.co/blog/2020-05-06_sparklyr-1-2/#foreach"><code>doSpark</code></a> to
parallelize R computations among Spark workers have probably encountered some
challenges arising from the serialization of R closures.
In some scenarios, the
serialized size of the R closure can become too large, often due to the size
of the enclosing R environment required by the closure. In other
scenarios, the serialization itself may take too much time, partially offsetting
the performance gain from parallelization. Recently, multiple optimizations went
into <code>sparklyr</code> to address those challenges. One of the optimizations was to
make good use of the
<a href="https://spark.apache.org/docs/latest/rdd-programming-guide.html#broadcast-variables" target="_blank" rel="noopener">broadcast variable</a>
construct in Apache Spark to reduce the overhead of distributing shared and
immutable task states across all Spark workers. In <code>sparklyr</code> 1.7, there is
also support for custom <code>spark_apply()</code> serializers, which offers more fine-grained
control over the trade-off between speed and compression level of serialization
algorithms. For example, one can specify</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">options</span><span class="p">(</span><span class="n">sparklyr.spark_apply.serializer</span> <span class="o">=</span> <span class="s">&#34;qs&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>,</p>
<p>which will apply the default options of <code>qs::qserialize()</code> to achieve a high
compression level, or</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">options</span><span class="p">(</span><span class="n">sparklyr.spark_apply.serializer</span> <span class="o">=</span> <span class="kr">function</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="n">qs</span><span class="o">::</span><span class="nf">qserialize</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">preset</span> <span class="o">=</span> <span class="s">&#34;fast&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="nf">options</span><span class="p">(</span><span class="n">sparklyr.spark_apply.deserializer</span> <span class="o">=</span> <span class="kr">function</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="n">qs</span><span class="o">::</span><span class="nf">qdeserialize</span><span class="p">(</span><span class="n">x</span><span class="p">))</span></span></span></code></pre></div></div>
<p>,</p>
<p>which will aim for faster serialization speed with less compression.</p>
<h3 id="inferring-dependencies-automatically">Inferring dependencies automatically
</h3>
<p>In <code>sparklyr</code> 1.7, <code>spark_apply()</code> also provides the experimental
<code>auto_deps = TRUE</code> option. With <code>auto_deps</code> enabled, <code>spark_apply()</code> will
examine the R closure being applied, infer the list of required R packages,
and only copy the required R packages and their transitive dependencies
to Spark workers. In many scenarios, the <code>auto_deps = TRUE</code> option will be a
significantly better alternative compared to the default <code>packages = TRUE</code>
behavior, which is to ship everything within <code>.libPaths()</code> to Spark worker
nodes, or the advanced <code>packages = &lt;package config&gt;</code> option, which requires
users to supply the list of required R packages or manually create a
<code>spark_apply()</code> bundle.</p>
<h2 id="better-integration-with-sparklyr-extensions">Better integration with sparklyr extensions
</h2>
<p>Substantial effort went into <code>sparklyr</code> 1.7 to make lives easier for <code>sparklyr</code>
extension authors. Experience suggests two areas where any <code>sparklyr</code> extension
can go through a frictional and non-straightforward path integrating with
<code>sparklyr</code> are the following:</p>
<ul>
<li>The <a href="https://github.com/sparklyr/sparklyr/blob/1242adb632c881f0a8dd234898af84a76614f590/R/dplyr_spark_connection.R#L184" target="_blank" rel="noopener"><code>dbplyr</code> SQL translation environment</a></li>
<li><a href="https://spark.rstudio.com/extensions/#calling-spark-from-r" target="_blank" rel="noopener">Invocation of Java/Scala functions from R</a></li>
</ul>
<p>We will elaborate on recent progress in both areas in the sub-sections below.</p>
<h3 id="customizing-the-dbplyr-sql-translation-environment">Customizing the <code>dbplyr</code> SQL translation environment
</h3>
<p><code>sparklyr</code> extensions can now customize <code>sparklyr</code>&rsquo;s <code>dbplyr</code> SQL translations
through the
<a href="https://spark.rstudio.com/reference/spark_dependency.html" target="_blank" rel="noopener"><code>spark_dependency()</code></a>
specification returned from <code>spark_dependencies()</code> callbacks.
This type of flexibility becomes useful, for instance, in scenarios where a
<code>sparklyr</code> extension needs to insert type casts for inputs to custom Spark
UDFs. We can find a concrete example of this in
<a href="https://github.com/apache/incubator-sedona/tree/master/R/sparklyr.sedona#sparklyrsedona" target="_blank" rel="noopener"><code>sparklyr.sedona</code></a>,
a <code>sparklyr</code> extension to facilitate geo-spatial analyses using
<a href="https://sedona.apache.org/" target="_blank" rel="noopener">Apache Sedona</a>. Geo-spatial UDFs supported by Apache
Sedona such as <code>ST_Point()</code> and <code>ST_PolygonFromEnvelope()</code> require all inputs to be
<code>DECIMAL(24, 20)</code> quantities rather than <code>DOUBLE</code>s. Without any customization to
<code>sparklyr</code>&rsquo;s <code>dbplyr</code> SQL variant, the only way for a <code>dplyr</code>
query involving <code>ST_Point()</code> to actually work in <code>sparklyr</code> would be to explicitly
implement any type cast needed by the query using <code>dplyr::sql()</code>, e.g.,</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">my_geospatial_sdf</span> <span class="o">&lt;-</span> <span class="n">my_geospatial_sdf</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="n">dplyr</span><span class="o">::</span><span class="nf">mutate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">x</span> <span class="o">=</span> <span class="n">dplyr</span><span class="o">::</span><span class="nf">sql</span><span class="p">(</span><span class="s">&#34;CAST(`x` AS DECIMAL(24, 20))&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">y</span> <span class="o">=</span> <span class="n">dplyr</span><span class="o">::</span><span class="nf">sql</span><span class="p">(</span><span class="s">&#34;CAST(`y` AS DECIMAL(24, 20))&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="n">dplyr</span><span class="o">::</span><span class="nf">mutate</span><span class="p">(</span><span class="n">pt</span> <span class="o">=</span> <span class="nf">ST_Point</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">))</span></span></span></code></pre></div></div>
<p>.</p>
<p>This would, to some extent, be antithetical to <code>dplyr</code>&rsquo;s goal of freeing R users from
laboriously spelling out SQL queries. Whereas by customizing <code>sparklyr</code>&rsquo;s <code>dplyr</code> SQL
translations (as implemented in
<a href="https://github.com/apache/incubator-sedona/blob/d8c2aae0678b7262660bda68eb0a2048b849e438/R/sparklyr.sedona/R/dependencies.R#L55" target="_blank" rel="noopener">here</a>
and
<a href="https://github.com/apache/incubator-sedona/blob/d8c2aae0678b7262660bda68eb0a2048b849e438/R/sparklyr.sedona/R/dependencies.R#L135" target="_blank" rel="noopener">here</a>
), <code>sparklyr.sedona</code> allows users to simply write</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">my_geospatial_sdf</span> <span class="o">&lt;-</span> <span class="n">my_geospatial_sdf</span> <span class="o">%&gt;%</span> <span class="n">dplyr</span><span class="o">::</span><span class="nf">mutate</span><span class="p">(</span><span class="n">pt</span> <span class="o">=</span> <span class="nf">ST_Point</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">))</span></span></span></code></pre></div></div>
<p>instead, and the required Spark SQL type casts are generated automatically.</p>
<h3 id="improved-interface-for-invoking-javascala-functions">Improved interface for invoking Java/Scala functions
</h3>
<p>In <code>sparklyr</code> 1.7, the R interface for Java/Scala invocations saw a number of
improvements.</p>
<p>With previous versions of <code>sparklyr</code>, many <code>sparklyr</code> extension authors would
run into trouble when attempting to invoke Java/Scala functions accepting an
<code>Array[T]</code> as one of their parameters, where <code>T</code> is any type bound more specific
than <code>java.lang.Object</code> / <code>AnyRef</code>. This was because any array of objects passed
through <code>sparklyr</code>&rsquo;s Java/Scala invocation interface will be interpreted as simply
an array of <code>java.lang.Object</code>s in absence of additional type information.
For this reason, a helper function
<a href="https://spark.rstudio.com/reference/jarray.html" target="_blank" rel="noopener"><code>jarray()</code></a> was implemented as
part of <code>sparklyr</code> 1.7 as a way to overcome the aforementioned problem.
For example, executing</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">sc</span> <span class="o">&lt;-</span> <span class="nf">spark_connect</span><span class="p">(</span><span class="kc">...</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">arr</span> <span class="o">&lt;-</span> <span class="nf">jarray</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">sc</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nf">seq</span><span class="p">(</span><span class="m">5</span><span class="p">)</span> <span class="o">%&gt;%</span> <span class="nf">lapply</span><span class="p">(</span><span class="kr">function</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="nf">invoke_new</span><span class="p">(</span><span class="n">sc</span><span class="p">,</span> <span class="s">&#34;MyClass&#34;</span><span class="p">,</span> <span class="n">x</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">  <span class="n">element_type</span> <span class="o">=</span> <span class="s">&#34;MyClass&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p>will assign to <code>arr</code> a <em>reference</em> to an <code>Array[MyClass]</code> of length 5, rather
than an <code>Array[AnyRef]</code>. Subsequently, <code>arr</code> becomes suitable to be passed as a
parameter to functions accepting only <code>Array[MyClass]</code>s as inputs. Previously,
some possible workarounds of this <code>sparklyr</code> limitation included changing
function signatures to accept <code>Array[AnyRef]</code>s instead of <code>Array[MyClass]</code>s, or
implementing a &ldquo;wrapped&rdquo; version of each function accepting <code>Array[AnyRef]</code>
inputs and converting them to <code>Array[MyClass]</code> before the actual invocation.
None of such workarounds was an ideal solution to the problem.</p>
<p>Another similar hurdle that was addressed in <code>sparklyr</code> 1.7 as well involves
function parameters that must be single-precision floating point numbers or
arrays of single-precision floating point numbers.
For those scenarios,
<a href="https://spark.rstudio.com/reference/jfloat.html" target="_blank" rel="noopener"><code>jfloat()</code></a> and
<a href="https://spark.rstudio.com/reference/jfloat_array.html" target="_blank" rel="noopener"><code>jfloat_array()</code></a>
are the helper functions that allow numeric quantities in R to be passed to
<code>sparklyr</code>&rsquo;s Java/Scala invocation interface as parameters with desired types.</p>
<p>In addition, while previous verisons of <code>sparklyr</code> failed to serialize
parameters with <code>NaN</code> values correctly, <code>sparklyr</code> 1.7 preserves <code>NaN</code>s as
expected in its Java/Scala invocation interface.</p>
<h2 id="other-exciting-news">Other exciting news
</h2>
<p>There are numerous other new features, enhancements, and bug fixes made to
<code>sparklyr</code> 1.7, all listed in the
<a href="https://github.com/sparklyr/sparklyr/blob/main/NEWS.md#sparklyr-170" target="_blank" rel="noopener">NEWS.md</a>
file of the <code>sparklyr</code> repo and documented in <code>sparklyr</code>&rsquo;s
<a href="https://spark.rstudio.com/reference/" target="_blank" rel="noopener">HTML reference</a> pages.
In the interest of brevity, we will not describe all of them in great detail
within this blog post.</p>
<h2 id="acknowledgement">Acknowledgement
</h2>
<p>In chronological order, we would like to thank the following individuals who
have authored or co-authored pull requests that were part of the <code>sparklyr</code> 1.7
release:</p>
<ul>
<li><a href="https://github.com/yitao-li" target="_blank" rel="noopener">@yitao-li</a></li>
<li><a href="https://github.com/mzorko" target="_blank" rel="noopener">@mzorko</a></li>
<li><a href="https://github.com/jozefhajnala" target="_blank" rel="noopener">@jozefhajnala</a></li>
<li><a href="https://github.com/lresende" target="_blank" rel="noopener">@lresende</a></li>
</ul>
<p>We&rsquo;re also extremely grateful to everyone who has submitted
feature requests or bug reports, many of which have been tremendously helpful in
shaping <code>sparklyr</code> into what it is today.</p>
<p>Furthermore, the author of this blog post is indebted to
<a href="https://github.com/skeydan" target="_blank" rel="noopener">@skeydan</a> for her awesome editorial suggestions.
Without her insights about good writing and story-telling, expositions like this
one would have been less readable.</p>
<p>If you wish to learn more about <code>sparklyr</code>, we recommend visiting
<a href="https://sparklyr.ai" target="_blank" rel="noopener">sparklyr.ai</a>, <a href="https://spark.rstudio.com" target="_blank" rel="noopener">spark.rstudio.com</a>,
and also reading some previous <code>sparklyr</code> release posts such as
<a href="https://opensource.posit.co/blog/2021-03-25_sparklyr-1.6/">sparklyr 1.6</a>
and
<a href="https://opensource.posit.co/blog/2020-12-14_sparklyr-1.5/">sparklyr 1.5</a>.</p>
<p>That is all. Thanks for reading!</p>
<p>Databricks, Inc. 2019. <em>Deep Learning Pipelines for Apache Spark</em>. V. 1.5.0. Released January 25. <a href="https://spark-packages.org/package/databricks/spark-deep-learning" target="_blank" rel="noopener">https://spark-packages.org/package/databricks/spark-deep-learning</a>.</p>
<p>Elson, Jeremy, John (JD) Douceur, Jon Howell, and Jared Saul. 2007. &ldquo;Asirra: A CAPTCHA That Exploits Interest-Aligned Manual Image Categorization.&rdquo; <em>Proceedings of 14th ACM Conference on Computer and Communications Security (CCS)</em>, Proceedings of 14th ACM Conference on Computer and Communications Security (CCS) Editions. <a href="https://www.microsoft.com/en-us/research/publication/asirra-a-captcha-that-exploits-interest-aligned-manual-image-categorization/" target="_blank" rel="noopener">https://www.microsoft.com/en-us/research/publication/asirra-a-captcha-that-exploits-interest-aligned-manual-image-categorization/</a>.</p>
<p>Szegedy, Christian, Wei Liu, Yangqing Jia, et al. 2015. &ldquo;Going Deeper with Convolutions.&rdquo; <em>Computer Vision and Pattern Recognition (CVPR)</em>. <a href="http://arxiv.org/abs/1409.4842" target="_blank" rel="noopener">http://arxiv.org/abs/1409.4842</a>.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Fun exercise for our readers: why not experiment with different subsets of cats-vs-dogs images for training
and testing, or even better, replace train and test images with your own images of cats and dogs, and see what
happens?&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Another way to see why it works: in fact the pre-built <code>Inception</code>-based feature
extractor simply applies all transformations <code>Inception</code> would have applied to its input,
except for the last logistic-regression-esque affine transformation plus non-linearity
producing the final categorical output, and <code>Inception</code> is a highly successful
convolutional neural network trained to recognize 1000 categories of animals and objects,
including multiple types of cats and dogs.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2021-07-06_sparklyr-1.7/thumbnail.png" length="7251" type="image/png" />
    </item>
    <item>
      <title>sparklyr 1.6: weighted quantile summaries, power iteration clustering, spark_write_rds(), and more</title>
      <link>https://opensource.posit.co/blog/2021-03-25_sparklyr-1.6/</link>
      <pubDate>Thu, 25 Mar 2021 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2021-03-25_sparklyr-1.6/</guid>
      <dc:creator>Yitao Li</dc:creator><description><![CDATA[<p><a href="https://sparklyr.ai" target="_blank" rel="noopener"><code>Sparklyr</code></a> 1.6 is now available on <a href="https://cran.r-project.org/web/packages/sparklyr/index.html" target="_blank" rel="noopener">CRAN</a>!</p>
<p>To install <code>sparklyr</code> 1.6 from CRAN, run</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;sparklyr&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>In this blog post, we shall highlight the following features and enhancements
from <code>sparklyr</code> 1.6:</p>
<ul>
<li><a href="#weighted-quantile-summaries">Weighted quantile summaries</a></li>
<li><a href="#power-iteration-clustering">Power iteration clustering</a></li>
<li><a href="#spark_write_rds-collect_from_rds"><code>spark_write_rds()</code> + <code>collect_from_rds()</code></a></li>
<li><a href="#dplyr-related-improvements">Dplyr-related improvements</a></li>
</ul>
<h2 id="weighted-quantile-summaries">Weighted quantile summaries
</h2>
<p><a href="https://spark.apache.org" target="_blank" rel="noopener">Apache Spark</a> is well-known for supporting
approximate algorithms that trade off marginal amounts of accuracy for greater
speed and parallelism.
Such algorithms are particularly beneficial for performing preliminary data
explorations at scale, as they enable users to quickly query certain estimated
statistics within a predefined error margin, while avoiding the high cost of
exact computations.
One example is the Greenwald-Khanna algorithm for on-line computation of quantile
summaries, as described in Greenwald and Khanna (2001).
This algorithm was originally designed for efficient $\epsilon$-
approximation of quantiles within a large dataset <em>without</em> the notion of data
points carrying different weights, and the unweighted version of it has been
implemented as
<a href="https://spark.apache.org/docs/2.0.0/api/java/org/apache/spark/sql/DataFrameStatFunctions.html#approxQuantile%28java.lang.String,%20double%5B%5D,%20double%29" target="_blank" rel="noopener"><code>approxQuantile()</code></a>
since Spark 2.0.
However, the same algorithm can be generalized to handle weighted
inputs, and as <code>sparklyr</code> user <a href="https://github.com/Zhuk66" target="_blank" rel="noopener">@Zhuk66</a> mentioned
in <a href="https://github.com/sparklyr/sparklyr/issues/2915" target="_blank" rel="noopener">this issue</a>, a
<a href="https://github.com/sparklyr/sparklyr/blob/4b6bc6677ecf92787ab3521f364a8d80b973d92f/java/spark-1.5.2/weightedquantilesummaries.scala#L13-L332" target="_blank" rel="noopener">weighted version</a>
of this algorithm makes for a useful <code>sparklyr</code> feature.</p>
<p>To properly explain what weighted-quantile means, we must clarify what the
weight of each data point signifies. For example, if we have a sequence of
observations $(1, 1, 1, 1, 0, 2, -1, -1)$, and would like to approximate the
median of all data points, then we have the following two options:</p>
<ul>
<li>
<p>Either run the unweighted version of <code>approxQuantile()</code> in Spark to scan
through all 8 data points</p>
</li>
<li>
<p>Or alternatively, &ldquo;compress&rdquo; the data into 4 tuples of (value, weight):
$(1, 0.5), (0, 0.125), (2, 0.125), (-1, 0.25)$, where the second component of
each tuple represents how often a value occurs relative to the rest of the
observed values, and then find the median by scanning through the 4 tuples
using the weighted version of the Greenwald-Khanna algorithm</p>
</li>
</ul>
<p>We can also run through a contrived example involving the standard normal
distribution to illustrate the power of weighted quantile estimation in
<code>sparklyr</code> 1.6. Suppose we cannot simply run <code>qnorm()</code> in R to evaluate the
<a href="https://en.wikipedia.org/wiki/Normal_distribution#Quantile_function" target="_blank" rel="noopener">quantile function</a>
of the standard normal distribution at $p = 0.25$ and $p = 0.75$, how can
we get some vague idea about the 1st and 3rd quantiles of this distribution?
One way is to sample a large number of data points from this distribution, and
then apply the Greenwald-Khanna algorithm to our unweighted samples, as shown
below:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">sparklyr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">sc</span> <span class="o">&lt;-</span> <span class="nf">spark_connect</span><span class="p">(</span><span class="n">master</span> <span class="o">=</span> <span class="s">&#34;local&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">num_samples</span> <span class="o">&lt;-</span> <span class="m">1e6</span>
</span></span><span class="line"><span class="cl"><span class="n">samples</span> <span class="o">&lt;-</span> <span class="nf">data.frame</span><span class="p">(</span><span class="n">x</span> <span class="o">=</span> <span class="nf">rnorm</span><span class="p">(</span><span class="n">num_samples</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">samples_sdf</span> <span class="o">&lt;-</span> <span class="nf">copy_to</span><span class="p">(</span><span class="n">sc</span><span class="p">,</span> <span class="n">samples</span><span class="p">,</span> <span class="n">name</span> <span class="o">=</span> <span class="nf">random_string</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">samples_sdf</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">sdf_quantile</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">column</span> <span class="o">=</span> <span class="s">&#34;x&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">probabilities</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="m">0.25</span><span class="p">,</span> <span class="m">0.75</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">relative.error</span> <span class="o">=</span> <span class="m">0.01</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">print</span><span class="p">()</span></span></span></code></pre></div></div>
<pre><code>##        25%        75%
## -0.6629242  0.6874939
</code></pre>
<p>Notice that because we are working with an approximate algorithm, and have specified
<code>relative.error = 0.01</code>, the estimated value of $-0.6629242$ from above
could be anywhere between the 24th and the 26th percentile of all samples.
In fact, it falls in the $25.36896$-th percentile:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">pnorm</span><span class="p">(</span><span class="m">-0.6629242</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code>## [1] 0.2536896
</code></pre>
<p>Now how can we make use of weighted quantile estimation from <code>sparklyr</code> 1.6 to
obtain similar results? Simple! We can sample a large number of $x$ values
uniformly randomly from $(-\infty, \infty)$ (or alternatively, just select a
large number of values evenly spaced between $(-M, M)$ where $M$ is
approximately $\infty$), and assign each $x$ value a weight of
$\displaystyle \frac{1}{\sqrt{2 \pi}}e^{-\frac{x^2}{2}}$, the standard normal
distribution&rsquo;s probability density at $x$. Finally, we run the weighted version
of <code>sdf_quantile()</code> from <code>sparklyr</code> 1.6, as shown below:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">sparklyr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">sc</span> <span class="o">&lt;-</span> <span class="nf">spark_connect</span><span class="p">(</span><span class="n">master</span> <span class="o">=</span> <span class="s">&#34;local&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">num_samples</span> <span class="o">&lt;-</span> <span class="m">1e6</span>
</span></span><span class="line"><span class="cl"><span class="n">M</span> <span class="o">&lt;-</span> <span class="m">1000</span>
</span></span><span class="line"><span class="cl"><span class="n">samples</span> <span class="o">&lt;-</span> <span class="n">tibble</span><span class="o">::</span><span class="nf">tibble</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">x</span> <span class="o">=</span> <span class="n">M</span> <span class="o">*</span> <span class="nf">seq</span><span class="p">(</span><span class="o">-</span><span class="n">num_samples</span> <span class="o">/</span> <span class="m">2</span> <span class="o">+</span> <span class="m">1</span><span class="p">,</span> <span class="n">num_samples</span> <span class="o">/</span> <span class="m">2</span><span class="p">)</span> <span class="o">/</span> <span class="n">num_samples</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">weight</span> <span class="o">=</span> <span class="nf">dnorm</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">samples_sdf</span> <span class="o">&lt;-</span> <span class="nf">copy_to</span><span class="p">(</span><span class="n">sc</span><span class="p">,</span> <span class="n">samples</span><span class="p">,</span> <span class="n">name</span> <span class="o">=</span> <span class="nf">random_string</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">samples_sdf</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">sdf_quantile</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">column</span> <span class="o">=</span> <span class="s">&#34;x&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">weight.column</span> <span class="o">=</span> <span class="s">&#34;weight&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">probabilities</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="m">0.25</span><span class="p">,</span> <span class="m">0.75</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">relative.error</span> <span class="o">=</span> <span class="m">0.01</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">print</span><span class="p">()</span></span></span></code></pre></div></div>
<pre><code>##    25%    75%
## -0.696  0.662
</code></pre>
<p>Voilà! The estimates are not too far off from the 25th and 75th percentiles (in
relation to our abovementioned maximum permissible error of $0.01$):</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">pnorm</span><span class="p">(</span><span class="m">-0.696</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code>## [1] 0.2432144
</code></pre>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">pnorm</span><span class="p">(</span><span class="m">0.662</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code>## [1] 0.7460144
</code></pre>
<h2 id="power-iteration-clustering">Power iteration clustering
</h2>
<p>Power iteration clustering (PIC), a simple and scalable graph clustering method
presented in Lin and Cohen (2010), first finds a low-dimensional embedding of a dataset, using
truncated power iteration on a normalized pairwise-similarity matrix of all data
points, and then uses this embedding as the &ldquo;cluster indicator&rdquo;, an intermediate
representation of the dataset that leads to fast convergence when used as input
to k-means clustering. This process is very well illustrated in figure 1
of Lin and Cohen (2010) (reproduced below)</p>
<img src="https://opensource.posit.co/blog/2021-03-25_sparklyr-1.6/images/PIC.png" width="612" />
<p>in which the leftmost image is the visualization of a dataset consisting of 3
circles, with points colored in red, green, and blue indicating clustering
results, and the subsequent images show the power iteration process gradually
transforming the original set of points into what appears to be three disjoint line
segments, an intermediate representation that can be rapidly separated into 3
clusters using k-means clustering with $k = 3$.</p>
<p>In <code>sparklyr</code> 1.6, <code>ml_power_iteration()</code> was implemented to make the
<a href="http://spark.apache.org/docs/latest/api/java/org/apache/spark/ml/clustering/PowerIterationClustering.html" target="_blank" rel="noopener">PIC functionality</a>
in Spark accessible from R. It expects as input a 3-column Spark dataframe that
represents a pairwise-similarity matrix of all data points. Two of
the columns in this dataframe should contain 0-based row and column indices, and
the third column should hold the corresponding similarity measure.
In the example below, we will see a dataset consisting of two circles being
easily separated into two clusters by <code>ml_power_iteration()</code>, with the Gaussian
kernel being used as the similarity measure between any 2 points:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">gen_similarity_matrix</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Guassian similarity measure</span>
</span></span><span class="line"><span class="cl">  <span class="n">guassian_similarity</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">pt1</span><span class="p">,</span> <span class="n">pt2</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">exp</span><span class="p">(</span><span class="o">-</span><span class="nf">sum</span><span class="p">((</span><span class="n">pt2</span> <span class="o">-</span> <span class="n">pt1</span><span class="p">)</span> <span class="n">^</span> <span class="m">2</span><span class="p">)</span> <span class="o">/</span> <span class="m">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># generate evenly distributed points on a circle centered at the origin</span>
</span></span><span class="line"><span class="cl">  <span class="n">gen_circle</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">radius</span><span class="p">,</span> <span class="n">num_pts</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">seq</span><span class="p">(</span><span class="m">0</span><span class="p">,</span> <span class="n">num_pts</span> <span class="o">-</span> <span class="m">1</span><span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">      <span class="n">purrr</span><span class="o">::</span><span class="nf">map_dfr</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="kr">function</span><span class="p">(</span><span class="n">idx</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="n">theta</span> <span class="o">&lt;-</span> <span class="m">2</span> <span class="o">*</span> <span class="kc">pi</span> <span class="o">*</span> <span class="n">idx</span> <span class="o">/</span> <span class="n">num_pts</span>
</span></span><span class="line"><span class="cl">          <span class="n">radius</span> <span class="o">*</span> <span class="nf">c</span><span class="p">(</span><span class="n">x</span> <span class="o">=</span> <span class="nf">cos</span><span class="p">(</span><span class="n">theta</span><span class="p">),</span> <span class="n">y</span> <span class="o">=</span> <span class="nf">sin</span><span class="p">(</span><span class="n">theta</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">        <span class="p">})</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># generate points on both circles</span>
</span></span><span class="line"><span class="cl">  <span class="n">pts</span> <span class="o">&lt;-</span> <span class="nf">rbind</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="nf">gen_circle</span><span class="p">(</span><span class="n">radius</span> <span class="o">=</span> <span class="m">1</span><span class="p">,</span> <span class="n">num_pts</span> <span class="o">=</span> <span class="m">80</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="nf">gen_circle</span><span class="p">(</span><span class="n">radius</span> <span class="o">=</span> <span class="m">4</span><span class="p">,</span> <span class="n">num_pts</span> <span class="o">=</span> <span class="m">80</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># populate the pairwise similarity matrix (stored as a 3-column dataframe)</span>
</span></span><span class="line"><span class="cl">  <span class="n">similarity_matrix</span> <span class="o">&lt;-</span> <span class="nf">data.frame</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="kr">for</span> <span class="p">(</span><span class="n">i</span> <span class="kr">in</span> <span class="nf">seq</span><span class="p">(</span><span class="m">2</span><span class="p">,</span> <span class="nf">nrow</span><span class="p">(</span><span class="n">pts</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl">    <span class="n">similarity_matrix</span> <span class="o">&lt;-</span> <span class="n">similarity_matrix</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">      <span class="nf">rbind</span><span class="p">(</span><span class="nf">seq</span><span class="p">(</span><span class="n">i</span> <span class="o">-</span> <span class="m">1L</span><span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">        <span class="n">purrr</span><span class="o">::</span><span class="nf">map_dfr</span><span class="p">(</span><span class="o">~</span> <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="n">src</span> <span class="o">=</span> <span class="n">i</span> <span class="o">-</span> <span class="m">1L</span><span class="p">,</span> <span class="n">dst</span> <span class="o">=</span> <span class="n">.x</span> <span class="o">-</span> <span class="m">1L</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">similarity</span> <span class="o">=</span> <span class="nf">guassian_similarity</span><span class="p">(</span><span class="n">pts[i</span><span class="p">,</span><span class="n">]</span><span class="p">,</span> <span class="n">pts[.x</span><span class="p">,</span><span class="n">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="p">))</span>
</span></span><span class="line"><span class="cl">      <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">similarity_matrix</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">sparklyr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">sc</span> <span class="o">&lt;-</span> <span class="nf">spark_connect</span><span class="p">(</span><span class="n">master</span> <span class="o">=</span> <span class="s">&#34;local&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">sdf</span> <span class="o">&lt;-</span> <span class="nf">copy_to</span><span class="p">(</span><span class="n">sc</span><span class="p">,</span> <span class="nf">gen_similarity_matrix</span><span class="p">())</span>
</span></span><span class="line"><span class="cl"><span class="n">clusters</span> <span class="o">&lt;-</span> <span class="nf">ml_power_iteration</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">sdf</span><span class="p">,</span> <span class="n">k</span> <span class="o">=</span> <span class="m">2</span><span class="p">,</span> <span class="n">max_iter</span> <span class="o">=</span> <span class="m">10</span><span class="p">,</span> <span class="n">init_mode</span> <span class="o">=</span> <span class="s">&#34;degree&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">src_col</span> <span class="o">=</span> <span class="s">&#34;src&#34;</span><span class="p">,</span> <span class="n">dst_col</span> <span class="o">=</span> <span class="s">&#34;dst&#34;</span><span class="p">,</span> <span class="n">weight_col</span> <span class="o">=</span> <span class="s">&#34;similarity&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">clusters</span> <span class="o">%&gt;%</span> <span class="nf">print</span><span class="p">(</span><span class="n">n</span> <span class="o">=</span> <span class="m">160</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code>## # A tibble: 160 x 2
##        id cluster
##     &lt;dbl&gt;   &lt;int&gt;
##   1     0       1
##   2     1       1
##   3     2       1
##   4     3       1
##   5     4       1
##   ...
##   157   156       0
##   158   157       0
##   159   158       0
##   160   159       0
</code></pre>
<p>The output shows points from the two circles being assigned to separate clusters,
as expected, after only a small number of PIC iterations.</p>
<h2 id="spark_write_rds--collect_from_rds"><code>spark_write_rds()</code> + <code>collect_from_rds()</code>
</h2>
<p><code>spark_write_rds()</code> and <code>collect_from_rds()</code> are implemented as a less memory-
consuming alternative to <code>collect()</code>. Unlike <code>collect()</code>, which retrieves all
elements of a Spark dataframe through the Spark driver node, hence potentially
causing slowness or out-of-memory failures when collecting large amounts of data,
<code>spark_write_rds()</code>, when used in conjunction with <code>collect_from_rds()</code>, can
retrieve all partitions of a Spark dataframe directly from Spark workers,
rather than through the Spark driver node.
First, <code>spark_write_rds()</code> will
distribute the tasks of serializing Spark dataframe partitions in RDS version
2 format among Spark workers. Spark workers can then process multiple partitions
in parallel, each handling one partition at a time and persisting the RDS output
directly to disk, rather than sending dataframe partitions to the Spark driver
node. Finally, the RDS outputs can be re-assembled to R dataframes using
<code>collect_from_rds()</code>.</p>
<p>Shown below is an example of <code>spark_write_rds()</code> + <code>collect_from_rds()</code> usage,
where RDS outputs are first saved to HDFS, then downloaded to the local
filesystem with <code>hadoop fs -get</code>, and finally, post-processed with
<code>collect_from_rds()</code>:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">sparklyr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">nycflights13</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">num_partitions</span> <span class="o">&lt;-</span> <span class="m">10L</span>
</span></span><span class="line"><span class="cl"><span class="n">sc</span> <span class="o">&lt;-</span> <span class="nf">spark_connect</span><span class="p">(</span><span class="n">master</span> <span class="o">=</span> <span class="s">&#34;yarn&#34;</span><span class="p">,</span> <span class="n">spark_home</span> <span class="o">=</span> <span class="s">&#34;/usr/lib/spark&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">flights_sdf</span> <span class="o">&lt;-</span> <span class="nf">copy_to</span><span class="p">(</span><span class="n">sc</span><span class="p">,</span> <span class="n">flights</span><span class="p">,</span> <span class="n">repartition</span> <span class="o">=</span> <span class="n">num_partitions</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Spark workers serialize all partition in RDS format in parallel and write RDS</span>
</span></span><span class="line"><span class="cl"><span class="c1"># outputs to HDFS</span>
</span></span><span class="line"><span class="cl"><span class="nf">spark_write_rds</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">flights_sdf</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">dest_uri</span> <span class="o">=</span> <span class="s">&#34;hdfs://&lt;namenode&gt;:8020/flights-part-{partitionId}.rds&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run `hadoop fs -get` to download RDS files from HDFS to local file system</span>
</span></span><span class="line"><span class="cl"><span class="kr">for</span> <span class="p">(</span><span class="n">partition</span> <span class="kr">in</span> <span class="nf">seq</span><span class="p">(</span><span class="n">num_partitions</span><span class="p">)</span> <span class="o">-</span> <span class="m">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="nf">system2</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s">&#34;hadoop&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nf">c</span><span class="p">(</span><span class="s">&#34;fs&#34;</span><span class="p">,</span> <span class="s">&#34;-get&#34;</span><span class="p">,</span> <span class="nf">sprintf</span><span class="p">(</span><span class="s">&#34;hdfs://&lt;namenode&gt;:8020/flights-part-%d.rds&#34;</span><span class="p">,</span> <span class="n">partition</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Post-process RDS outputs</span>
</span></span><span class="line"><span class="cl"><span class="n">partitions</span> <span class="o">&lt;-</span> <span class="nf">seq</span><span class="p">(</span><span class="n">num_partitions</span><span class="p">)</span> <span class="o">-</span> <span class="m">1</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">lapply</span><span class="p">(</span><span class="kr">function</span><span class="p">(</span><span class="n">partition</span><span class="p">)</span> <span class="nf">collect_from_rds</span><span class="p">(</span><span class="nf">sprintf</span><span class="p">(</span><span class="s">&#34;flights-part-%d.rds&#34;</span><span class="p">,</span> <span class="n">partition</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Optionally, call `rbind()` to combine data from all partitions into a single R dataframe</span>
</span></span><span class="line"><span class="cl"><span class="n">flights_df</span> <span class="o">&lt;-</span> <span class="nf">do.call</span><span class="p">(</span><span class="n">rbind</span><span class="p">,</span> <span class="n">partitions</span><span class="p">)</span></span></span></code></pre></div></div>
<h2 id="dplyr-related-improvements">Dplyr-related improvements
</h2>
<p>Similar to other recent <code>sparklyr</code> releases, <code>sparklyr</code> 1.6 comes with a
number of dplyr-related improvements, such as</p>
<ul>
<li>Support for <code>where()</code> predicate within <code>select()</code> and <code>summarize(across(...))</code>
operations on Spark dataframes</li>
<li>Addition of <code>if_all()</code> and <code>if_any()</code> functions</li>
<li>Full compatibility with <code>dbplyr</code> 2.0 backend API</li>
</ul>
<h3 id="selectwhere-and-summarizeacrosswhere"><code>select(where(...))</code> and <code>summarize(across(where(...)))</code>
</h3>
<p>The dplyr <code>where(...)</code> construct is useful for applying a selection or
aggregation function to multiple columns that satisfy some boolean predicate.
For example,</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">dplyr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">iris</span> <span class="o">%&gt;%</span> <span class="nf">select</span><span class="p">(</span><span class="nf">where</span><span class="p">(</span><span class="n">is.numeric</span><span class="p">))</span></span></span></code></pre></div></div>
<p>returns all numeric columns from the <code>iris</code> dataset, and</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">dplyr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">iris</span> <span class="o">%&gt;%</span> <span class="nf">summarize</span><span class="p">(</span><span class="nf">across</span><span class="p">(</span><span class="nf">where</span><span class="p">(</span><span class="n">is.numeric</span><span class="p">),</span> <span class="n">mean</span><span class="p">))</span></span></span></code></pre></div></div>
<p>computes the average of each numeric column.</p>
<p>In sparklyr 1.6, both types of operations can be applied to Spark dataframes, e.g.,</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">dplyr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">sparklyr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">sc</span> <span class="o">&lt;-</span> <span class="nf">spark_connect</span><span class="p">(</span><span class="n">master</span> <span class="o">=</span> <span class="s">&#34;local&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">iris_sdf</span> <span class="o">&lt;-</span> <span class="nf">copy_to</span><span class="p">(</span><span class="n">sc</span><span class="p">,</span> <span class="n">iris</span><span class="p">,</span> <span class="n">name</span> <span class="o">=</span> <span class="nf">random_string</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">iris_sdf</span> <span class="o">%&gt;%</span> <span class="nf">select</span><span class="p">(</span><span class="nf">where</span><span class="p">(</span><span class="n">is.numeric</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">iris</span> <span class="o">%&gt;%</span> <span class="nf">summarize</span><span class="p">(</span><span class="nf">across</span><span class="p">(</span><span class="nf">where</span><span class="p">(</span><span class="n">is.numeric</span><span class="p">),</span> <span class="n">mean</span><span class="p">))</span></span></span></code></pre></div></div>
<h3 id="if_all-and-if_any"><code>if_all()</code> and <code>if_any()</code>
</h3>
<p><code>if_all()</code> and <code>if_any()</code> are two convenience functions from <code>dplyr</code> 1.0.4 (see
<a href="https://opensource.posit.co/blog/2021-02-02_dplyr-1-0-4-if-any/">here</a> for more details)
that effectively <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>
combine the results of applying a boolean predicate to a tidy selection of columns
using the logical <code>and</code>/<code>or</code> operators.</p>
<p>Starting from sparklyr 1.6, <code>if_all()</code> and <code>if_any()</code> can also be applied to
Spark dataframes, .e.g.,</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">dplyr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">sparklyr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">sc</span> <span class="o">&lt;-</span> <span class="nf">spark_connect</span><span class="p">(</span><span class="n">master</span> <span class="o">=</span> <span class="s">&#34;local&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">iris_sdf</span> <span class="o">&lt;-</span> <span class="nf">copy_to</span><span class="p">(</span><span class="n">sc</span><span class="p">,</span> <span class="n">iris</span><span class="p">,</span> <span class="n">name</span> <span class="o">=</span> <span class="nf">random_string</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Select all records with Petal.Width &gt; 2 and Petal.Length &gt; 2</span>
</span></span><span class="line"><span class="cl"><span class="n">iris_sdf</span> <span class="o">%&gt;%</span> <span class="nf">filter</span><span class="p">(</span><span class="nf">if_all</span><span class="p">(</span><span class="nf">starts_with</span><span class="p">(</span><span class="s">&#34;Petal&#34;</span><span class="p">),</span> <span class="o">~</span> <span class="n">.x</span> <span class="o">&gt;</span> <span class="m">2</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Select all records with Petal.Width &gt; 5 or Petal.Length &gt; 5</span>
</span></span><span class="line"><span class="cl"><span class="n">iris_sdf</span> <span class="o">%&gt;%</span> <span class="nf">filter</span><span class="p">(</span><span class="nf">if_any</span><span class="p">(</span><span class="nf">starts_with</span><span class="p">(</span><span class="s">&#34;Petal&#34;</span><span class="p">),</span> <span class="o">~</span> <span class="n">.x</span> <span class="o">&gt;</span> <span class="m">5</span><span class="p">))</span></span></span></code></pre></div></div>
<h3 id="compatibility-with-dbplyr-20-backend-api">Compatibility with <code>dbplyr</code> 2.0 backend API
</h3>
<p><code>Sparklyr</code> 1.6 is fully compatible with the newer <code>dbplyr</code> 2.0 backend API (by
implementing all interface changes recommended in
<a href="https://dbplyr.tidyverse.org/articles/backend-2.html" target="_blank" rel="noopener">here</a>), while still
maintaining backward compatibility with the previous edition of <code>dbplyr</code> API, so
that <code>sparklyr</code> users will not be forced to switch to any particular version of
<code>dbplyr</code>.</p>
<p>This should be a mostly non-user-visible change as of now. In fact, the only
discernible behavior change will be the following code</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">dbplyr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">sparklyr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">sc</span> <span class="o">&lt;-</span> <span class="nf">spark_connect</span><span class="p">(</span><span class="n">master</span> <span class="o">=</span> <span class="s">&#34;local&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">print</span><span class="p">(</span><span class="nf">dbplyr_edition</span><span class="p">(</span><span class="n">sc</span><span class="p">))</span></span></span></code></pre></div></div>
<p>outputting</p>
<pre><code>[1] 2
</code></pre>
<p>if <code>sparklyr</code> is working with <code>dbplyr</code> 2.0+, and</p>
<pre><code>[1] 1
</code></pre>
<p>if otherwise.</p>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>In chronological order, we would like to thank the following contributors for
making <code>sparklyr</code> 1.6 awesome:</p>
<ul>
<li><a href="https://github.com/yitao-li" target="_blank" rel="noopener">@yitao-li</a></li>
<li><a href="https://github.com/pgramme" target="_blank" rel="noopener">@pgramme</a></li>
<li><a href="https://github.com/javierluraschi" target="_blank" rel="noopener">@javierluraschi</a></li>
<li><a href="https://github.com/andrew-christianson" target="_blank" rel="noopener">@andrew-christianson</a></li>
<li><a href="https://github.com/jozefhajnala" target="_blank" rel="noopener">@jozefhajnala</a></li>
<li><a href="https://github.com/nathaneastwood" target="_blank" rel="noopener">@nathaneastwood</a></li>
<li><a href="https://github.com/mzorko" target="_blank" rel="noopener">@mzorko</a></li>
</ul>
<p>We would also like to give a big shout-out to the wonderful open-source community
behind <code>sparklyr</code>, without whom we would not have benefitted from numerous
<code>sparklyr</code>-related bug reports and feature suggestions.</p>
<p>Finally, the author of this blog post also very much appreciates the highly
valuable editorial suggestions from <a href="https://github.com/skeydan" target="_blank" rel="noopener">@skeydan</a>.</p>
<p>If you wish to learn more about <code>sparklyr</code>, we recommend checking out
<a href="https://sparklyr.ai" target="_blank" rel="noopener">sparklyr.ai</a>, <a href="https://spark.rstudio.com" target="_blank" rel="noopener">spark.rstudio.com</a>,
and also some previous <code>sparklyr</code> release posts such as
<a href="https://opensource.posit.co/blog/2020-12-14_sparklyr-1.5/">sparklyr 1.5</a>
and <a href="https://opensource.posit.co/blog/2020-09-30_sparklyr-1.4/">sparklyr 1.4</a>.</p>
<p>That is all. Thanks for reading!</p>
<p>Greenwald, Michael, and Sanjeev Khanna. 2001. &ldquo;Space-Efficient Online Computation of Quantile Summaries.&rdquo; <em>SIGMOD Rec.</em> (New York, NY, USA) 30 (2): 58&ndash;66. <a href="https://doi.org/10.1145/376284.375670" target="_blank" rel="noopener">https://doi.org/10.1145/376284.375670</a>.</p>
<p>Lin, Frank, and William Cohen. 2010. &ldquo;Power Iteration Clustering.&rdquo; August, 655&ndash;62.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>modulo possible implementation-dependent short-circuit evaluations&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2021-03-25_sparklyr-1.6/thumbnail.jpg" length="89031" type="image/jpeg" />
    </item>
    <item>
      <title>sparklyr 1.5: better dplyr interface, more sdf_* functions, and RDS-based serialization routines</title>
      <link>https://opensource.posit.co/blog/2020-12-14_sparklyr-1.5/</link>
      <pubDate>Mon, 14 Dec 2020 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2020-12-14_sparklyr-1.5/</guid>
      <dc:creator>Yitao Li</dc:creator><description><![CDATA[<p>We are thrilled to announce <a href="https://sparklyr.ai" target="_blank" rel="noopener"><code>sparklyr</code></a> 1.5 is now
available on <a href="https://cran.r-project.org/web/packages/sparklyr/index.html" target="_blank" rel="noopener">CRAN</a>!</p>
<p>To install <code>sparklyr</code> 1.5 from CRAN, run</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;sparklyr&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>In this blog post, we will highlight the following aspects of <code>sparklyr</code> 1.5:</p>
<ul>
<li><a href="#better-dplyr-interface">Better <code>dplyr</code> interface</a></li>
<li><a href="#new-additions-to-the-sdf_-family-of-functions">4 useful additions to the <code>sdf_*</code> family of functions</a></li>
<li>New <a href="#rds-based-serialization-routines">RDS-based serialization routines</a> along with several serialization-related improvements and bug fixes</li>
</ul>
<h2 id="better-dplyr-interface">Better dplyr interface
</h2>
<p>A large fraction of pull requests that went into the <code>sparklyr</code> 1.5 release were focused on making
Spark dataframes work with various <code>dplyr</code> verbs in the same way that R dataframes do.
The full list of <code>dplyr</code>-related bugs and feature requests that were resolved in
<code>sparklyr</code> 1.5 can be found in <a href="https://github.com/sparklyr/sparklyr/issues?q=is%3Aissue&#43;is%3Aclosed&#43;label%3Adplyr&#43;milestone%3A1.5.0" target="_blank" rel="noopener">here</a>.</p>
<p>In this section, we will showcase three new dplyr functionalities that were shipped with <code>sparklyr</code> 1.5.</p>
<h3 id="stratified-sampling">Stratified sampling
</h3>
<p>Stratified sampling on an R dataframe can be accomplished with a combination of <code>dplyr::group_by()</code> followed by
<code>dplyr::sample_n()</code> or <code>dplyr::sample_frac()</code>, where the grouping variables specified in the <code>dplyr::group_by()</code>
step are the ones that define each stratum. For instance, the following query will group <code>mtcars</code> by number
of cylinders and return a weighted random sample of size two from each group, without replacement, and weighted by
the <code>mpg</code> column:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">mtcars</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="n">dplyr</span><span class="o">::</span><span class="nf">group_by</span><span class="p">(</span><span class="n">cyl</span><span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="n">dplyr</span><span class="o">::</span><span class="nf">sample_n</span><span class="p">(</span><span class="n">size</span> <span class="o">=</span> <span class="m">2</span><span class="p">,</span> <span class="n">weight</span> <span class="o">=</span> <span class="n">mpg</span><span class="p">,</span> <span class="n">replace</span> <span class="o">=</span> <span class="kc">FALSE</span><span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">print</span><span class="p">()</span></span></span></code></pre></div></div>
<pre><code>## # A tibble: 6 x 11
## # Groups:   cyl [3]
##     mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb
##   &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt;
## 1  33.9     4  71.1    65  4.22  1.84  19.9     1     1     4     1
## 2  22.8     4 108      93  3.85  2.32  18.6     1     1     4     1
## 3  21.4     6 258     110  3.08  3.22  19.4     1     0     3     1
## 4  21       6 160     110  3.9   2.62  16.5     0     1     4     4
## 5  15.5     8 318     150  2.76  3.52  16.9     0     0     3     2
## 6  19.2     8 400     175  3.08  3.84  17.0     0     0     3     2
</code></pre>
<p>Starting from <code>sparklyr</code> 1.5, the same can also be done for Spark dataframes with Spark 3.0 or above, e.g.,:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">sparklyr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">sc</span> <span class="o">&lt;-</span> <span class="nf">spark_connect</span><span class="p">(</span><span class="n">master</span> <span class="o">=</span> <span class="s">&#34;local&#34;</span><span class="p">,</span> <span class="n">version</span> <span class="o">=</span> <span class="s">&#34;3.0.0&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">mtcars_sdf</span> <span class="o">&lt;-</span> <span class="nf">copy_to</span><span class="p">(</span><span class="n">sc</span><span class="p">,</span> <span class="n">mtcars</span><span class="p">,</span> <span class="n">replace</span> <span class="o">=</span> <span class="kc">TRUE</span><span class="p">,</span> <span class="n">repartition</span> <span class="o">=</span> <span class="m">3</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">mtcars_sdf</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="n">dplyr</span><span class="o">::</span><span class="nf">group_by</span><span class="p">(</span><span class="n">cyl</span><span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="n">dplyr</span><span class="o">::</span><span class="nf">sample_n</span><span class="p">(</span><span class="n">size</span> <span class="o">=</span> <span class="m">2</span><span class="p">,</span> <span class="n">weight</span> <span class="o">=</span> <span class="n">mpg</span><span class="p">,</span> <span class="n">replace</span> <span class="o">=</span> <span class="kc">FALSE</span><span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">print</span><span class="p">()</span></span></span></code></pre></div></div>
<pre><code># Source: spark&lt;?&gt; [?? x 11]
# Groups: cyl
    mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb
  &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt;
1  21       6 160     110  3.9   2.62  16.5     0     1     4     4
2  21.4     6 258     110  3.08  3.22  19.4     1     0     3     1
3  27.3     4  79      66  4.08  1.94  18.9     1     1     4     1
4  32.4     4  78.7    66  4.08  2.2   19.5     1     1     4     1
5  16.4     8 276.    180  3.07  4.07  17.4     0     0     3     3
6  18.7     8 360     175  3.15  3.44  17.0     0     0     3     2
</code></pre>
<p>or</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">mtcars_sdf</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="n">dplyr</span><span class="o">::</span><span class="nf">group_by</span><span class="p">(</span><span class="n">cyl</span><span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="n">dplyr</span><span class="o">::</span><span class="nf">sample_frac</span><span class="p">(</span><span class="n">size</span> <span class="o">=</span> <span class="m">0.2</span><span class="p">,</span> <span class="n">weight</span> <span class="o">=</span> <span class="n">mpg</span><span class="p">,</span> <span class="n">replace</span> <span class="o">=</span> <span class="kc">FALSE</span><span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">print</span><span class="p">()</span></span></span></code></pre></div></div>
<pre><code>## # Source: spark&lt;?&gt; [?? x 11]
## # Groups: cyl
##     mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb
##   &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt;
## 1  21       6 160     110  3.9   2.62  16.5     0     1     4     4
## 2  21.4     6 258     110  3.08  3.22  19.4     1     0     3     1
## 3  22.8     4 141.     95  3.92  3.15  22.9     1     0     4     2
## 4  33.9     4  71.1    65  4.22  1.84  19.9     1     1     4     1
## 5  30.4     4  95.1   113  3.77  1.51  16.9     1     1     5     2
## 6  15.5     8 318     150  2.76  3.52  16.9     0     0     3     2
## 7  18.7     8 360     175  3.15  3.44  17.0     0     0     3     2
## 8  16.4     8 276.    180  3.07  4.07  17.4     0     0     3     3
</code></pre>
<h3 id="row-sums">Row sums
</h3>
<p>The <code>rowSums()</code> functionality offered by <code>dplyr</code> is handy when one needs to sum up
a large number of columns within an R dataframe that are impractical to be enumerated
individually.
For example, here we have a six-column dataframe of random real numbers, where the
<code>partial_sum</code> column in the result contains the sum of columns <code>b</code> through <code>d</code> within
each row:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">ncols</span> <span class="o">&lt;-</span> <span class="m">6</span>
</span></span><span class="line"><span class="cl"><span class="n">nums</span> <span class="o">&lt;-</span> <span class="nf">seq</span><span class="p">(</span><span class="n">ncols</span><span class="p">)</span> <span class="o">%&gt;%</span> <span class="nf">lapply</span><span class="p">(</span><span class="kr">function</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="nf">runif</span><span class="p">(</span><span class="m">5</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="nf">names</span><span class="p">(</span><span class="n">nums</span><span class="p">)</span> <span class="o">&lt;-</span> <span class="kc">letters</span><span class="n">[1</span><span class="o">:</span><span class="n">ncols]</span>
</span></span><span class="line"><span class="cl"><span class="n">tbl</span> <span class="o">&lt;-</span> <span class="n">tibble</span><span class="o">::</span><span class="nf">as_tibble</span><span class="p">(</span><span class="n">nums</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">tbl</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="n">dplyr</span><span class="o">::</span><span class="nf">mutate</span><span class="p">(</span><span class="n">partial_sum</span> <span class="o">=</span> <span class="nf">rowSums</span><span class="p">(</span><span class="n">.[2</span><span class="o">:</span><span class="m">5</span><span class="n">]</span><span class="p">))</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">print</span><span class="p">()</span></span></span></code></pre></div></div>
<pre><code>## # A tibble: 5 x 7
##         a     b     c      d     e      f partial_sum
##     &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt;  &lt;dbl&gt; &lt;dbl&gt;  &lt;dbl&gt;       &lt;dbl&gt;
## 1 0.781   0.801 0.157 0.0293 0.169 0.0978        1.16
## 2 0.696   0.412 0.221 0.941  0.697 0.675         2.27
## 3 0.802   0.410 0.516 0.923  0.190 0.904         2.04
## 4 0.200   0.590 0.755 0.494  0.273 0.807         2.11
## 5 0.00149 0.711 0.286 0.297  0.107 0.425         1.40
</code></pre>
<p>Beginning with <code>sparklyr</code> 1.5, the same operation can be performed with Spark dataframes:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">sparklyr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">sc</span> <span class="o">&lt;-</span> <span class="nf">spark_connect</span><span class="p">(</span><span class="n">master</span> <span class="o">=</span> <span class="s">&#34;local&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">sdf</span> <span class="o">&lt;-</span> <span class="nf">copy_to</span><span class="p">(</span><span class="n">sc</span><span class="p">,</span> <span class="n">tbl</span><span class="p">,</span> <span class="n">overwrite</span> <span class="o">=</span> <span class="kc">TRUE</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">sdf</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="n">dplyr</span><span class="o">::</span><span class="nf">mutate</span><span class="p">(</span><span class="n">partial_sum</span> <span class="o">=</span> <span class="nf">rowSums</span><span class="p">(</span><span class="n">.[2</span><span class="o">:</span><span class="m">5</span><span class="n">]</span><span class="p">))</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">print</span><span class="p">()</span></span></span></code></pre></div></div>
<pre><code>## # Source: spark&lt;?&gt; [?? x 7]
##         a     b     c      d     e      f partial_sum
##     &lt;dbl&gt; &lt;dbl&gt; &lt;dbl&gt;  &lt;dbl&gt; &lt;dbl&gt;  &lt;dbl&gt;       &lt;dbl&gt;
## 1 0.781   0.801 0.157 0.0293 0.169 0.0978        1.16
## 2 0.696   0.412 0.221 0.941  0.697 0.675         2.27
## 3 0.802   0.410 0.516 0.923  0.190 0.904         2.04
## 4 0.200   0.590 0.755 0.494  0.273 0.807         2.11
## 5 0.00149 0.711 0.286 0.297  0.107 0.425         1.40
</code></pre>
<p>As a bonus from implementing the <code>rowSums</code> feature for Spark dataframes,
<code>sparklyr</code> 1.5 now also offers limited support for the column-subsetting
operator on Spark dataframes.
For example, all code snippets below will return some subset of columns from
the dataframe named <code>sdf</code>:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># select columns `b` through `e`</span>
</span></span><span class="line"><span class="cl"><span class="n">sdf[2</span><span class="o">:</span><span class="m">5</span><span class="n">]</span></span></span></code></pre></div></div>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># select columns `b` and `c`</span>
</span></span><span class="line"><span class="cl"><span class="n">sdf</span><span class="nf">[c</span><span class="p">(</span><span class="s">&#34;b&#34;</span><span class="p">,</span> <span class="s">&#34;c&#34;</span><span class="p">)</span><span class="n">]</span></span></span></code></pre></div></div>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># drop the first and third columns and return the rest</span>
</span></span><span class="line"><span class="cl"><span class="n">sdf</span><span class="nf">[c</span><span class="p">(</span><span class="m">-1</span><span class="p">,</span> <span class="m">-3</span><span class="p">)</span><span class="n">]</span></span></span></code></pre></div></div>
<h3 id="weighted-mean-summarizer">Weighted-mean summarizer
</h3>
<p>Similar to the two <code>dplyr</code> functions mentioned above, the <code>weighted.mean()</code> summarizer is another
useful function that has become part of the <code>dplyr</code> interface for Spark dataframes in <code>sparklyr</code> 1.5.
One can see it in action by, for example, comparing the output from the following</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">sparklyr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">sc</span> <span class="o">&lt;-</span> <span class="nf">spark_connect</span><span class="p">(</span><span class="n">master</span> <span class="o">=</span> <span class="s">&#34;local&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">mtcars_sdf</span> <span class="o">&lt;-</span> <span class="nf">copy_to</span><span class="p">(</span><span class="n">sc</span><span class="p">,</span> <span class="n">mtcars</span><span class="p">,</span> <span class="n">replace</span> <span class="o">=</span> <span class="kc">TRUE</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">mtcars_sdf</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="n">dplyr</span><span class="o">::</span><span class="nf">group_by</span><span class="p">(</span><span class="n">cyl</span><span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="n">dplyr</span><span class="o">::</span><span class="nf">summarize</span><span class="p">(</span><span class="n">mpg_wm</span> <span class="o">=</span> <span class="nf">weighted.mean</span><span class="p">(</span><span class="n">mpg</span><span class="p">,</span> <span class="n">wt</span><span class="p">))</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">print</span><span class="p">()</span></span></span></code></pre></div></div>
<p>with output from the equivalent operation on <code>mtcars</code> in R:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">mtcars</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="n">dplyr</span><span class="o">::</span><span class="nf">group_by</span><span class="p">(</span><span class="n">cyl</span><span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="n">dplyr</span><span class="o">::</span><span class="nf">summarize</span><span class="p">(</span><span class="n">mpg_wm</span> <span class="o">=</span> <span class="nf">weighted.mean</span><span class="p">(</span><span class="n">mpg</span><span class="p">,</span> <span class="n">wt</span><span class="p">))</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">print</span><span class="p">()</span></span></span></code></pre></div></div>
<p>both of them should evaluate to the following:</p>
<pre><code>##     cyl mpg_wm
##   &lt;dbl&gt;  &lt;dbl&gt;
## 1     4   25.9
## 2     6   19.6
## 3     8   14.8
</code></pre>
<h2 id="new-additions-to-the-sdf_-family-of-functions">New additions to the <code>sdf_*</code> family of functions
</h2>
<p><code>sparklyr</code> provides a large number of convenience functions for working with Spark dataframes,
and all of them have names starting with the <code>sdf_</code> prefix.</p>
<p>In this section we will briefly mention four new additions
and show some example scenarios in which those functions are useful.</p>
<h3 id="sdf_expand_grid"><code>sdf_expand_grid()</code>
</h3>
<p>As the name suggests, <code>sdf_expand_grid()</code> is simply the Spark equivalent of <code>expand.grid()</code>.
Rather than running <code>expand.grid()</code> in R and importing the resulting R dataframe to Spark, one
can now run <code>sdf_expand_grid()</code>, which accepts both R vectors and Spark dataframes and supports
hints for broadcast hash joins. The example below shows <code>sdf_expand_grid()</code> creating a
100-by-100-by-10-by-10 grid in Spark over 1000 Spark partitions, with broadcast hash join hints
on variables with small cardinalities:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">sparklyr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">sc</span> <span class="o">&lt;-</span> <span class="nf">spark_connect</span><span class="p">(</span><span class="n">master</span> <span class="o">=</span> <span class="s">&#34;local&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">grid_sdf</span> <span class="o">&lt;-</span> <span class="nf">sdf_expand_grid</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">sc</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">var1</span> <span class="o">=</span> <span class="nf">seq</span><span class="p">(</span><span class="m">100</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">var2</span> <span class="o">=</span> <span class="nf">seq</span><span class="p">(</span><span class="m">100</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">var3</span> <span class="o">=</span> <span class="nf">seq</span><span class="p">(</span><span class="m">10</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">var4</span> <span class="o">=</span> <span class="nf">seq</span><span class="p">(</span><span class="m">10</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">broadcast_vars</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="n">var3</span><span class="p">,</span> <span class="n">var4</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">repartition</span> <span class="o">=</span> <span class="m">1000</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">grid_sdf</span> <span class="o">%&gt;%</span> <span class="nf">sdf_nrow</span><span class="p">()</span> <span class="o">%&gt;%</span> <span class="nf">print</span><span class="p">()</span></span></span></code></pre></div></div>
<pre><code>## [1] 1e+06
</code></pre>
<h3 id="sdf_partition_sizes"><code>sdf_partition_sizes()</code>
</h3>
<p>As <code>sparklyr</code> user <a href="https://github.com/sbottelli" target="_blank" rel="noopener">@sbottelli</a> suggested <a href="https://github.com/sparklyr/sparklyr/issues/2791" target="_blank" rel="noopener">here</a>,
one thing that would be great to have in <code>sparklyr</code> is an efficient way to query partition sizes of a Spark dataframe.
In <code>sparklyr</code> 1.5, <code>sdf_partition_sizes()</code> does exactly that:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">sparklyr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">sc</span> <span class="o">&lt;-</span> <span class="nf">spark_connect</span><span class="p">(</span><span class="n">master</span> <span class="o">=</span> <span class="s">&#34;local&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">sdf_len</span><span class="p">(</span><span class="n">sc</span><span class="p">,</span> <span class="m">1000</span><span class="p">,</span> <span class="n">repartition</span> <span class="o">=</span> <span class="m">5</span><span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">sdf_partition_sizes</span><span class="p">()</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">print</span><span class="p">(</span><span class="n">row.names</span> <span class="o">=</span> <span class="kc">FALSE</span><span class="p">)</span></span></span></code></pre></div></div>
<pre><code>##  partition_index partition_size
##                0            200
##                1            200
##                2            200
##                3            200
##                4            200
</code></pre>
<h3 id="sdf_unnest_longer-and-sdf_unnest_wider"><code>sdf_unnest_longer()</code> and <code>sdf_unnest_wider()</code>
</h3>
<p><code>sdf_unnest_longer()</code> and <code>sdf_unnest_wider()</code> are the equivalents of
<code>tidyr::unnest_longer()</code> and <code>tidyr::unnest_wider()</code> for Spark dataframes.
<code>sdf_unnest_longer()</code> expands all elements in a struct column into multiple rows, and
<code>sdf_unnest_wider()</code> expands them into multiple columns. As illustrated with an example
dataframe below,</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">sparklyr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">sc</span> <span class="o">&lt;-</span> <span class="nf">spark_connect</span><span class="p">(</span><span class="n">master</span> <span class="o">=</span> <span class="s">&#34;local&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">sdf</span> <span class="o">&lt;-</span> <span class="nf">copy_to</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">sc</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">tibble</span><span class="o">::</span><span class="nf">tibble</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">id</span> <span class="o">=</span> <span class="nf">seq</span><span class="p">(</span><span class="m">3</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">attribute</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="nf">list</span><span class="p">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">&#34;Alice&#34;</span><span class="p">,</span> <span class="n">grade</span> <span class="o">=</span> <span class="s">&#34;A&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="nf">list</span><span class="p">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">&#34;Bob&#34;</span><span class="p">,</span> <span class="n">grade</span> <span class="o">=</span> <span class="s">&#34;B&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="nf">list</span><span class="p">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">&#34;Carol&#34;</span><span class="p">,</span> <span class="n">grade</span> <span class="o">=</span> <span class="s">&#34;C&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">sdf</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">sdf_unnest_longer</span><span class="p">(</span><span class="n">col</span> <span class="o">=</span> <span class="n">record</span><span class="p">,</span> <span class="n">indices_to</span> <span class="o">=</span> <span class="s">&#34;key&#34;</span><span class="p">,</span> <span class="n">values_to</span> <span class="o">=</span> <span class="s">&#34;value&#34;</span><span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">print</span><span class="p">()</span></span></span></code></pre></div></div>
<p>evaluates to</p>
<pre><code>## # Source: spark&lt;?&gt; [?? x 3]
##      id value key
##   &lt;int&gt; &lt;chr&gt; &lt;chr&gt;
## 1     1 A     grade
## 2     1 Alice name
## 3     2 B     grade
## 4     2 Bob   name
## 5     3 C     grade
## 6     3 Carol name
</code></pre>
<p>whereas</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">sdf</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">sdf_unnest_wider</span><span class="p">(</span><span class="n">col</span> <span class="o">=</span> <span class="n">record</span><span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">  <span class="nf">print</span><span class="p">()</span></span></span></code></pre></div></div>
<p>evaluates to</p>
<pre><code>## # Source: spark&lt;?&gt; [?? x 3]
##      id grade name
##   &lt;int&gt; &lt;chr&gt; &lt;chr&gt;
## 1     1 A     Alice
## 2     2 B     Bob
## 3     3 C     Carol
</code></pre>
<h2 id="rds-based-serialization-routines">RDS-based serialization routines
</h2>
<p>Some readers must be wondering why a brand new serialization format would need to be implemented in <code>sparklyr</code> at all.
Long story short, the reason is that RDS serialization is a strictly better replacement for its CSV predecessor.
It possesses all desirable attributes the CSV format has,
while avoiding a number of disadvantages that are common among text-based data formats.</p>
<p>In this section, we will briefly outline why <code>sparklyr</code> should support at least one serialization format other than <code>arrow</code>,
deep-dive into issues with CSV-based serialization,
and then show how the new RDS-based serialization is free from those issues.</p>
<h3 id="why-arrow-is-not-for-everyone">Why <code>arrow</code> is not for everyone?
</h3>
<p>To transfer data between Spark and R correctly and efficiently, <code>sparklyr</code> must rely on some data serialization
format that is well-supported by both Spark and R.
Unfortunately, not many serialization formats satisfy this requirement,
and among the ones that do are text-based formats such as CSV and JSON,
and binary formats such as Apache Arrow, Protobuf, and as of recent, a small subset of RDS version 2.
Further complicating the matter is the additional consideration that
<code>sparklyr</code> should support at least one serialization format whose implementation can be fully self-contained within the <code>sparklyr</code> code base,
i.e., such serialization should not depend on any external R package or system library,
so that it can accommodate users who want to use <code>sparklyr</code> but who do not necessarily have the required C++ compiler tool chain and
other system dependencies for setting up R packages such as <a href="https://cran.r-project.org/web/packages/arrow/index.html" target="_blank" rel="noopener"><code>arrow</code></a> or
<a href="https://cran.r-project.org/web/packages/protolite/index.html" target="_blank" rel="noopener"><code>protolite</code></a>.
Prior to <code>sparklyr</code> 1.5, CSV-based serialization was the default alternative to fallback to when users do not have the <code>arrow</code> package installed or
when the type of data being transported from R to Spark is unsupported by the version of <code>arrow</code> available.</p>
<h3 id="why-is-the-csv-format-not-ideal">Why is the CSV format not ideal?
</h3>
<p>There are at least three reasons to believe CSV format is not the best choice when it comes to exporting data from R to Spark.</p>
<p>One reason is efficiency. For example, a double-precision floating point number such as <code>.Machine$double.eps</code> needs to
be expressed as <code>&quot;2.22044604925031e-16&quot;</code> in CSV format in order to not incur any loss of precision, thus taking up 20 bytes
rather than 8 bytes.</p>
<p>But more important than efficiency are correctness concerns. In a R dataframe, one can store both <code>NA_real_</code> and
<code>NaN</code> in a column of floating point numbers. <code>NA_real_</code> should ideally translate to <code>null</code> within a Spark dataframe, whereas
<code>NaN</code> should continue to be <code>NaN</code> when transported from R to Spark. Unfortunately, <code>NA_real_</code> in R becomes indistinguishable
from <code>NaN</code> once serialized in CSV format, as evident from a quick demo shown below:</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">original_df</span> <span class="o">&lt;-</span> <span class="nf">data.frame</span><span class="p">(</span><span class="n">x</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="kc">NA_real_</span><span class="p">,</span> <span class="kc">NaN</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="n">original_df</span> <span class="o">%&gt;%</span> <span class="n">dplyr</span><span class="o">::</span><span class="nf">mutate</span><span class="p">(</span><span class="n">is_nan</span> <span class="o">=</span> <span class="nf">is.nan</span><span class="p">(</span><span class="n">x</span><span class="p">))</span> <span class="o">%&gt;%</span> <span class="nf">print</span><span class="p">()</span></span></span></code></pre></div></div>
<pre><code>##     x is_nan
## 1  NA  FALSE
## 2 NaN   TRUE
</code></pre>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">csv_file</span> <span class="o">&lt;-</span> <span class="s">&#34;/tmp/data.csv&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nf">write.csv</span><span class="p">(</span><span class="n">original_df</span><span class="p">,</span> <span class="n">file</span> <span class="o">=</span> <span class="n">csv_file</span><span class="p">,</span> <span class="n">row.names</span> <span class="o">=</span> <span class="kc">FALSE</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">deserialized_df</span> <span class="o">&lt;-</span> <span class="nf">read.csv</span><span class="p">(</span><span class="n">csv_file</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">deserialized_df</span> <span class="o">%&gt;%</span> <span class="n">dplyr</span><span class="o">::</span><span class="nf">mutate</span><span class="p">(</span><span class="n">is_nan</span> <span class="o">=</span> <span class="nf">is.nan</span><span class="p">(</span><span class="n">x</span><span class="p">))</span> <span class="o">%&gt;%</span> <span class="nf">print</span><span class="p">()</span></span></span></code></pre></div></div>
<pre><code>##    x is_nan
## 1 NA  FALSE
## 2 NA  FALSE
</code></pre>
<p>Another correctness issue very much similar to the one above was the fact that
<code>&quot;NA&quot;</code> and <code>NA</code> within a string column of an R dataframe become indistinguishable
once serialized in CSV format, as correctly pointed out in
<a href="https://github.com/sparklyr/sparklyr/issues/2031" target="_blank" rel="noopener">this Github issue</a>
by <a href="https://github.com/caewok" target="_blank" rel="noopener">@caewok</a> and others.</p>
<h3 id="rds-to-the-rescue">RDS to the rescue!
</h3>
<p>RDS format is one of the most widely used binary formats for serializing R objects.
It is described in some detail in chapter 1, section 8 of
<a href="https://cran.r-project.org/doc/manuals/r-patched/R-ints.pdf" target="_blank" rel="noopener">this document</a>.
Among advantages of the RDS format are efficiency and accuracy: it has a reasonably
efficient implementation in base R, and supports all R data types.</p>
<p>Also worth noticing is the fact that when an R dataframe containing only data types
with sensible equivalents in Apache Spark (e.g., <code>RAWSXP</code>, <code>LGLSXP</code>, <code>CHARSXP</code>, <code>REALSXP</code>, etc)
is saved using RDS version 2,
(e.g., <code>serialize(mtcars, connection = NULL, version = 2L, xdr = TRUE)</code>),
only a tiny subset of the RDS format will be involved in the serialization process,
and implementing deserialization routines in Scala capable of decoding such a restricted
subset of RDS constructs is in fact a reasonably simple and straightforward task
(as shown in
<a href="https://github.com/sparklyr/sparklyr/blob/5e27668f16faa4852deae2db14828cfd1614c982/java/spark-1.5.2/rutils.scala#L47" target="_blank" rel="noopener">here</a>
).</p>
<p>Last but not least, because RDS is a binary format, it allows <code>NA_character_</code>, <code>&quot;NA&quot;</code>,
<code>NA_real_</code>, and <code>NaN</code> to all be encoded in an unambiguous manner, hence allowing <code>sparklyr</code>
1.5 to avoid all correctness issues detailed above in non-<code>arrow</code> serialization use cases.</p>
<h3 id="other-benefits-of-rds-serialization">Other benefits of RDS serialization
</h3>
<p>In addition to correctness guarantees, RDS format also offers quite a few other advantages.</p>
<p>One advantage is of course performance: for example, importing a non-trivially-sized dataset
such as <code>nycflights13::flights</code> from R to Spark using the RDS format in sparklyr 1.5 is
roughly 40%-50% faster compared to CSV-based serialization in sparklyr 1.4. The
current RDS-based implementation is still nowhere as fast as <code>arrow</code>-based serialization
though (<code>arrow</code> is about 3-4x faster), so for performance-sensitive tasks involving
heavy serialization, <code>arrow</code> should still be the top choice.</p>
<p>Another advantage is that with RDS serialization, <code>sparklyr</code> can import R dataframes containing
<code>raw</code> columns directly into binary columns in Spark. Thus, use cases such as the one below
will work in <code>sparklyr</code> 1.5</p>
<div class="code-block"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">sparklyr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">sc</span> <span class="o">&lt;-</span> <span class="nf">spark_connect</span><span class="p">(</span><span class="n">master</span> <span class="o">=</span> <span class="s">&#34;local&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">tbl</span> <span class="o">&lt;-</span> <span class="n">tibble</span><span class="o">::</span><span class="nf">tibble</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">x</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span><span class="nf">serialize</span><span class="p">(</span><span class="s">&#34;sparklyr&#34;</span><span class="p">,</span> <span class="kc">NULL</span><span class="p">),</span> <span class="nf">serialize</span><span class="p">(</span><span class="nf">c</span><span class="p">(</span><span class="m">123456</span><span class="p">,</span> <span class="m">789</span><span class="p">),</span> <span class="kc">NULL</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">sdf</span> <span class="o">&lt;-</span> <span class="nf">copy_to</span><span class="p">(</span><span class="n">sc</span><span class="p">,</span> <span class="n">tbl</span><span class="p">)</span></span></span></code></pre></div></div>
<p>While most <code>sparklyr</code> users probably won&rsquo;t find this capability of importing binary columns
to Spark immediately useful in their typical <code>sparklyr::copy_to()</code> or <code>sparklyr::collect()</code>
usages, it does play a crucial role in reducing serialization overheads in the Spark-based
<a href="https://opensource.posit.co/blog/2020-05-06_sparklyr-1-2/#foreach"><code>foreach</code></a> parallel backend that
was first introduced in <code>sparklyr</code> 1.2.
This is because Spark workers can directly fetch the serialized R closures to be computed
from a binary Spark column instead of extracting those serialized bytes from intermediate
representations such as base64-encoded strings.
Similarly, the R results from executing worker closures will be directly available in RDS
format which can be efficiently deserialized in R, rather than being delivered in other
less efficient formats.</p>
<h2 id="acknowledgement">Acknowledgement
</h2>
<p>In chronological order, we would like to thank the following contributors for making their pull
requests part of <code>sparklyr</code> 1.5:</p>
<ul>
<li><a href="https://github.com/wkdavis" target="_blank" rel="noopener">@wkdavis</a></li>
<li><a href="https://github.com/yitao-li" target="_blank" rel="noopener">@yitao-li</a></li>
<li><a href="https://github.com/falaki" target="_blank" rel="noopener">@falaki</a></li>
<li><a href="https://github.com/nathaneastwood" target="_blank" rel="noopener">@nathaneastwood</a></li>
<li><a href="https://github.com/pgramme" target="_blank" rel="noopener">@pgramme</a></li>
</ul>
<p>We would also like to express our gratitude towards numerous bug reports and feature requests for
<code>sparklyr</code> from a fantastic open-source community.</p>
<p>Finally, the author of this blog post is indebted to
<a href="https://github.com/javierluraschi" target="_blank" rel="noopener">@javierluraschi</a>,
<a href="https://github.com/batpigandme" target="_blank" rel="noopener">@batpigandme</a>,
and <a href="https://github.com/skeydan" target="_blank" rel="noopener">@skeydan</a> for their valuable editorial inputs.</p>
<p>If you wish to learn more about <code>sparklyr</code>, check out <a href="https://sparklyr.ai" target="_blank" rel="noopener">sparklyr.ai</a>,
<a href="https://spark.rstudio.com" target="_blank" rel="noopener">spark.rstudio.com</a>, and some of the previous release posts such as
<a href="https://opensource.posit.co/blog/2020-09-30_sparklyr-1.4/">sparklyr 1.4</a> and
<a href="https://opensource.posit.co/blog/2020-07-16_sparklyr-1-3/">sparklyr 1.3</a>.</p>
<p>Thanks for reading!</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2020-12-14_sparklyr-1.5/thumbnail.jpg" length="752491" type="image/jpeg" />
    </item>
  </channel>
</rss>
