<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>R-bloggers</title>
	<atom:link href="https://www.r-bloggers.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.r-bloggers.com</link>
	<description>R news and tutorials contributed by hundreds of R bloggers</description>
	<lastBuildDate>Thu, 15 Oct 2026 03:00:00 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=5.5.23</generator>

<image>
	<url>https://i0.wp.com/www.r-bloggers.com/wp-content/uploads/2016/08/cropped-R_single_01-200.png?fit=32%2C32&#038;ssl=1</url>
	<title>R-bloggers</title>
	<link>https://www.r-bloggers.com</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">11524731</site>	<item>
		<title>maestro 1.4.0 adds custom pipeline metrics</title>
		<link>https://www.r-bloggers.com/2026/10/maestro-1-4-0-adds-custom-pipeline-metrics/</link>
		
		<dc:creator><![CDATA[Will Hipson]]></dc:creator>
		<pubDate>Thu, 15 Oct 2026 03:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://whipson.github.io/data-in-flight/posts/maestro-1-4-0/maestro-1-4-0-release.html</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>The 1.4.0 release of maestro is out, and with it comes the ability to record custom metrics from your pipelines. Execution time and success/failure were already tracked, but there was no good way to answer questions like “how many rows did ...</p></div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/maestro-1-4-0-adds-custom-pipeline-metrics/">maestro 1.4.0 adds custom pipeline metrics</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://whipson.github.io/data-in-flight/posts/maestro-1-4-0/maestro-1-4-0-release.html"> data-in-flight</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
 





<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://i1.wp.com/whipson.github.io/data-in-flight/posts/maestro-1-4-0/images/clipboard-2853204278.png?w=578&#038;ssl=1" class="img-fluid quarto-figure quarto-figure-center figure-img" data-recalc-dims="1"></p>
</figure>
</div>
<p>The 1.4.0 release of maestro is out, and with it comes the ability to record custom metrics from your pipelines. Execution time and success/failure were already tracked, but there was no good way to answer questions like “how many rows did this pipeline process?” or “how many API calls did it take?” Now there is.</p>
<p>If you haven’t heard of maestro, it’s a pipeline orchestration package. You can learn more about it <a href="https://whipson.github.io/maestro/index.html" rel="nofollow" target="_blank">here</a>.</p>
<p>Get it from CRAN:</p>
<div class="cell">
<pre>install.packages(&quot;maestro&quot;)</pre>
</div>
<section id="recording-metrics-with-update_metric" class="level2">
<h2 class="anchored" data-anchor-id="recording-metrics-with-update_metric">Recording metrics with <code>update_metric()</code></h2>
<p>A metric is any value that can be represented as a single number: rows processed, bytes read, API calls made, attempts on a task. To record one, call <code>update_metric()</code> from inside a pipeline with the name of the metric and its value. maestro automatically attributes the metric to the pipeline (and run) that is currently executing.</p>
<p>An important design point is that metrics are recorded on the side. They are not part of a pipeline’s return value, so they never get passed downstream in a <a href="https://whipson.github.io/maestro/articles/maestro-4-directed-acyclic-graphs.html" rel="nofollow" target="_blank">DAG</a>.</p>
<p>Here’s a two-step DAG that extracts some flight data and loads it, recording a few metrics along the way:</p>
<div class="cell">
<pre>#' ./pipelines/metric_example.R
#' @maestroFrequency 1 hour
extract_flights &lt;- function() {
  
  # Pretends to fetch data from an API
  df &lt;- data.frame(
    flight_id = 1:5,
    airline_code = c(&quot;UA&quot;, &quot;AC&quot;, &quot;AC&quot;, &quot;AA&quot;, &quot;DE&quot;),
    departure_time = as.POSIXct(&quot;2026-10-14 12:00:00&quot;) + c(100, 450, 750, 1450, 1750)
  )
  
  update_metric(&quot;rows_read&quot;, nrow(df))
  update_metric(&quot;bytes_read&quot;, as.numeric(object.size(df)))
  
  df
}

#' @maestroInputs extract_flights
load_flights &lt;- function(.input) {
  
  write.csv(.input, tempfile(fileext = &quot;.csv&quot;))
  
  update_metric(&quot;rows_written&quot;, nrow(.input))
}</pre>
</div>
</section>
<section id="retrieving-metrics-with-get_metrics" class="level2">
<h2 class="anchored" data-anchor-id="retrieving-metrics-with-get_metrics">Retrieving metrics with <code>get_metrics()</code></h2>
<p>After running the schedule, <code>get_metrics()</code> returns a long data.frame of every metric recorded. Each row gives the pipeline it came from (<code>pipe_name</code>), the run it was recorded in (<code>run_id</code>), the <code>metric</code> name, and its numeric <code>value</code>.</p>
<div class="cell">
<pre>library(maestro)

schedule &lt;- build_schedule(quiet = TRUE)

status &lt;- run_schedule(
  schedule,
  orch_frequency = &quot;1 hour&quot;,
  quiet = TRUE
)

get_metrics(schedule)</pre>
<div class="cell-output cell-output-stdout">
<pre># A tibble: 3 × 4
  pipe_name       run_id metric       value
  &lt;chr&gt;           &lt;chr&gt;  &lt;chr&gt;        &lt;dbl&gt;
1 extract_flights vPnJoC rows_read        5
2 extract_flights vPnJoC bytes_read    1648
3 load_flights    ulLl5m rows_written     5</pre>
</div>
</div>
<p>Notice that each pipeline gets its own <code>run_id</code>, even within a DAG. Because the result is a plain data.frame, it’s easy to summarise further or write out to a database or file, which makes it a natural fit for monitoring pipeline health over time.</p>
<p>A couple of other things to know:</p>
<ul>
<li>Metrics are only recorded when a pipeline is executed by <code>run_schedule()</code>. Calling <code>update_metric()</code> while developing or testing a pipeline function interactively is harmless: it emits an informational message and does nothing.</li>
<li>Execution time and success/failure are already available via <code>get_status()</code> and <code>get_schedule()</code>, so there’s no need to record those yourself.</li>
</ul>
</section>
<section id="metrics-are-per-execution" class="level2">
<h2 class="anchored" data-anchor-id="metrics-are-per-execution">Metrics are per execution</h2>
<p>Metrics belong to a single execution of a pipeline. Each time a pipeline runs it gets a new <code>run_id</code>, and its metrics are stored separately from those of earlier runs. Within one execution, calling <code>update_metric()</code> again with the same name overwrites the earlier value (the last write wins).</p>
<p>Running the schedule a second time shows this: the metrics from both runs are retained and distinguished by <code>run_id</code>.</p>
<div class="cell">
<pre>status &lt;- run_schedule(
  schedule,
  orch_frequency = &quot;1 hour&quot;,
  quiet = TRUE
)

get_metrics(schedule)</pre>
<div class="cell-output cell-output-stdout">
<pre># A tibble: 6 × 4
  pipe_name       run_id metric       value
  &lt;chr&gt;           &lt;chr&gt;  &lt;chr&gt;        &lt;dbl&gt;
1 extract_flights vPnJoC rows_read        5
2 extract_flights vPnJoC bytes_read    1648
3 extract_flights bilN7s rows_read        5
4 extract_flights bilN7s bytes_read    1648
5 load_flights    ulLl5m rows_written     5
6 load_flights    R9iMP0 rows_written     5</pre>
</div>
</div>
</section>
<section id="accumulating-with-increment-true" class="level2">
<h2 class="anchored" data-anchor-id="accumulating-with-increment-true">Accumulating with <code>increment = TRUE</code></h2>
<p>Sometimes a metric is built up over the course of a pipeline rather than computed once, like counting API calls inside a loop or the number of attempts on a flaky task. Setting <code>increment = TRUE</code> adds <code>value</code> to the metric’s current value instead of overwriting it. If the metric hasn’t been set yet in the current run, the first call simply sets it. A negative <code>value</code> decrements.</p>
<div class="cell">
<pre>#' ./pipelines/metric_increment.R
#' @maestroFrequency 1 day
fetch_paginated &lt;- function() {
  
  results &lt;- list()
  
  for (page in 1:3) {
    attempts &lt;- 0
    
    repeat {
      attempts &lt;- attempts + 1
      update_metric(&quot;attempts&quot;, 1, increment = TRUE)
      
      # Pretends to call an API that sometimes fails
      ok &lt;- runif(1) &gt; 0.5
      if (ok || attempts &gt;= 3) break
    }
    
    update_metric(&quot;api_calls&quot;, 1, increment = TRUE)
    results[[page]] &lt;- data.frame(page = page, value = rnorm(10))
  }
  
  df &lt;- do.call(rbind, results)
  
  # Overwriting (the default) is fine for a one-off value
  update_metric(&quot;rows_read&quot;, nrow(df))
  
  df
}</pre>
</div>
<p>In this example both <code>api_calls</code> and <code>attempts</code> accumulate. When it finishes, <code>api_calls</code> is 3 (one per page) while <code>attempts</code> is at least 3, depending on how many retries were needed. Each metric still ends up with a single value per pipeline run.</p>
<p>For the full details, see the <a href="https://whipson.github.io/maestro/articles/maestro-9-metrics.html" rel="nofollow" target="_blank">metrics vignette</a>.</p>
</section>
<section id="other-recent-changes" class="level2">
<h2 class="anchored" data-anchor-id="other-recent-changes">Other recent changes</h2>
<p>If you missed the 1.3.x releases, there are a couple of smaller things worth mentioning:</p>
<ul>
<li>The new <code>@maestroCascadeTags</code> tag propagates selected metadata tags (<code>label</code>, <code>flags</code>, and/or <code>loglevel</code>) from a pipeline to all of its downstream pipelines in a DAG. With no value, it cascades all three.</li>
<li><code>get_slot_usage()</code> gained <code>min_datetime</code> and <code>max_datetime</code> parameters so the time window can be fixed rather than anchored to the current time.</li>
</ul>
<p>The full list of changes is in the <a href="https://whipson.github.io/maestro/news/index.html" rel="nofollow" target="_blank">release notes</a>.</p>
<p>As always, happy orchestrating!</p>


</section>

 
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://whipson.github.io/data-in-flight/posts/maestro-1-4-0/maestro-1-4-0-release.html"> data-in-flight</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/maestro-1-4-0-adds-custom-pipeline-metrics/">maestro 1.4.0 adds custom pipeline metrics</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404231</post-id>	</item>
		<item>
		<title>A Trip from Covariance to Correlation and Back</title>
		<link>https://www.r-bloggers.com/2026/10/a-trip-from-covariance-to-correlation-and-back/</link>
		
		<dc:creator><![CDATA[Andrea Onofri]]></dc:creator>
		<pubDate>Wed, 14 Oct 2026 22:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://www.statforbiology.com/posts/general_CorrelationCovariance.html</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>When we have a multi-trait table of data, we might be interested in investigating variability structure and the reciprocal relationships (whether causal or non-causal) among variables. For example, let’s consider the ‘mtcars’ dataset, shown belo...</p></div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/a-trip-from-covariance-to-correlation-and-back/">A Trip from Covariance to Correlation and Back</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://www.statforbiology.com/posts/general_CorrelationCovariance.html"> Statforbiology</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
 





<p>When we have a multi-trait table of data, we might be interested in investigating variability structure and the reciprocal relationships (whether causal or non-causal) among variables. For example, let’s consider the ‘mtcars’ dataset, shown below:</p>
<div class="cell">
<pre>data(mtcars)
head(mtcars)
##                    mpg cyl disp  hp drat    wt  qsec vs am gear carb
## Mazda RX4         21.0   6  160 110 3.90 2.620 16.46  0  1    4    4
## Mazda RX4 Wag     21.0   6  160 110 3.90 2.875 17.02  0  1    4    4
## Datsun 710        22.8   4  108  93 3.85 2.320 18.61  1  1    4    1
## Hornet 4 Drive    21.4   6  258 110 3.08 3.215 19.44  1  0    3    1
## Hornet Sportabout 18.7   8  360 175 3.15 3.440 17.02  0  0    3    2
## Valiant           18.1   6  225 105 2.76 3.460 20.22  1  0    3    1</pre>
</div>
<p>Considering the different cars (along the rows), we might be interested in investigating the structure of variability for fuel consumption (mpg), number of cylinders (cyl), displacement (disp) and horsepower (hp). How much does each variable change across the different cars? How much does each variable change when another variable changes? Is there any kind of joint variation, either positive or negative?</p>
<p>One suitable statistic to answer these question is the covariance, that is:</p>
<p><img src="https://latex.codecogs.com/png.latex?%5Ctextrm%7Bcov%7D(X,%20Y)%20=%20%5Cfrac%7B1%7D%7Bn%20-%201%7D%5Csum%5Climits_%7Bi=1%7D%5E%7Bn%7D%20%7B(X_i%20-%20%5Cbar%7BX%7D)(Y_i%20-%20%5Cbar%7BY%7D)%7D"></p>
<p>where <img src="https://latex.codecogs.com/png.latex?%5Cbar%7BY%7D"> and <img src="https://latex.codecogs.com/png.latex?%5Cbar%7BX%7D"> are the means for each variable and <img src="https://latex.codecogs.com/png.latex?n"> is the number of individuals. In words, covariance is the sum of products of the i<sup>th</sup> deviations from the mean for both variables. In this setting, the variance is just the covariance of a variable with itself.</p>
<p>For a data table, variances and covariances can be calculated by using the <code>cov()</code> function, that returns a matrix (the variance-covariance matrix, indeed):</p>
<div class="cell">
<pre>matr &lt;- mtcars[,1:4]

# Covariances
Sigma &lt;- cov(matr)
Sigma
##              mpg        cyl       disp        hp
## mpg    36.324103  -9.172379  -633.0972 -320.7321
## cyl    -9.172379   3.189516   199.6603  101.9315
## disp -633.097208 199.660282 15360.7998 6721.1587
## hp   -320.732056 101.931452  6721.1587 4700.8669</pre>
</div>
<p>While covariances indicate direction (positive or negative association), their values depend on the scale of measurement, making it difficult to gauge the strength of the relationship at a glance. For this reason, covariances are usually ‘standardized’ by dividing by the product of the standard deviations to obtain Pearson’s correlation coefficient:</p>
<p><img src="https://latex.codecogs.com/png.latex?%5Ctextrm%7Bcor%7D(X,%20Y)%20=%20%5Cfrac%7B%5Ctextrm%7Bcov%7D(X,%20Y)%7D%7B%5Csigma_x%20%5Csigma_y%7D%20"></p>
<p>where <img src="https://latex.codecogs.com/png.latex?%5Csigma_x"> and <img src="https://latex.codecogs.com/png.latex?%5Csigma_y"> are the standard deviations for <img src="https://latex.codecogs.com/png.latex?X"> and <img src="https://latex.codecogs.com/png.latex?Y">. Unlike covariances, correlations go from -1 to 1 and the correlation matrix has 1s along its main diagonal (since the correlation of any variable with itself is 1). In R, the matrix of correlation is obtained by using the <code>cor()</code> function:</p>
<div class="cell">
<pre># Correlations
R &lt;- cor(matr)
R
##             mpg        cyl       disp         hp
## mpg   1.0000000 -0.8521620 -0.8475514 -0.7761684
## cyl  -0.8521620  1.0000000  0.9020329  0.8324475
## disp -0.8475514  0.9020329  1.0000000  0.7909486
## hp   -0.7761684  0.8324475  0.7909486  1.0000000</pre>
</div>
<p>This brings us to the main question of this post: Given a variance-covariance matrix, can we compute the correlation matrix? And can we do the reverse—moving from a correlation matrix back to a variance-covariance matrix? With reference to the matrices above, can we go from ‘Sigma’ to ‘R’ and from ‘R’ to ‘Sigma’? I often find myself needing to make these conversions, especially when working with mixed models or time series analysis.</p>
<section id="from-covariance-to-correlation" class="level1">
<h1>From covariance to correlation</h1>
<p>Considering the previous expression, it is easy to see that, if we take the covariance between ‘disp’ and ‘mpg’ above (-633.0972), the correlation can be obtained as:</p>
<div class="cell">
<pre>-633.097208 / (sqrt(36.324103) * sqrt(15360.7998))
## [1] -0.8475514</pre>
</div>
<p>and all the necessary information is contained in the variance-covariance matrix.</p>
<p>Of course, doing these calculations for all covariances one by one is tedious. The question is: is there any simple way to go from a covariance matrix to a correlation matrix with one single operation?</p>
<p>The answer is found in matrix algebra; let’s take a look at the variance-covariane matrix: what we have to do is to take each element in this matrix and divide it by the square roots of the diagonal elements in the same column and in the same row (see figure below). Consequently, our task is:</p>
<ol type="1">
<li>take the diagonal elements</li>
<li>take each i<sup>th</sup> row and divide all its elements by the i<sup>th</sup> diagonal element (row-scaling)</li>
<li>take each i<sup>th</sup> column of the resulting matrix and divide all its elements by the i<sup>th</sup> diagonal element (column-scaling)</li>
</ol>
<div class="cell">
<div class="cell-output-display">
<div>
<figure class="figure">
<p><img src="https://i0.wp.com/www.statforbiology.com/Figures/CovarianceCorrelation.png?w=578&#038;ssl=1" class="img-fluid figure-img" data-recalc-dims="1"></p>
</figure>
</div>
</div>
</div>
<p>Row-scaling can be performed by using matrix-algebra; in particular, we can take a diagonal matrix of weights and multiply it by the square matrix we want to scale, as shown below.</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Cbegin%7Bpmatrix%7D%0A1%20&#038;%200%20&#038;%200%20&#038;%200%20%5C%5C%0A0%20&#038;%202%20&#038;%200%20&#038;%200%20%5C%5C%0A0%20&#038;%200%20&#038;%203%20&#038;%200%20%5C%5C%0A0%20&#038;%200%20&#038;%200%20&#038;%204%0A%5Cend%7Bpmatrix%7D%0A%5Ctimes%0A%5Cbegin%7Bpmatrix%7D%0A1%20&#038;%201%20&#038;%201%20&#038;%201%20%5C%5C%0A1%20&#038;%201%20&#038;%201%20&#038;%201%20%5C%5C%0A1%20&#038;%201%20&#038;%201%20&#038;%201%20%5C%5C%0A1%20&#038;%201%20&#038;%201%20&#038;%201%0A%5Cend%7Bpmatrix%7D%0A=%0A%5Cbegin%7Bpmatrix%7D%0A1%20&#038;%201%20&#038;%201%20&#038;%201%20%5C%5C%0A2%20&#038;%202%20&#038;%202%20&#038;%202%20%5C%5C%0A3%20&#038;%203%20&#038;%203%20&#038;%203%20%5C%5C%0A4%20&#038;%204%20&#038;%204%20&#038;%204%0A%5Cend%7Bpmatrix%7D%0A"></p>
<p>We see that all elements of the first row were multiplied by the first weighting element, all elements of the second row were multiplied by the second weighting element and so forth.</p>
<p>Column scaling is done similarly, but in reverse order: we take the target matrix and post-multiply it by the diagonal matrix of weights, as shown below.</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Cbegin%7Bpmatrix%7D%0A1%20&#038;%201%20&#038;%201%20&#038;%201%20%5C%5C%0A1%20&#038;%201%20&#038;%201%20&#038;%201%20%5C%5C%0A1%20&#038;%201%20&#038;%201%20&#038;%201%20%5C%5C%0A1%20&#038;%201%20&#038;%201%20&#038;%201%0A%5Cend%7Bpmatrix%7D%0A%5Ctimes%0A%5Cbegin%7Bpmatrix%7D%0A1%20&#038;%200%20&#038;%200%20&#038;%200%20%5C%5C%0A0%20&#038;%202%20&#038;%200%20&#038;%200%20%5C%5C%0A0%20&#038;%200%20&#038;%203%20&#038;%200%20%5C%5C%0A0%20&#038;%200%20&#038;%200%20&#038;%204%0A%5Cend%7Bpmatrix%7D%0A=%0A%5Cbegin%7Bpmatrix%7D%0A1%20&#038;%202%20&#038;%203%20&#038;%204%20%5C%5C%0A1%20&#038;%202%20&#038;%203%20&#038;%204%20%5C%5C%0A1%20&#038;%202%20&#038;%203%20&#038;%204%20%5C%5C%0A1%20&#038;%202%20&#038;%203%20&#038;%204%0A%5Cend%7Bpmatrix%7D%0A"></p>
<p>We see that all elements of the first column were multiplied by the first weighting element, all elements of the second row were multiplied by the second weighting element, and so forth.</p>
<p>Consequently, we can take our variance-covariance matrix and:</p>
<ol type="1">
<li>Take the square roots of the diagonal elements (standard deviations) and load them into a diagonal matrix</li>
<li>Invert this diagonal matrix</li>
<li>Pre-multiply and post-multiply the variance-covariance matrix by this diagonal matrix of inverted standard deviations</li>
</ol>
<div class="cell">
<pre># from covariance to correlation - 1
StDev &lt;- sqrt(diag(Sigma))
StDevMat &lt;- diag(StDev)
InvStDev &lt;- solve(StDevMat)
InvStDev %*% Sigma %*% InvStDev
##            [,1]       [,2]       [,3]       [,4]
## [1,]  1.0000000 -0.8521620 -0.8475514 -0.7761684
## [2,] -0.8521620  1.0000000  0.9020329  0.8324475
## [3,] -0.8475514  0.9020329  1.0000000  0.7909486
## [4,] -0.7761684  0.8324475  0.7909486  1.0000000</pre>
</div>
<p>The previous solution is correct, but, in R, it is rather inefficient. The best way is to use the <code>cov2cor()</code> function in the ‘stat’ package.</p>
<div class="cell">
<pre># From covariance to correlation - 2
cov2cor(Sigma)
##             mpg        cyl       disp         hp
## mpg   1.0000000 -0.8521620 -0.8475514 -0.7761684
## cyl  -0.8521620  1.0000000  0.9020329  0.8324475
## disp -0.8475514  0.9020329  1.0000000  0.7909486
## hp   -0.7761684  0.8324475  0.7909486  1.0000000</pre>
</div>
<p>With base R, we can also <code>sweep()</code> twice:</p>
<div class="cell">
<pre># From covariance to correlation - 3
sweep(sweep(Sigma, 1, StDev, FUN = &quot;/&quot;), 2, StDev, FUN = &quot;/&quot;)
##             mpg        cyl       disp         hp
## mpg   1.0000000 -0.8521620 -0.8475514 -0.7761684
## cyl  -0.8521620  1.0000000  0.9020329  0.8324475
## disp -0.8475514  0.9020329  1.0000000  0.7909486
## hp   -0.7761684  0.8324475  0.7909486  1.0000000</pre>
</div>
<p>Alternatively, we can combine <code>scale()</code> and <code>t()</code> (transpose) twice, though it is noticeably less intuitive:</p>
<div class="cell">
<pre># From covariance to correlation - 4
scale(t(scale(t(Sigma), center = F, scale = StDev)), 
      center = F, scale = StDev)
##             mpg        cyl       disp         hp
## mpg   1.0000000 -0.8521620 -0.8475514 -0.7761684
## cyl  -0.8521620  1.0000000  0.9020329  0.8324475
## disp -0.8475514  0.9020329  1.0000000  0.7909486
## hp   -0.7761684  0.8324475  0.7909486  1.0000000
## attr(,&quot;scaled:scale&quot;)
##        mpg        cyl       disp         hp 
##   6.026948   1.785922 123.938694  68.562868</pre>
</div>
<p>Another very clean approach in R is based on the outer product. If we have a vectors of standard deviations ($), the outer product is:</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Cboldsymbol%7B%5Csigma%7D%20%5Cboldsymbol%7B%5Csigma%7D%5ET%20=%0A%5Cbegin%7Bpmatrix%7D%0A%5Csigma_1%20%5C%5C%20%5Csigma_2%20%5C%5C%20%5Cvdots%20%5C%5C%20%5Csigma_n%0A%5Cend%7Bpmatrix%7D%0A%5Cbegin%7Bpmatrix%7D%0A%5Csigma_1%20&#038;%20%5Csigma_2%20&#038;%20%5Cdots%20&#038;%20%5Csigma_n%0A%5Cend%7Bpmatrix%7D%0A=%20%5Cbegin%7Bpmatrix%7D%0A%5Csigma_1%20%5Csigma_1%20&#038;%20%5Csigma_1%20%5Csigma_2%20&#038;%20%5Cdots%20&#038;%20%5Csigma_1%20%5Csigma_n%20%5C%5C%0A%5Csigma_2%20%5Csigma_1%20&#038;%20%5Csigma_2%20%5Csigma_2%20&#038;%20%5Cdots%20&#038;%20%5Csigma_2%20%5Csigma_n%20%5C%5C%0A%5Cvdots%20&#038;%20%5Cvdots%20&#038;%20%5Cddots%20&#038;%20%5Cvdots%20%5C%5C%0A%5Csigma_n%20%5Csigma_1%20&#038;%20%5Csigma_n%20%5Csigma_2%20&#038;%20%5Cdots%20&#038;%20%5Csigma_n%20%5Csigma_n%0A%5Cend%7Bpmatrix%7D%0A"></p>
<p>Thus, the correlation is calculated as:</p>
<div class="cell">
<pre># From covariance to correlation - 5
Sigma/outer(StDev, StDev)
##             mpg        cyl       disp         hp
## mpg   1.0000000 -0.8521620 -0.8475514 -0.7761684
## cyl  -0.8521620  1.0000000  0.9020329  0.8324475
## disp -0.8475514  0.9020329  1.0000000  0.7909486
## hp   -0.7761684  0.8324475  0.7909486  1.0000000</pre>
</div>
</section>
<section id="from-correlation-to-covariance" class="level1">
<h1>From correlation to covariance</h1>
<p>It is easy to see that:</p>
<p><img src="https://latex.codecogs.com/png.latex?%20%5Ctextrm%7Bcov%7D(X,%20Y)%20=%20%5Ctextrm%7Bcor%7D(X,%20Y)%20%5Csigma_x%20%5Csigma_y"> Thus, if we have the correlation between ‘disp’ and ‘mpg’ (-0.8521620), we can easily calculate the covariance as:</p>
<div class="cell">
<pre>-0.8475514 * sqrt(36.324103) * sqrt(15360.7998)
## [1] -633.0972</pre>
</div>
<p>The only problem is that the variances are not contained in a correlation matrix, so we need to get them from elsewhere. Once we have them, we</p>
<ol type="1">
<li><ol type="1">
<li>Take their square roots and place them into a diagonal matrix</li>
</ol></li>
<li>Pre-multiply and post-multiply the correlation matrix for this diagonal matrix of standard deviations</li>
</ol>
<p>The solution is as follows:</p>
<div class="cell">
<pre># from correlation to covariance - 1
StDev &lt;- sqrt(diag(Sigma))
StDevMat &lt;- diag(StDev)
StDevMat %*% R %*% StDevMat
##             [,1]       [,2]       [,3]      [,4]
## [1,]   36.324103  -9.172379  -633.0972 -320.7321
## [2,]   -9.172379   3.189516   199.6603  101.9315
## [3,] -633.097208 199.660282 15360.7998 6721.1587
## [4,] -320.732056 101.931452  6721.1587 4700.8669</pre>
</div>
<p>More efficiently, we can use the <code>cor2cov()</code> function in the ‘lavaan’ package.</p>
<div class="cell">
<pre># From correlation to covariance - 2
lavaan::cor2cov(R, StDev)
##              mpg        cyl       disp        hp
## mpg    36.324103  -9.172379  -633.0972 -320.7321
## cyl    -9.172379   3.189516   199.6603  101.9315
## disp -633.097208 199.660282 15360.7998 6721.1587
## hp   -320.732056 101.931452  6721.1587 4700.8669</pre>
</div>
<p>Or, we can <code>sweep()</code> twice:</p>
<div class="cell">
<pre># From correlation to covariance - 3
sweep(sweep(R, 1, StDev, FUN = &quot;*&quot;), 2, StDev, FUN = &quot;*&quot;)
##              mpg        cyl       disp        hp
## mpg    36.324103  -9.172379  -633.0972 -320.7321
## cyl    -9.172379   3.189516   199.6603  101.9315
## disp -633.097208 199.660282 15360.7998 6721.1587
## hp   -320.732056 101.931452  6721.1587 4700.8669</pre>
</div>
<p>or <code>scale()</code> and <code>t()</code> twice:</p>
<div class="cell">
<pre># From correlation to covariance - 5
scale(t(scale(t(R), center = F, scale = 1/StDev)), 
      center = F, scale = 1/StDev)
##              mpg        cyl       disp        hp
## mpg    36.324103  -9.172379  -633.0972 -320.7321
## cyl    -9.172379   3.189516   199.6603  101.9315
## disp -633.097208 199.660282 15360.7998 6721.1587
## hp   -320.732056 101.931452  6721.1587 4700.8669
## attr(,&quot;scaled:scale&quot;)
##         mpg         cyl        disp          hp 
## 0.165921457 0.559934979 0.008068505 0.014585154</pre>
</div>
<p>Finally, we can use the outer product as follows:</p>
<div class="cell">
<pre># From correlation to covariance - 6
R * outer(StDev, StDev)
##              mpg        cyl       disp        hp
## mpg    36.324103  -9.172379  -633.0972 -320.7321
## cyl    -9.172379   3.189516   199.6603  101.9315
## disp -633.097208 199.660282 15360.7998 6721.1587
## hp   -320.732056 101.931452  6721.1587 4700.8669</pre>
</div>
<p>Do you have a cleaner or faster trick for this in R? Drop a comment below and I’ll be happy to feature it here!</p>
<p>Thanks for reading, and happy coding!</p>
<p>(P.S. If you enjoyed this post, check out my book “Field Research Methods in Agriculture: An Introduction with R” via the link below!)</p>
<p>Prof. Andrea Onofri<br>
Department of Agricultural, Food and Environmental Sciences<br>
University of Perugia (Italy)<br>
Send comments to: <a href="mailto:andrea.onofri@unipg.it" rel="nofollow" target="_blank">andrea.onofri@unipg.it</a></p>
<p><a href="https://www.awin1.com/cread.php?awinmid=26429&#038;awinaffid=2675822&#038;ued=https%3A%2F%2Flink.springer.com%2Fbook%2F10.1007%2F978-3-032-08199-5" rel="nofollow" target="_blank"><img src="https://i0.wp.com/www.statforbiology.com/Figures/Email_Signature_978-3-032-08199-5.png?w=578&#038;ssl=1" alt="Book cover" class="cover" align="center" data-recalc-dims="1"></a></p>


</section>

 
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://www.statforbiology.com/posts/general_CorrelationCovariance.html"> Statforbiology</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/a-trip-from-covariance-to-correlation-and-back/">A Trip from Covariance to Correlation and Back</a>]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">404212</post-id>	</item>
		<item>
		<title>Dogs of New York</title>
		<link>https://www.r-bloggers.com/2026/10/dogs-of-new-york-2/</link>
		
		<dc:creator><![CDATA[R on kieranhealy.org]]></dc:creator>
		<pubDate>Wed, 14 Oct 2026 17:17:28 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://kieranhealy.org/blog/archives/2026/10/14/dogs-of-new-york/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; "> I recently updated nycdogs, a data package for R that I originally made for teaching purposes. It bundles up New York City’s dog licensing and dog bite datasets into an R package. The data is already provided by the city in a directly usable way ...</div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/dogs-of-new-york-2/">Dogs of New York</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://kieranhealy.org/blog/archives/2026/10/14/dogs-of-new-york/"> R on kieranhealy.org</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
<p>I recently updated <a href="https://kjhealy.github.io/nycdogs/" rel="nofollow" target="_blank">nycdogs</a>, a data package for R that I originally made for teaching purposes. It bundles up New York City’s <a href="https://data.cityofnewyork.us/Health/NYC-Dog-Licensing-Dataset/nu7n-tubp/about_data" rel="nofollow" target="_blank">dog licensing</a> and <a href="https://data.cityofnewyork.us/Health/DOHMH-Dog-Bite-Data/rsgh-akpg/about_data" rel="nofollow" target="_blank">dog bite</a> datasets into an R package. The data is already provided by the city in a directly usable way (as CSV files), but packaging it up makes it easier to integrate with other packages and get things moving a little more quickly in class.</p>
<p>The <code>nyc_license</code> table looks like this:</p>
<div class="highlight-wrapper">
    
    
        <div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre> 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
</pre></td>
<td class="lntd">
<pre>nyc_license
#&gt; # A tibble: 819,323 × 12
#&gt;    animal_name animal_gender animal_birth_year breed_name       breed_rc zip_code zip   license_issued_date license_expired_date extract_year borough city 
#&gt;    &lt;chr&gt;       &lt;chr&gt;                     &lt;int&gt; &lt;chr&gt;            &lt;chr&gt;    &lt;chr&gt;    &lt;chr&gt; &lt;date&gt;              &lt;date&gt;                      &lt;int&gt; &lt;chr&gt;   &lt;chr&gt;
#&gt;  1 Paige       F                          2014 American Pit Bu… Pit Bul… 10035    10035 2014-09-12          2017-09-12                   2016 Manhat… New …
#&gt;  2 Yogi        M                          2010 Boxer            Boxer    10465    10465 2014-09-12          2017-10-02                   2016 Bronx   Bronx
#&gt;  3 Ali         M                          2014 Basenji          Basenji  10013    10013 2014-09-12          2019-09-12                   2016 Manhat… New …
#&gt;  4 Queen       F                          2013 Akita Crossbreed Akita C… 10013    10013 2014-09-12          2017-09-12                   2016 Manhat… New …
#&gt;  5 Lola        F                          2009 Maltese          Maltese  10028    10028 2014-09-12          2017-10-09                   2016 Manhat… New …
#&gt;  6 Ian         M                          2006 Unknown          Unknown  10013    10013 2014-09-12          2019-10-30                   2016 Manhat… New …
#&gt;  7 Buddy       M                          2008 Unknown          Unknown  10025    10025 2014-09-12          2017-10-20                   2016 Manhat… New …
#&gt;  8 Chewbacca   F                          2012 Labrador Retrie… Labrado… 10013    10013 2014-09-12          2019-10-01                   2016 Manhat… New …
#&gt;  9 Heidi-bo    F                          2007 Dachshund Smoot… Dachshu… 11215    11215 2014-09-13          2017-04-16                   2016 Brookl… Broo…
#&gt; 10 Massimo     M                          2009 Bull Dog, French French … 11201    11201 2014-09-13          2017-09-17                   2016 Brookl… Broo…
#&gt; # &#x2139; 819,313 more rows</pre></td></tr></table>
</div>
</div>
    
</div>

<p>The dataset is good for teaching with for several reasons. First, people like
dogs. Second, it has about 800,000 records, which is a lot in many ways but also
trivial for a modern laptop to work with. Third, the data are pretty clean, but
they’re not <em>that</em> clean. There are various things going on in there that invite
us to explore it but also to be careful about what we can and can’t be confident
about.</p>
<p>The first thing we ask about any data set like this is: what is each row in this
table? In the case of <code>nyc_license</code>, each row is a record of one time-limited
license for a dog. A row is not necessarily the record of a unique dog. When an owner
renews a dog’s license, the dog will appear one time for each license period. In
New York City, dog licenses can be issued for one year or five years. The table
does not provide any <code>dog_id</code> column that ties sequences of licenses to individual
dogs.</p>
<p>Second, the table contains several “extracts” of the license data. It seems like
the way the provider updates the NYC Open Data record is to periodically pull
from its internal database every year or so and append it to the existing,
previously-released version. The <code>extract_year</code> column identifies the extract,
so at least we know that. But it’s also the case that a license that was active
in more than one extract year appears in each extract. As a result, the table
has a large number of duplicate rows.</p>
<p>Third, license information is provided by dog owners. This means that there’s a
fair amount of noise in the records, because people make typos and other errors.
For instance, the ZIP code data has incomplete ZIPs. Some people provide invalid
ZIP codes (e.g. ones that are fewer than five digits long). Others (about 3,300
records) have ZIP codes that are not in New York City. Eighty percent of these
(about 2,700) are ZIP codes located elsewhere in New York state. Another 77 are
located in Florida, which I kind of like. Five of them live in Boca Raton, for
example. Their kids never call. You’d think it would be easy for them to just
pick up the bone, I mean phone, but you never hear from them.</p>
<p>About 650 rows have valid ZIP codes that are in Manhattan, but these ZIPs are
not
<a href="https://www.census.gov/programs-surveys/geography/guidance/geo-areas/zctas.html" rel="nofollow" target="_blank">ZCTAs</a>.
The trouble with ZIP codes is that people think of them as areas, as polygons,
but in fact they are lines. They are defined by the Post Office as postal
routes, i.e. as journeys a mail carrier takes. While they <em>tend</em> to form closed
polygons, they are absolutely not guaranteed to. But people know their ZIP code
and all kinds of analysts (from marketers to political operatives) like thinking
in terms of ZIP codes. So the Census defines <a href="https://www.census.gov/programs-surveys/geography/guidance/geo-areas/zctas.html" rel="nofollow" target="_blank">ZIP Code Tabulation
Areas</a>
that are a kind of best-effort at answering the question “What if this ZIP code
were a closed polygon?” ZCTAs exclude every P.O. box and single-address ZIP
nationwide. Manhattan has several buildings that have their own ZIP code. New York City also defines <a href="https://data.cityofnewyork.us/Health/Modified-Zip-Code-Tabulation-Areas-MODZCTA-/pri4-ifjk/about_data" rel="nofollow" target="_blank">MODZCTAs</a> that merge some low- or no-population ZCTAs with their neighbors. I use these below.</p>
<p>Fourth, beyond the ZIP codes, owners also provide information about the age and
sex of their dogs, the dog’s name, and the dog’s breed. Again, this kind of
information tends to be a little noisy. For example, people vary in how they
spell names. They also vary in how they classify their dog by breed. Beyond the
list of “recognized” breeds (itself something that dog owners variably conform
to), some people might describe their dog as, e.g., “Labrador-Sheltie mix” while
others might just say “Unknown”. The dataset includes the <code>breed_name</code> column from
the original dataset and also <code>breed_rc</code>, which is a recoding by me to clean up equivalent terms (“Yorkshire Terrier” and “Terrier, Yorkshire”, for example) along with a few other coding decisions.</p>
<p>Finally, and only because I’m choosing to stop here, the table just shows the
number of licensed dogs in New York City. That is definitely not the same as the
number of dogs there. Moreover, license compliance varies quite a bit in ways
that are related to characteristics of the owner that we can’t observe, and also
ones that we partially can, like where they live.</p>
<p>All of which is related to the fact that one of the lolsob-mantras of courses I
teach is “Now you know the true meaning of data”. In the graphs and maps below,
I’ve done the best I can, where appropriate, to uniquely identify individual
dogs in the dataset (even though ultimately we cannot).</p>
<h2 id="popular-breeds">Popular Breeds</h2>
<p>With that out of the way, it’s time to indulge your prejudices about Dogs, New
Yorkers, and New York Dogs. The most common “breed” of dog in New York is
“Unknown”, as befits a city of immigrants, hustlers, scrappy up-and-comers, and
dogs seeking to leave their small-town past behind and remake their identities.
Somewhere between eight and ten percent of records each year have “Unknown”
recorded as their breed. There’s some change over time (a declining share), but
I think this is an artifact of how breeds are getting recorded rather than a
change in the composition of the dog population. If we leave the Unknowns out,
here are the trends in annual licenses issued to the twenty most popular breeds.</p>
<figure class="full-width"><a href="https://i0.wp.com/kieranhealy.org/blog/archives/2026/10/14/dogs-of-new-york/nyc-popular-breeds.png?ssl=1" rel="nofollow" target="_blank">
    <img src="https://i0.wp.com/kieranhealy.org/blog/archives/2026/10/14/dogs-of-new-york/nyc-popular-breeds.png?w=578&#038;ssl=1"
         alt="Yorkshire terrier is the most popular breed. Goldendoodles are on the rise." data-recalc-dims="1"/></a><figcaption>
            <p>Top Dogs.</p>
        </figcaption>
</figure>
<p>Some breeds are pretty steadily popular. Smaller breeds do well in a city full
of apartments. We can also see some evidence here of trends in popularity.
However, these annual numbers are what we might call the prevalence of dogs,
rather than the incidence of new dogs. If we want to get a better sense of
fashion trends in dog breeds, we should look at puppies. (Who doesn’t want to
look at puppies?) Because of the structure of the data we can’t quite do this.
The best we can do, based on the dog’s age, is to examine licenses issued to
dogs younger than a year old. Let’s pick out some breeds that seem to be rising,
a few that seem relatively steady, and a few that might be declining. I’ve
indexed these changes relative to each breed in 2016 by setting the value for
that year to 100. Even though our range is narrow, we use a log scale on the
y-axis because, e.g., a rise in a breed’s index from 100 to 200
reflects the same relative change as a fall from 100 to 50.</p>
<figure class="full-width"><a href="https://i1.wp.com/kieranhealy.org/blog/archives/2026/10/14/dogs-of-new-york/nyc-breed-trends-young-index.png?ssl=1" rel="nofollow" target="_blank">
    <img src="https://i1.wp.com/kieranhealy.org/blog/archives/2026/10/14/dogs-of-new-york/nyc-breed-trends-young-index.png?w=578&#038;ssl=1"
         alt="The rise of the Maltipoo, Dachshund, and Goldendoodle." data-recalc-dims="1"/></a>
</figure>
<p>Maltipoos and Goldendoodles seem to be doing pretty well for themselves. Golden Retrievers
remain steadily popular. Meanwhile, Shiba Inus, beloved fad-breed of the 2010s,
seem to have been affected by their association with crypto and are strongly in
decline. Much drop. Very decline. Wow. Beagles are also doing poorly. Good grief.</p>
<h2 id="where-breeds-live">Where Breeds Live</h2>
<p>We can look at the relative prevalence of breeds by their ZIP code (strictly
speaking, by their MODZCTA). I hesitate a little on this just because, even with
800,000 licenses, we’ll run out of degrees of freedom fairly quickly if we break
things down by breed and ZIP. Relatedly, we should ask what sort of metric we’re
trying to show here. What’s the denominator? (A surprising amount of Statistics,
I claim, is essentially exploring the question “What happens if I divide by
<em>this</em> number instead of <em>that</em> number?”) Popular dogs are, to a first
approximation, pretty popular all over the city. Most ZIP codes have a solid
contingent of Yorkies. If, for each popular breed, we calculate the share of
that breed as a percentage of all dogs in the ZIP code, we’ll see some
ZIP-by-ZIP variation come out.</p>
<p>It would also be nice to adjust for the fact that different ZIP codes have quite
different shares of the total number of dogs licensed in the city. (Which,
again, is not the same as share of the total number of dogs in the city.) So we
calculate the within-ZIP share and make a ratio of it by dividing that number by
the breed’s citywide share. The ratio is 1 when the breed is just as common in
that ZIP code as it is across the whole city. If it’s 2, the breed is twice as
common in that ZIP as it is across the city. If it’s 0.5, the breed is half as
common in that ZIP as it is in the city as a whole. The main downside of ratios
like this is that they can exaggerate differences a little, especially if the
base rates are small. We have enough breeds in each zip code to make me feel OK
about doing this. But if you refer to the bar charts above you can see that even
for disproportionately popular breeds in zips, you’re looking at changes like
“twice as common with respect to a baseline of five percent of all dogs”. Here’s
the map:</p>
<figure class="full-width"><a href="https://i1.wp.com/kieranhealy.org/blog/archives/2026/10/14/dogs-of-new-york/nyc-dogs-of-nyc-dog-ratio.png?ssl=1" rel="nofollow" target="_blank">
    <img src="https://i1.wp.com/kieranhealy.org/blog/archives/2026/10/14/dogs-of-new-york/nyc-dogs-of-nyc-dog-ratio.png?w=578&#038;ssl=1" data-recalc-dims="1"/></a>
</figure>
<p>We see some plausible evidence that breeds are distributed across ZIP codes in a
manner that tracks the demography of owners. French Bulldogs in Chelsea, Soho,
and TriBeCa. Havanese on the Upper East Side. Pit Bulls in the Bronx. German
Shepherds on Staten Island. Goldendoodles and King Charles Spaniels in Manhattan
south of 96th.</p>
<h2 id="the-diffusion-of-goldendoodles">The Diffusion of Goldendoodles</h2>
<p>Let’s take a closer look at the Goldendoodles. In 2015, Goldendoodles weren’t
that common. We can ask: where were they initially popular? Did they diffuse
across New York City from focal points where people were more likely to set
trends in breed fashion? Restricting ourselves to young dogs, we can try to look
at each ZIP’s share of all Goldendoodle puppies licensed each year from 2015
to 2025. Here’s what that looks like:</p>
<figure class="full-width"><a href="https://i0.wp.com/kieranhealy.org/blog/archives/2026/10/14/dogs-of-new-york/nyc-goldendoodles-young-yr-wide.png?ssl=1" rel="nofollow" target="_blank">
    <img src="https://i0.wp.com/kieranhealy.org/blog/archives/2026/10/14/dogs-of-new-york/nyc-goldendoodles-young-yr-wide.png?w=578&#038;ssl=1"
         alt="Doodle diffusion." data-recalc-dims="1"/></a><figcaption>
            <p>Doodle diffusion.</p>
        </figcaption>
</figure>
<p>In 2015, Goldendoodle puppies are licensed in a relatively small number of ZIP
codes, mostly in Manhattan. Most ZIP codes are doodle-free. Over the next five
years, they gradually spread across the city. By 2023 or 2024, most—though
still not all—ZIP codes are licensing young Goldendoodles.</p>
<p>Again, the underlying numbers aren’t huge. So take all of this with a delicious
treat. I mean, grain of salt. I also wonder what the next few years are going to
look like in terms of dog popularity. Based on the maps above, I think
Australian Shepherds are likely to continue to rise in popularity amongst owners
looking to acquire a fashionable breed. And while they’re not on any of the maps
above, the data also suggest that the
<a href="https://en.wikipedia.org/wiki/Korean_Jindo" rel="nofollow" target="_blank">Jindo</a>, a Korean breed that is a
relatively recent arrival in the US, has been sharply on the rise. If you ask
me, though, if you’re in New York City and planning to get a dog, you should
throw in your lot with Unknown and get a good, solid, dog-shaped mutt.
Preferably one with strong views on the state of the Subway these days, or who
knows a guy who can get you a deal on some shirts, or the special ramen, or
tickets. Not tickets for anything in particular. Just, like, tickets. Works out
of an upstairs bar downtown on Water St. Trust me.</p>

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://kieranhealy.org/blog/archives/2026/10/14/dogs-of-new-york/"> R on kieranhealy.org</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/dogs-of-new-york-2/">Dogs of New York</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404219</post-id>	</item>
		<item>
		<title>A Bayesian Network to Explore Data Center Risks</title>
		<link>https://www.r-bloggers.com/2026/10/a-bayesian-network-to-explore-data-center-risks/</link>
		
		<dc:creator><![CDATA[Joseph Rickert]]></dc:creator>
		<pubDate>Tue, 13 Oct 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://rworks.dev/posts/data-center-risk-model/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>Data centers required to fuel the ambitions of Artificial Intelligence companies have sparked a nationwide conversation. AI companies are moving quickly to build out infrastructure and local communities are pushing back to resist what they see a...</p></div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/a-bayesian-network-to-explore-data-center-risks/">A Bayesian Network to Explore Data Center Risks</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://rworks.dev/posts/data-center-risk-model/"> R Works</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
 





<p>Data centers required to fuel the ambitions of Artificial Intelligence companies have sparked a nationwide conversation. AI companies are moving quickly to build out infrastructure and local communities are pushing back to resist what they see as the appropriation of public resources such as electricity and water to further enrich wealthy corporations. Moreover, the sentiment shared by both the political left and right is that data centers will significantly degrade the quality of life for those living adjacent to a data center and to the extended community as well. This post implements a non-trivial Bayesian Network to study the risks involved. I hope it is of some interest to a broad audience including, people interested in clarifying their thoughts about the risks that data centers may pose to their particular community and analysts curious about Bayesian Networks. However, I think the sample model built out in this post will be especially interesting to decision analysts who know quite a bit about Bayesian Networks but may be unfamiliar with the <code>R</code> programming language and unaware that there is capable opensource software which can provide capabilities beyond those offered by proprietary software systems.</p>
<p>Other than addressing the topic of data center risks the analysis contains no original research except perhaps the contribution of ChatGPT 5.6 Luna which conjured up the innovative parts of the synthetic data generation algorithm from a series of requirements.</p>
<section id="data-center-risks" class="level3">
<h3 class="anchored" data-anchor-id="data-center-risks">Data Center Risks</h3>
<p>Rational discussion about the perceived risks associated with the construction of data centers could benefit from a disciplined approach to organizing information and opinions into risk a analysis flexible enough to the consequences of complex interactions among risk factors. The Bayesian Network presented in this post is intended to be first step towards building such a tool.</p>
<p>In general, the risks imposed by data centers fall into the following general categories. See Appendix C below for some references to investigative reporting supporting these risk categories.</p>
<ul>
<li><p>Electricity. A data center’s power demand can affect the immediate substation, the regional transmission system, and electricity rates for customers across a utility territory. The key questions are who pays for new generation and transmission, whether the facility has a dedicated rate, and whether its demand is flexible during shortages.</p></li>
<li><p>Water. Water use depends heavily on cooling design, climate, server load, and the use of reclaimed water. The relevant impact area is often the watershed rather than the property boundary. During droughts, a data center may compete with households, agriculture, industry, and ecosystems.</p></li>
<li><p>Land and housing. The facility itself may occupy hundreds of acres when parking, substations, cooling systems, security setbacks, and transmission corridors are included. Construction can remove farmland or open space, while the associated workforce and infrastructure can increase housing demand.</p></li>
<li><p>Noise and air quality. The most immediate quality-of-life impacts are usually construction activity, cooling-system noise, transformer hum, generator testing, lighting, and occasional diesel emissions. These effects decline with distance but can remain significant for nearby homes, schools, and outdoor spaces.</p></li>
<li><p>Grid reliability and affordability. A data center can bring grid upgrades that improve regional infrastructure, but rapid demand growth can also strain generation and transmission. If costs are not assigned directly to the facility, residential and small-business customers may absorb part of them.</p></li>
<li><p>Governance and fairness. The distance framework should be paired with an equity analysis: who receives jobs, tax revenue, and infrastructure improvements, and who experiences noise, land loss, water competition, higher rates, or pollution? Community-benefit agreements, transparent resource-demand disclosures, enforceable noise and water standards, and clear cost-recovery rules can substantially change the outcome.</p></li>
</ul>
</section>
<section id="potential-benefits" class="level3">
<h3 class="anchored" data-anchor-id="potential-benefits">Potential Benefits</h3>
<p>Supporters of data centers emphasize the potential benefits of building a data center. These may include increased tax revenue and increased temporary employment. However, most of the reporting referenced below indicates that they do not compensate for the risks.</p>
</section>
<section id="organizing-the-risks-and-benefits" class="level2">
<h2 class="anchored" data-anchor-id="organizing-the-risks-and-benefits">Organizing the risks and benefits</h2>
<p>Model building begins by organizing nodes into a a Directed Acyclic Graph (DAG) with arrows pointing from root nodes which are thought to drive other nodes that represent factors that intensify risk. Eventually the information in these nodes flow to nodes which quantify different kinds of risk. Finally the information from the risk nodes flow into a node that quantifies overall community risk. The following graph expands on the general categories listed above, enumerates a number of risk factors with arrows indicating the direction influence or causality. The names of the nodes are for the most part self explanatory. Note that the scale of the data center and distance from the data center are fundamental factors driving risk.</p>
<p>The number of nodes in this graph are probably much larger than would be required for an exploratory mode. However, I have elected to go with the larges list that seemed to me to be reasonable and coherent in order to provide an example that is more complex than a typical “toy” mode. And since it is usually much easier to remove code than add it, the model below should be useful in deriving a more parsimonious custom model.</p>
<div class="cell" data-layout-align="default">
<div class="cell-output-display">
<div>
<p></p><figure class="figure"><p></p>
<div>
<pre>%%| label: fig-risk-network
%%| fig-cap: &quot;Data Center Risk Network&quot;
%%| fig-alt: &quot;Directed network showing data-center impacts and risks leading to overall community risk.&quot;



flowchart LR

  Scale --&gt; LandFootprint
  Scale --&gt; ElectricityDemand
  Scale --&gt; WaterDemand
  Scale --&gt; ConstructionIntensity
  Scale --&gt; BackupGeneration
  Scale --&gt; WorkforceDemand
  Scale --&gt; NoiseExposure
  Scale --&gt; AirQualityExposure
  Scale --&gt; AccessToBenefits
  Scale --&gt; EconomicBenefit

  Cooling --&gt; WaterDemand
  Cooling --&gt; NoiseExposure

  Distance --&gt; NoiseExposure
  Distance --&gt; AirQualityExposure
  Distance --&gt; TrafficExposure
  Distance --&gt; VisualLandUseExposure
  Distance --&gt; AccessToBenefits
  Distance --&gt; ElectricityCostRisk

  LandFootprint --&gt; VisualLandUseExposure
  ConstructionIntensity --&gt; TrafficExposure
  BackupGeneration --&gt; AirQualityExposure

  ElectricityDemand --&gt; GridStress
  GridCapacity --&gt; GridStress
  GridStress --&gt; ElectricityCostRisk
  GridStress --&gt; ReliabilityRisk

  WaterDemand --&gt; WaterCompetition
  WaterAvailability --&gt; WaterCompetition
  WaterCompetition --&gt; WaterSecurityRisk
  Vulnerability --&gt; WaterSecurityRisk

  WorkforceDemand --&gt; HousingPressure
  HousingAvailability --&gt; HousingPressure
  WorkforceDemand --&gt; ServicePressure
  HousingAvailability --&gt; ServicePressure

  LandFootprint --&gt; EnvironmentalDegradation
  WaterDemand --&gt; EnvironmentalDegradation
  WaterAvailability --&gt; EnvironmentalDegradation

  NoiseExposure --&gt; QualityOfLifeRisk
  AirQualityExposure --&gt; QualityOfLifeRisk
  TrafficExposure --&gt; QualityOfLifeRisk
  VisualLandUseExposure --&gt; QualityOfLifeRisk
  HousingPressure --&gt; QualityOfLifeRisk
  Vulnerability --&gt; QualityOfLifeRisk

  AccessToBenefits --&gt; EconomicBenefit

  QualityOfLifeRisk --&gt; OverallCommunityRisk
  WaterSecurityRisk --&gt; OverallCommunityRisk
  ElectricityCostRisk --&gt; OverallCommunityRisk
  ReliabilityRisk --&gt; OverallCommunityRisk
  EnvironmentalDegradation --&gt; OverallCommunityRisk
  HousingPressure --&gt; OverallCommunityRisk
  ServicePressure --&gt; OverallCommunityRisk
  EconomicBenefit --&gt; OverallCommunityRisk

</pre>
</div>
<p></p></figure><p></p>
</div>
</div>
</div>
</section>
<section id="the-dag" class="level2">
<h2 class="anchored" data-anchor-id="the-dag">The DAG</h2>
<p>The code below builds a discrete Bayesian Network based on the above directed acyclic graph using <code>bnlearn</code> package.</p>
<div class="cell">
<details class="code-fold">
<summary>Required packages and helper functions</summary>
<pre># ============================================================
# Bayesian Network for Data-Center Community Risk
# ============================================================

# Install packages once if necessary:
# install.packages(c(&quot;bnlearn&quot;, &quot;ggplot2&quot;))

library(bnlearn)
library(ggplot2)
library(Rgraphviz)
library(igraph)
library(dplyr)
library(tidyr)
library(ggridges)
library(ggtern)

# ------------------------------------------------------------
# 2. Helper functions for generating categorical variables
# ------------------------------------------------------------

# sample_category &lt;- function(probabilities, labels = names(probabilities)) {
#   sample(labels, size = 1, prob = probabilities)
# }

# Convert an ordinal category to a numeric score.
# Useful for simple synthetic-data rules.
ordinal_score &lt;- function(x) {
  match(x, c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;, &quot;VeryHigh&quot;))
}</pre>
</details>
</div>
<section id="formally-define-the-dag-structure" class="level3">
<h3 class="anchored" data-anchor-id="formally-define-the-dag-structure">Formally define the DAG structure</h3>
<p>This block of code defines the network structure for the <code>bnlearn</code> package and produces the list of the nodes.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre>set.seed(20260914)

# ------------------------------------------------------------
# 1. Define the expert network structure
# ------------------------------------------------------------

# Short node names are used because they are easier to reference
# in bnlearn expressions and plots.

network_string &lt;- paste0(
  &quot;[Distance]&quot;,
  &quot;[Scale]&quot;,
  &quot;[Cooling]&quot;,
  &quot;[GridCapacity]&quot;,
  &quot;[WaterAvailability]&quot;,
  &quot;[HousingAvailability]&quot;,
  &quot;[Vulnerability]&quot;,
  &quot;[LandFootprint|Scale]&quot;,
  &quot;[ElectricityDemand|Scale]&quot;,
  &quot;[WaterDemand|Scale:Cooling]&quot;,
  &quot;[ConstructionIntensity|Scale]&quot;,
  &quot;[BackupGeneration|Scale]&quot;,
  &quot;[WorkforceDemand|Scale]&quot;,
  &quot;[NoiseExposure|Distance:Cooling:Scale]&quot;,
  &quot;[AirQualityExposure|Distance:BackupGeneration:Scale]&quot;,
  &quot;[TrafficExposure|Distance:ConstructionIntensity]&quot;,
  &quot;[VisualLandUseExposure|Distance:LandFootprint]&quot;,
  &quot;[AccessToBenefits|Distance:Scale]&quot;,
  &quot;[GridStress|ElectricityDemand:GridCapacity]&quot;,
  &quot;[ElectricityCostRisk|GridStress:Distance]&quot;,
  &quot;[ReliabilityRisk|GridStress]&quot;,
  &quot;[WaterCompetition|WaterDemand:WaterAvailability]&quot;,
  &quot;[WaterSecurityRisk|WaterCompetition:Vulnerability]&quot;,
  &quot;[HousingPressure|WorkforceDemand:HousingAvailability]&quot;,
  &quot;[ServicePressure|WorkforceDemand:HousingAvailability]&quot;,
  &quot;[EnvironmentalDegradation|LandFootprint:WaterDemand:WaterAvailability]&quot;,
  &quot;[QualityOfLifeRisk|NoiseExposure:AirQualityExposure:TrafficExposure:&quot;,
  &quot;VisualLandUseExposure:HousingPressure:Vulnerability]&quot;,
  &quot;[EconomicBenefit|Scale:AccessToBenefits]&quot;,
  &quot;[OverallCommunityRisk|QualityOfLifeRisk:WaterSecurityRisk:&quot;,
  &quot;ElectricityCostRisk:ReliabilityRisk:EnvironmentalDegradation:&quot;,
  &quot;HousingPressure:ServicePressure:EconomicBenefit]&quot;
)

dag &lt;- model2network(network_string)

# Optional inspection:
#print(dag)
#print(modelstring(dag))

#list(nodes(dag))
#The `arcs()` function explicitly lists the from/to structure of the network.

arcs_dag &lt;- as.data.frame(arcs(dag))
#rownames(arcs_dag) &lt;- NULL
#print(arcs_dag[order(arcs_dag$from),],row.names=FALSE)</pre>
</details>
</div>
</section>
<section id="short-names-for-nodes" class="level3">
<h3 class="anchored" data-anchor-id="short-names-for-nodes">Short Names for Nodes</h3>
<p>Because the descriptive names given to the nodes may be too long to for plotting the DAG and displaying results this section of code creates a second DAG with identical structure to the first but uses short aliases the node names.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre># Replace each original node name with its alias.
# The lookaround expressions ensure that only complete node names are replaced.

# Alias mapping: original node name = abbreviated node name
node_alias &lt;- c(
  Distance                  = &quot;dst&quot;,
  Scale                     = &quot;scl&quot;,
  Cooling                   = &quot;clg&quot;,
  GridCapacity              = &quot;grd&quot;,
  WaterAvailability         = &quot;wat&quot;,
  HousingAvailability       = &quot;hou&quot;,
  Vulnerability              = &quot;vul&quot;,
  LandFootprint              = &quot;lnd&quot;,
  ElectricityDemand         = &quot;eld&quot;,
  WaterDemand               = &quot;wdr&quot;,
  ConstructionIntensity     = &quot;con&quot;,
  BackupGeneration          = &quot;gen&quot;,
  WorkforceDemand           = &quot;wrk&quot;,
  NoiseExposure             = &quot;noi&quot;,
  AirQualityExposure        = &quot;air&quot;,
  TrafficExposure           = &quot;trf&quot;,
  VisualLandUseExposure     = &quot;vis&quot;,
  AccessToBenefits          = &quot;ben&quot;,
  GridStress                = &quot;gst&quot;,
  ElectricityCostRisk       = &quot;ecr&quot;,
  ReliabilityRisk           = &quot;rel&quot;,
  WaterCompetition          = &quot;wcp&quot;,
  WaterSecurityRisk         = &quot;wsr&quot;,
  HousingPressure           = &quot;hpr&quot;,
  ServicePressure           = &quot;spr&quot;,
  EnvironmentalDegradation  = &quot;env&quot;,
  QualityOfLifeRisk         = &quot;qol&quot;,
  EconomicBenefit            = &quot;eco&quot;,
  OverallCommunityRisk      = &quot;ocr&quot;
)
network_string_alias &lt;- network_string

for (node_name in names(node_alias)) {
  alias_name &lt;- unname(node_alias[node_name])

  pattern &lt;- paste0(
    &quot;(?&lt;![A-Za-z0-9_.])&quot;,
    node_name,
    &quot;(?![A-Za-z0-9_.])&quot;
  )

  network_string_alias &lt;- gsub(
    pattern,
    alias_name,
    network_string_alias,
    perl = TRUE
  )
}

# Create the original DAG
dag &lt;- model2network(network_string)

# Create the DAG using aliases
dag_alias &lt;- model2network(network_string_alias)

# View the aliased network string
#network_string_alias

# View the aliased DAG
#dag_alias</pre>
</details>
</div>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre>graphviz.plot(dag_alias,
              shape = &quot;ellipse&quot;,
              main = &quot;Data-Center Community-Risk Bayesian Network&quot;)</pre>
</details>
<div class="cell-output-display">
<div>
<figure class="figure">
<p><a href="https://i0.wp.com/rworks.dev/posts/data-center-risk-model/index_files/figure-html/unnamed-chunk-5-1.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-1" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/data-center-risk-model/index_files/figure-html/unnamed-chunk-5-1.png?w=450&#038;ssl=1" class="img-fluid figure-img"  data-recalc-dims="1"></a></p>
</figure>
</div>
</div>
</div>
</section>
<section id="some-dag-statistics" class="level3">
<h3 class="anchored" data-anchor-id="some-dag-statistics">Some DAG Statistics</h3>
<p>There are a few statistics that may provide some insight into the structure of a Bayesian Network.</p>
<section id="average-markov-blanket-size" class="level4">
<h4 class="anchored" data-anchor-id="average-markov-blanket-size">Average Markov Blanket Size</h4>
<p>In a Bayesian network, a Markov blanket of a target variable consists of the variable’s parent nodes, child nodes, and spouses child nodes, the other parents of the children. These are the nodes that determine the target variables behavior. In general a smaller value for average Markov blanket size indicates a sparse network and lower computational complexity (the computational power required for probabilistic inference)</p>
</section>
<section id="average-branching-factor" class="level4">
<h4 class="anchored" data-anchor-id="average-branching-factor">Average Branching Factor</h4>
<p>The branching factor of a node is equal to the number of direct children. The Average Branching Factor is the total number of directed edges divided by the total number of nodes. This may be interpreted as the average spread of downstream influence.</p>
</section>
<section id="average-neighborhood-size" class="level4">
<h4 class="anchored" data-anchor-id="average-neighborhood-size">Average Neighborhood Size</h4>
<p>The neighborhood of a node consists of both its direct parents and its direct children, treating the graph as undirected. Average neighborhood size is the average number of incoming and outgoing connections.</p>
<p>This block of codes computes these statistics, which may be useful in comparing variations of the network.</p>
<div class="cell">
<details class="code-fold">
<summary>Network Statistics Calculations</summary>
<pre>#library(bnlearn)

# dag should be a bnlearn network created with model2network(),
# empty.graph(), hc(), tabu(), gs(), etc.

node_names &lt;- nodes(dag)

# Markov blanket size for each node
markov_blanket_sizes &lt;- sapply(
  node_names,
  function(node) {
    length(mb(dag, node))
  }
)

# Direct neighbours = parents + children
neighbourhood_sizes &lt;- sapply(
  node_names,
  function(node) {
    direct_neighbours &lt;- unique(c(
      parents(dag, node),
      children(dag, node)
    ))
    
    length(direct_neighbours)
  }
)

# Branching factor = number of children
branching_factors &lt;- sapply(
  node_names,
  function(node) {
    length(children(dag, node))
  }
)

# Network-level averages
network_statistics &lt;- data.frame(
  Statistic = c(
    &quot;Average Markov blanket size&quot;,
    &quot;Average neighbourhood size&quot;,
    &quot;Average branching factor&quot;
  ),
  Value = c(
    mean(markov_blanket_sizes),
    mean(neighbourhood_sizes),
    mean(branching_factors)
  )
)

network_statistics$Value &lt;- round(
  network_statistics$Value,
  digits = 2
)

network_statistics</pre>
</details>
<div class="cell-output cell-output-stdout">
<pre>                    Statistic Value
1 Average Markov blanket size  7.31
2  Average neighbourhood size  3.52
3    Average branching factor  1.76</pre>
</div>
</div>
<p>The code to calculate Markov blanket size, neighborhood size and branching factor for each node is in this code block, but is commented out as a default.</p>
<div class="cell">
<details class="code-fold">
<summary>Individual Node Statistics Calculations</summary>
<pre>node_statistics &lt;- data.frame(
  Node = node_names,
  MB_Size = as.numeric(markov_blanket_sizes),
  Neigh_Size = as.numeric(neighbourhood_sizes),
  BF_Size = as.numeric(branching_factors),
  row.names = NULL
)

#node_statistics</pre>
</details>
</div>
</section>
</section>
</section>
<section id="synthetic-data" class="level2">
<h2 class="anchored" data-anchor-id="synthetic-data">Synthetic Data</h2>
<p>This code implements a custom structural risk-propagation simulator to generate synthetic data based on a Directed Acyclic Graph (DAG), estimates for the discrete probability distributions foverning the root nodes and influencer nodes, estimates of initial risk scores. Instead of relying on traditional Conditional Probability Tables this architecture utilizes a continuous, score-based risk-mapping framework. The simulation engine evaluates the nodes observation-by-observation in a strict topological ordering (parents before children). For each node, it calculates a localized Target Risk Score bounded between 0 and 1 (where 0 represents lowest risk/exposure and 1 represents highest). It then converts this target risk into discrete state probabilities using an exponential distance formula. This allows the network to linearly model highly complex, multi-parent conditional relationships.</p>
<section id="structure-of-the-data-generator" class="level3">
<h3 class="anchored" data-anchor-id="structure-of-the-data-generator">Structure of the Data Generator</h3>
<p>The data generation algorithm divides the DAG nodes into two distinct behavioral categories based on how their state-to-risk mappings are handled:</p>
<table class="caption-top table">
<colgroup>
<col style="width: 33%">
<col style="width: 33%">
<col style="width: 33%">
</colgroup>
<thead>
<tr class="header">
<th>Node Type</th>
<th>Definition & Purpose</th>
<th>Code Implementation</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Direct Nodes</td>
<td>Baseline operational variables that have specific impacts on risk. Their states do not follow a uniform scale. Protective variables (like EconomicBenefit or AccessToBenefits) map inversely, where high values equal low risk.</td>
<td>Explicitly Mapped inside create_risk_mappings() via custom, hand-tuned numeric vectors (e.g., Distance = c(Adjacent = 1.00, Neighborhood = 0.80, …)).</td>
</tr>
<tr class="even">
<td>Derived Nodes</td>
<td>Downstream environmental, social, infrastructure, or ultimate risk outcomes. They track consecutive intensities or severities that naturally follow a uniform logical progression from safe to severe.</td>
<td>Dynamically Populated using a single, reusable standard_mapping array (Low = 0.15, Moderate = 0.50, High = 0.85) iterated over a list of standard_risk_nodes.</td>
</tr>
</tbody>
</table>
<p>Prior probabilities are only assigned to root states (states that have no parents). For non-root (child) nodes, the distributions of probabilities for states like “Low”, “Moderate”, and “High” are not static. These are generated dynamically for every single row using a 4-step pipeline that maps parental risk onto an exponential distance curve.</p>
<section id="step-1-compress-parent-states-into-a-single-risk-score" class="level4">
<h4 class="anchored" data-anchor-id="step-1-compress-parent-states-into-a-single-risk-score">Step 1: Compress Parent States into a Single Risk Score</h4>
<p>The code looks at the already-generated states of the child’s parents in the current row. It translates those text states into numeric risk values using <code>create_risk_mappings()</code> and calculates a weighted average:</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Ctext%7BParent%5C_Risk%7D%20=%20%5Cfrac%7B%5Csum_%7Bi=1%7D%5E%7Bm%7D%20%5Ctext%7BParent%5C_Score%7D_%7Bi%7D%20%5Ctimes%20%5Ctext%7BParent%5C_Weight%7D_%7Bi%7D%7D%7B%5Csum_%7Bi=1%7D%5E%7Bm%7D%20%5Ctext%7BParent%5C_Weight%7D_%7Bi%7D%7D%0A"></p>
<p>This compresses multiple complex parents into a single, scalar value between <img src="https://latex.codecogs.com/png.latex?(0.00"> and <img src="https://latex.codecogs.com/png.latex?1.00">.</p>
</section>
<section id="step-2-scale-and-add-environmental-noise" class="level4">
<h4 class="anchored" data-anchor-id="step-2-scale-and-add-environmental-noise">Step 2: Scale and Add Environmental Noise</h4>
<p>Next, the function <code>calculate_state_probabilities()</code> adjusts this base parent risk to account for structural sensitivity, global scenario shifts, and inevitable real-world randomness: for example</p>
<p><img src="https://latex.codecogs.com/png.latex?%5Ctext%7Btarget%5C_risk%7D%20=%20%5Ctext%7Bconditional%5C_risk%7D%20+%20%5Ctext%7Bglobal%5C_risk%5C_shift%7D%20+%20N(%5Cmu%20=%200,%20%5Csigma%20=%201)"></p>
<p>For each child node risk assignment:</p>
<ul>
<li>Risk is moved up up or depending on the value <code>global_risk_shift</code>.</li>
<li>Random Gaussian noise is injected</li>
<li>The final value is forced to stay within the strict logical bounds:</li>
</ul>
<p><img src="https://latex.codecogs.com/png.latex?%5Ctext%7Btarget%5C_risk%7D=max%20(0.01,%5Cmin%20(%5Ctext%7Btarget%5C_risk%7D_%7B%5Ctext%7Bnoisy%7D%7D,0.99))"></p>
</section>
<section id="step-3-measure-exponential-distance-the-core-equation" class="level4">
<h4 class="anchored" data-anchor-id="step-3-measure-exponential-distance-the-core-equation">Step 3: Measure Exponential Distance (The Core Equation)</h4>
<p>The probabilities for the child states are set as follows. The code calculates how far each possible child state’s predefined risk score (e.g., <img src="https://latex.codecogs.com/png.latex?%5Ctext%7BLow%7D%20=%200.15">, <img src="https://latex.codecogs.com/png.latex?%5Ctext%7BModerate%7D%20=%200.50">, <img src="https://latex.codecogs.com/png.latex?%5Ctext%7BHigh%7D%20=%200.85">) is from the dynamic <img src="https://latex.codecogs.com/png.latex?%5Ctext%7BTarget%5C%20Risk%7D"> calculated in Step 2. It runs these distances through a Laplacian radial kernel.:</p>
<p><img src="https://latex.codecogs.com/png.latex?P_%7B%5Ctext%7Braw%7D%7D(%5Ctext%7Bstate%7D)%20=%20e%5E%7B-c%20%5Ccdot%20%7C%5Ctext%7BState%20Score%7D%20-%20%5Ctext%7BTarget%20Risk%7D%7C%7D"></p>
<p>Where:</p>
<ul>
<li><img src="https://latex.codecogs.com/png.latex?c%20=%206"> represents the concentration constant.</li>
<li>This steepness filter ensures that states mathematically closest to the <img src="https://latex.codecogs.com/png.latex?%5Ctext%7BTarget%5C%20Risk%7D"> get an exponentially larger value, while states far away drop rapidly toward zero.</li>
</ul>
</section>
<section id="step-4-normalize-to-sum-to-1.00" class="level4">
<h4 class="anchored" data-anchor-id="step-4-normalize-to-sum-to-1.00">Step 4: Normalize to Sum to 1.00</h4>
<p>Because exponential distance calculations do not naturally yield valid probabilities, the raw scores are passed to <code>normalize_probabilities()</code>. The code sums up all the raw exponential values for that node and divides each individual score by that sum:</p>
<p><img src="https://latex.codecogs.com/png.latex?P_%7B%5Ctext%7Bfinal%7D%7D(%5Ctext%7Bstate%7D)=%5Cfrac%7BP_%7B%5Ctext%7Braw%7D%7D(%5Ctext%7Bstate%7D)%7D%7B%5Csum%20_%7Bj%5Cin%20%5Ctext%7BStates%7D%7DP_%7B%5Ctext%7Braw%7D%7D(j)%7D"></p>
<p>This guarantees that the final array of weights for Low, Moderate, and High satisfies the fundamental probability axiom: <img src="https://latex.codecogs.com/png.latex?%5Csum%20_%7Bj%5Cin%20%5Ctext%7BStates%7D%7DP_%7B%5Ctext%7Bfinal%7D%7D(j)=1.00"></p>
<p>The resulting vector is ready for the <code>sample()</code> function to execute a weighted random draw for that row.</p>
</section>
</section>
<section id="data-generator-code" class="level3">
<h3 class="anchored" data-anchor-id="data-generator-code">Data Generator Code</h3>
<p>The following code implements the code to generate the data, and contains the default values for the probabilities of root node distributions of “Low”, “Moderate” and “High” risk and default values for risk estimates.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre># ================================================================
# Synthetic data generation from a bnlearn DAG
# ================================================================

#library(bnlearn)

# ----------------------------------------------------------------
# Main function
# ----------------------------------------------------------------
generate_synthetic_data &lt;- function(
  n = 10000, 
  global_risk_shift = 0.00, 
  parent_effect_strength = 1.00, 
  conditional_noise = 0.10
) {
  # ARCHITECTURAL NOTE: Dynamic environment lookup lets the function bypass bnlearn's 
  # restrictive function signatures while maintaining access to the global network structure.
  if (!exists(&quot;dag&quot;, envir = parent.frame(), inherits = TRUE)) {
    stop(&quot;An object named 'dag' must exist before calling generate_synthetic_data().&quot;)
  }
  dag &lt;- get(&quot;dag&quot;, envir = parent.frame(), inherits = TRUE)
  
  # Structural defensive validation steps
  validate_dag(dag)
  validate_generator_arguments(n, global_risk_shift, parent_effect_strength, conditional_noise)
  
  # Bundle configurations into a centralized configuration object
  config &lt;- create_network_config(
    n = n, 
    global_risk_shift = global_risk_shift, 
    parent_effect_strength = parent_effect_strength, 
    conditional_noise = conditional_noise
  )
  
  # Execute the core simulation pipeline
  generate_rows_from_dag(dag = dag, config = config)
}

# ----------------------------------------------------------------
# Configuration
# ----------------------------------------------------------------
create_network_config &lt;- function(n, global_risk_shift, parent_effect_strength, conditional_noise, seed = 20260921) {
  list(
    n = n,
    seed = seed,
    global_risk_shift = global_risk_shift,
    parent_effect_strength = parent_effect_strength,
    conditional_noise = conditional_noise,
    states = create_node_states(),
    root_probabilities = create_root_probabilities(),
    
    # PARENT WEIGHTING MATRIX: Allows specific upstream nodes to exert a stronger 
    # or weaker relative pull on their children during the weighted average calculation.
    parent_weights = c(
      Distance = 1.20, Scale = 1.00, Cooling = 1.00, GridCapacity = 1.00, 
      HousingAvailability = 1.00, Vulnerability = 1.10, WaterAvailability = 1.00
    ),
    risk_mappings = create_risk_mappings()
  )
}

create_node_states &lt;- function() {
  # Definition of ordinal categorical states for every node in the entire network universe
  list(
    Distance = c(&quot;Adjacent&quot;, &quot;Neighborhood&quot;, &quot;Municipality&quot;, &quot;Watershed&quot;, &quot;Regional&quot;, &quot;Global&quot;),
    Scale = c(&quot;Small&quot;, &quot;Medium&quot;, &quot;Large&quot;, &quot;Hyperscale&quot;),
    Cooling = c(&quot;AirCooled&quot;, &quot;Hybrid&quot;, &quot;Evaporative&quot;, &quot;WaterIntensive&quot;),
    GridCapacity = c(&quot;Adequate&quot;, &quot;Constrained&quot;, &quot;SeverelyConstrained&quot;),
    HousingAvailability = c(&quot;Adequate&quot;, &quot;Tight&quot;, &quot;SeverelyConstrained&quot;),
    Vulnerability = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;),
    WaterAvailability = c(&quot;Abundant&quot;, &quot;SeasonalStress&quot;, &quot;Scarce&quot;),
    AccessToBenefits = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;),
    BackupGeneration = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;Extensive&quot;),
    ConstructionIntensity = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;),
    ElectricityDemand = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;, &quot;Extreme&quot;),
    LandFootprint = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;),
    NoiseExposure = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;),
    WaterDemand = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;),
    WorkforceDemand = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;),
    AirQualityExposure = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;),
    EconomicBenefit = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;),
    EnvironmentalDegradation = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;),
    GridStress = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;),
    HousingPressure = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;),
    ServicePressure = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;),
    TrafficExposure = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;),
    VisualLandUseExposure = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;),
    WaterCompetition = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;),
    ElectricityCostRisk = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;),
    QualityOfLifeRisk = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;),
    ReliabilityRisk = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;),
    WaterSecurityRisk = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;),
    OverallCommunityRisk = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;)
  )
}

# create_root_probabilities sets the probability distributions for how frequently these sates occur.
create_root_probabilities &lt;- function() {
  # Baseline categorical probability distributions applied strictly to exogenous Root Nodes (no parents)
  list(
    Distance = c(Adjacent = 0.10, Neighborhood = 0.18, 
                 Municipality = 0.25, Watershed = 0.22, Regional = 0.18, Global = 0.07),
    Scale = c(Small = 0.15, Medium = 0.35, Large = 0.35, Hyperscale = 0.15),
    Cooling = c(AirCooled = 0.30, Hybrid = 0.30, Evaporative = 0.25, WaterIntensive = 0.15),
    GridCapacity = c(Adequate = 0.50, Constrained = 0.35, SeverelyConstrained = 0.15),
    HousingAvailability = c(Adequate = 0.45, Tight = 0.35, SeverelyConstrained = 0.20),
    Vulnerability = c(Low = 0.45, Moderate = 0.35, High = 0.20),
    WaterAvailability = c(Abundant = 0.45, SeasonalStress = 0.35, Scarce = 0.20)
  )
}

# risk mappings are not probabilities. They do not sum to 1.0. They quantify the downstream impact or &quot;hazard level&quot; of a state once it has been selected. 
create_risk_mappings &lt;- function() {
  # DIRECT NODES: Hand-calibrated risk translations mapping structural categories to numeric impacts.
  mappings &lt;- list(
    Distance = c(Adjacent = 1.00, Neighborhood = 0.80, Municipality = 0.45, Watershed = 0.25, Regional = 0.10, Global = 0.02),
    Scale = c(Small = 0.25, Medium = 0.50, Large = 0.75, Hyperscale = 1.00),
    Cooling = c(AirCooled = 0.25, Hybrid = 0.55, Evaporative = 0.75, WaterIntensive = 0.95),
    GridCapacity = c(Adequate = 0.10, Constrained = 0.55, SeverelyConstrained = 0.95),
    HousingAvailability = c(Adequate = 0.10, Tight = 0.60, SeverelyConstrained = 0.95),
    Vulnerability = c(Low = 0.10, Moderate = 0.55, High = 0.95),
    WaterAvailability = c(Abundant = 0.10, SeasonalStress = 0.60, Scarce = 0.95),
    LandFootprint = c(Low = 0.25, Moderate = 0.60, High = 1.00),
    ElectricityDemand = c(Low = 0.15, Moderate = 0.40, High = 0.70, Extreme = 1.00),
    WaterDemand = c(Low = 0.15, Moderate = 0.55, High = 1.00),
    ConstructionIntensity = c(Low = 0.25, Moderate = 0.60, High = 1.00),
    BackupGeneration = c(Low = 0.25, Moderate = 0.60, Extensive = 1.00),
    WorkforceDemand = c(Low = 0.15, Moderate = 0.55, High = 1.00),
    
    # PROTECTIVE VARIABLE CONVERSION: Inverts risk mapping so high benefits lower the downstream risk score
    AccessToBenefits = c(Low = 0.85, Moderate = 0.50, High = 0.15),
    EconomicBenefit = c(Low = 0.85, Moderate = 0.50, High = 0.15)
  )
  
  # DERIVED NODES: Programmatic batch generation of standardized downstream risk states
  standard_risk_nodes &lt;- c(
    &quot;NoiseExposure&quot;, &quot;AirQualityExposure&quot;, &quot;TrafficExposure&quot;, &quot;VisualLandUseExposure&quot;, 
    &quot;EnvironmentalDegradation&quot;, &quot;GridStress&quot;, &quot;HousingPressure&quot;, &quot;ServicePressure&quot;, 
    &quot;WaterCompetition&quot;, &quot;ElectricityCostRisk&quot;, &quot;QualityOfLifeRisk&quot;, &quot;ReliabilityRisk&quot;, 
    &quot;WaterSecurityRisk&quot;, &quot;OverallCommunityRisk&quot;
  )
  standard_mapping &lt;- c(Low = 0.15, Moderate = 0.50, High = 0.85)
  for (node in standard_risk_nodes) {
    mappings[[node]] &lt;- standard_mapping
  }
  mappings
}

# ----------------------------------------------------------------
# Generate all observations
# ----------------------------------------------------------------
generate_rows_from_dag &lt;- function(dag, config) {
  if (!is.null(config$seed)) { set.seed(config$seed) }
  
  dag_nodes &lt;- bnlearn::nodes(dag)
  
  # CRITICAL ARCHITECTURAL STEP: Nodes must be sorted topologically. 
  # This guarantees a parent node has simulated data before a child tries to read it.
  generation_order &lt;- get_topological_order(dag)
  
  validate_configuration(dag = dag, dag_nodes = dag_nodes, config = config)
  
  # OPTIMIZATION: Memory pre-allocation prevents R from copying data frames repeatedly in memory loops.
  result &lt;- setNames(vector(&quot;list&quot;, length(dag_nodes)), dag_nodes)
  for (node in dag_nodes) {
    result[[node]] &lt;- character(config$n)
  }
  
  # Row-by-row simulation loop
  for (i in seq_len(config$n)) {
    one_row &lt;- generate_one_observation(dag = dag, generation_order = generation_order, config = config)
    for (node in dag_nodes) {
      result[[node]][i] &lt;- one_row[[node]]
    }
  }
  
  # Formatting raw arrays back into structured, clean R Factor columns
  result &lt;- as.data.frame(result, stringsAsFactors = FALSE, check.names = FALSE)
  for (node in dag_nodes) {
    result[[node]] &lt;- factor(result[[node]], levels = config$states[[node]])
  }
  result
}

generate_one_observation &lt;- function(dag, generation_order, config) {
  row &lt;- setNames(vector(&quot;list&quot;, length(generation_order)), generation_order)
  
  # Sequentially process each node down the causal chain
  for (node in generation_order) {
    node_states &lt;- config$states[[node]]
    node_parents &lt;- bnlearn::parents(dag, node)
    
    # ROOT NODE INTERCEPT: If a node has no parents, sample from its static baseline distribution.
    if (length(node_parents) == 0) {
      probabilities &lt;- config$root_probabilities[[node]]
      probabilities &lt;- probabilities[node_states]
      probabilities &lt;- normalize_probabilities(probabilities)
      names(probabilities) &lt;- node_states
      row[[node]] &lt;- sample_state(probabilities)
      next
    }
    
    # CHILD NODE COMPUTATION: Compress multi-parent states into a singular continuous score
    parent_risk &lt;- calculate_parent_risk(parents = node_parents, row = row, config = config)
    
    # LINEAR SENSITIVITY TRANSFORM: Shifts values relative to a 0.5 mid-point based on network effects strength
    conditional_risk &lt;- 0.5 + config$parent_effect_strength * (parent_risk - 0.5)
    
    # Map the scalar risk target back into a probability distribution across discrete states
    probabilities &lt;- calculate_state_probabilities(
      node = node, states = node_states, 
      conditional_risk = conditional_risk, config = config
    )
    row[[node]] &lt;- sample_state(probabilities)
  }
  row
}

# ----------------------------------------------------------------
# Parent-risk calculations

## ----------------------------------------------------------------
calculate_parent_risk &lt;- function(parents, row, config) {
## Extract configured or default (1.00) relative weights for each parent
parent_weights &lt;- vapply(parents, function(parent) {
get_parent_weight(parent = parent, config = config)
}, numeric(1))
## Convert each parent's current simulated text state into its corresponding numeric risk score
weighted_scores &lt;- vapply(seq_along(parents), function(i) {
parent &lt;- parents[[i]]
parent_score &lt;- state_risk_score(node = parent, state = row[[parent]], config = config)
parent_score * parent_weights[[i]]
}, numeric(1))
## COMPRESSION METRIC: Calculated as a weighted average to safely scale multi-parent risks between 0 and 1
sum(weighted_scores) / sum(parent_weights)
}
get_parent_weight &lt;- function(parent, config) {
weights &lt;- config$parent_weights
## Defensive safety check guarding against structural mismatches between the input DAG and configuration vectors
if (is.null(weights) || is.null(names(weights)) || !parent %in% names(weights)) {
return(1.00)
}
weight &lt;- unname(weights[[parent]])
if (!is.finite(weight) || weight &lt; 0) {
stop(&quot;The weight for parent '&quot;, parent, &quot;' must be a finite, non-negative number.&quot;)
}
weight
}
## ----------------------------------------------------------------## Conditional probability calculations## ----------------------------------------------------------------
calculate_state_probabilities &lt;- function(node, states, conditional_risk, config) {
## STOCHASTIC PERTURBATION: Incorporates systemic shifts (scenarios) and random environmental noise via Gaussian distributions.
target_risk &lt;- conditional_risk + config$global_risk_shift + rnorm(n = 1, mean = 0, sd = config$conditional_noise)
## Hard boundaries preventing mathematically invalid probabilities outside (0, 1)
target_risk &lt;- max(0.01, min(target_risk, 0.99))
state_scores &lt;- vapply(states, function(state) {
state_risk_score(node = node, state = state, config = config)
}, numeric(1) )
## EXPONENTIAL DISTANCE KERNEL (RBF-style): States whose static risk mappings sit closest## to the dynamic target_risk calculation receive exponentially higher generation probabilities.
concentration &lt;- 6
probabilities &lt;- exp(-concentration * abs(state_scores - target_risk))
## Convert distances to a standard, valid probability array summing to 1.00
probabilities &lt;- normalize_probabilities(probabilities)
names(probabilities) &lt;- states
probabilities
}
state_risk_score &lt;- function(node, state, config) {
mapping &lt;- config$risk_mappings[[node]]
if (is.null(mapping)) { stop(&quot;No risk mapping is defined for node '&quot;, node, &quot;'.&quot;) }
if (!state %in% names(mapping)) { stop(&quot;No risk score is defined for node '&quot;, node, &quot;' and state '&quot;, state, &quot;'.&quot;) }
unname(mapping[[state]])
}
normalize_probabilities &lt;- function(probabilities) {
probabilities &lt;- pmax(probabilities, 0)
if (length(probabilities) == 0) { stop(&quot;Cannot normalize an empty probability vector.&quot;) }
## Uniform fallback handling to preserve math stability if all states evaluate to a zero score
if (sum(probabilities) == 0) {
probabilities &lt;- rep(1 / length(probabilities), length(probabilities))
} else {
probabilities &lt;- probabilities / sum(probabilities)
}
probabilities
}
sample_state &lt;- function(probabilities) {
## Executes a standard weighted random draw based on the dynamically formulated distribution
sample(x = names(probabilities), size = 1, prob = probabilities)
}
## ----------------------------------------------------------------## Validation &#038; Topological ordering## ----------------------------------------------------------------
validate_dag &lt;- function(dag) {
if (!inherits(dag, &quot;bn&quot;)) { stop(&quot;dag must be a bnlearn network object with class 'bn'.&quot;) }
}
validate_generator_arguments &lt;- function(n, global_risk_shift, parent_effect_strength, conditional_noise) {
if (length(n) != 1 || !is.numeric(n) || is.na(n) || n &lt; 1 || n != as.integer(n)) { stop(&quot;n must be one positive integer.&quot;) }
numeric_values &lt;- c(global_risk_shift, parent_effect_strength, conditional_noise)
if (any(!is.numeric(numeric_values)) || any(!is.finite(numeric_values))) { stop(&quot;global_risk_shift, parent_effect_strength, and conditional_noise must be finite numeric values.&quot;) }
if (conditional_noise &lt; 0) { stop(&quot;conditional_noise must be non-negative.&quot;) }
}
validate_configuration &lt;- function(dag, dag_nodes, config) {
missing_states &lt;- setdiff(dag_nodes, names(config$states))
if (length(missing_states) &gt; 0) { stop(&quot;No state definitions supplied for: &quot;, paste(missing_states, collapse = &quot;, &quot;)) }
root_nodes &lt;- dag_nodes[vapply(dag_nodes, function(node) { length(bnlearn::parents(dag, node)) == 0 }, logical(1))]
missing_root_probabilities &lt;- setdiff(root_nodes, names(config$root_probabilities))
if (length(missing_root_probabilities) &gt; 0) { stop(&quot;No root probabilities supplied for: &quot;, paste(missing_root_probabilities, collapse = &quot;, &quot;)) }
missing_mappings &lt;- setdiff(dag_nodes, names(config$risk_mappings))
if (length(missing_mappings) &gt; 0) { stop(&quot;No risk mappings supplied for: &quot;, paste(missing_mappings, collapse = &quot;, &quot;)) }
}
get_topological_order &lt;- function(dag) {
## KAHN'S ALGORITHM EXTENSION: Resolves dependence hierarchies.## Iteratively extracts nodes whose parents have already crossed the resolution boundary.
dag_nodes &lt;- bnlearn::nodes(dag)
remaining_nodes &lt;- dag_nodes
ordering &lt;- character(0)
while (length(remaining_nodes) &gt; 0) {
available_nodes &lt;- remaining_nodes[vapply(remaining_nodes, function(node) {
node_parents &lt;- bnlearn::parents(dag, node)
all(node_parents %in% ordering)
}, logical(1))]
# If no nodes are free but un-generated items remain, a cyclic loop exists.
if (length(available_nodes) == 0) {
stop(&quot;The graph does not appear to be acyclic, or its parent relationships could not be resolved.&quot;)
}
available_nodes &lt;- sort(available_nodes)
ordering &lt;- c(ordering, available_nodes)
remaining_nodes &lt;- setdiff(remaining_nodes, available_nodes)
}
ordering
}</pre>
</details>
</div>
<p><strong>This code runs the function to generate the synthetic data.</strong> It is the place where changes can be made to increase the risk intensity of the probability distributions.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre># ================================================================
# Usage
# ================================================================
#
# A DAG named `dag` must exist first.

# Global scenario controls.
  # global_risk_shift
  # 0.00 = baseline moderate-risk scenario
  # Positive values increase risk throughout the network
  # Negative values decrease risk throughout the network
  
  #  parent_effect_strength
  # Controls how strongly parent risk affects child risk.
  # 1.00 is the default.
 
  # conditional_noise 
  # Controls random variation in conditional probabilities.
  # Larger values produce less deterministic relationships.

#
synthetic &lt;- generate_synthetic_data(
  n = 5000,
  global_risk_shift = 0.00,
  parent_effect_strength = 1.0,
  conditional_noise = 0.10
)
#
# str(synthetic)
# head(synthetic)
# ================================================================</pre>
</details>
</div>
</section>
<section id="check-the-data" class="level3">
<h3 class="anchored" data-anchor-id="check-the-data">Check the Data</h3>
<p>You can also easily write queries to check that the proportions of data comprising the states for each node of the simulated match the input distributions that were used to create the data. For example the following code shows a query to check distance.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre>state_proportions &lt;- function(data) {
  lapply(data, function(x) {
    round(prop.table(table(x)), 3)
  })
}

proportions &lt;- state_proportions(synthetic)
proportions$Distance</pre>
</details>
<div class="cell-output cell-output-stdout">
<pre>x
    Adjacent Neighborhood Municipality    Watershed     Regional       Global 
       0.100        0.166        0.253        0.231        0.179        0.071 </pre>
</div>
<details class="code-fold">
<summary>Show the code</summary>
<pre>data_summary_table &lt;- do.call(
  rbind,
  lapply(names(synthetic), function(node) {
    counts &lt;- table(synthetic[[node]])
    proportions &lt;- prop.table(counts)

    data.frame(
      Node = node,
      State = names(counts),
      Count = as.integer(counts),
      Proportion = round(as.numeric(proportions), 3),
      row.names = NULL
    )
  })
)

#head(data_summary_table,10)</pre>
</details>
</div>
<p>These proportions match numbers which were inputs to the code above which generates the data frame <code>synthetic</code>.</p>
<p>Distance = c( Adjacent = 0.10, Neighborhood = 0.18, Municipality = 0.25, Watershed = 0.22, Regional = 0.18, Global = 0.07 ),</p>
<p>The above code also produces the table <code>data_summary_table</code> which formats the generated proportions for all of the nodes. The following table summarizes the probability distributions across the states of the risk specific nodes.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre>risk_nodes &lt;- c(
  &quot;GridStress&quot;,
  &quot;HousingPressure&quot;,
  &quot;ServicePressure&quot;,
  &quot;TrafficExposure&quot;,
  &quot;VisualLandUseExposure&quot;,
  &quot;WaterCompetition&quot;,
  &quot;ElectricityCostRisk&quot;,
  &quot;QualityOfLifeRisk&quot;,
  &quot;ReliabilityRisk&quot;,
  &quot;WaterSecurityRisk&quot;,
  &quot;OverallCommunityRisk&quot;
)

risk_summary &lt;- do.call(
  rbind,
  lapply(risk_nodes, function(node) {
    p &lt;- prop.table(table(synthetic[[node]]))

    data.frame(
      Node = node,
      Low = unname(p[&quot;Low&quot;]),
      Moderate = unname(p[&quot;Moderate&quot;]),
      High = unname(p[&quot;High&quot;])
    )
  })
)

risk_summary</pre>
</details>
<div class="cell-output cell-output-stdout">
<pre>                    Node    Low Moderate   High
1             GridStress 0.2788   0.4614 0.2598
2        HousingPressure 0.2562   0.4352 0.3086
3        ServicePressure 0.2438   0.4474 0.3088
4        TrafficExposure 0.2432   0.4764 0.2804
5  VisualLandUseExposure 0.2632   0.4674 0.2694
6       WaterCompetition 0.2602   0.4496 0.2902
7    ElectricityCostRisk 0.3258   0.4554 0.2188
8      QualityOfLifeRisk 0.2260   0.5550 0.2190
9        ReliabilityRisk 0.3070   0.4132 0.2798
10     WaterSecurityRisk 0.3138   0.4476 0.2386
11  OverallCommunityRisk 0.2068   0.5744 0.2188</pre>
</div>
</div>
<p>And, here is the probability distribution for the states of Overall Community Risk conditioned by scale of the data center.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre>round(
  with(
    synthetic,
    prop.table(table(Scale, OverallCommunityRisk), margin = 1)
  ),
  3
)</pre>
</details>
<div class="cell-output cell-output-stdout">
<pre>            OverallCommunityRisk
Scale          Low Moderate  High
  Small      0.350    0.549 0.100
  Medium     0.229    0.598 0.173
  Large      0.165    0.579 0.256
  Hyperscale 0.113    0.535 0.352</pre>
</div>
</div>
<p>Notice that the probabilities associated with the “Moderate” state of the multinomial distribution for Overall Community Risk to be higher than the probabilities for the “High” state at all level of Scale. These proportions are reflected in the chart below.</p>
</section>
<section id="visualizing-the-synthetic-data" class="level3">
<h3 class="anchored" data-anchor-id="visualizing-the-synthetic-data">Visualizing the synthetic data</h3>
<p>The following plot shows the relative proportions of “Low”, “Medium” and “High” risk states in the synthetic data for the risk nodes of the DAG. Notice that Overall Community Risk is broadly consistent with the other risk nodes that feed into it.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre># library(dplyr)
# library(tidyr)
# library(ggplot2)

risk_nodes &lt;- c(
  &quot;GridStress&quot;,
  &quot;HousingPressure&quot;,
  &quot;ServicePressure&quot;,
  &quot;TrafficExposure&quot;,
  &quot;VisualLandUseExposure&quot;,
  &quot;WaterCompetition&quot;,
  &quot;ElectricityCostRisk&quot;,
  &quot;QualityOfLifeRisk&quot;,
  &quot;ReliabilityRisk&quot;,
  &quot;WaterSecurityRisk&quot;,
  &quot;OverallCommunityRisk&quot;
)

risk_long &lt;- synthetic %&gt;%
  select(all_of(risk_nodes)) %&gt;%
  pivot_longer(
    cols = everything(),
    names_to = &quot;Node&quot;,
    values_to = &quot;Risk&quot;
  ) %&gt;%
  count(Node, Risk) %&gt;%
  group_by(Node) %&gt;%
  mutate(Proportion = n / sum(n)) %&gt;%
  ungroup()

risk_long$Risk &lt;- factor(
  risk_long$Risk,
  levels = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;)
)

risk_long$Node &lt;- factor(
  risk_long$Node,
  levels = rev(risk_nodes)
)

ggplot(risk_long, aes(x = Proportion, y = Node, fill = Risk)) +
  geom_col(width = 0.75) +
  scale_x_continuous(
    labels = scales::percent_format(),
    limits = c(0, 1)
  ) +
  scale_fill_manual(
    values = c(
      Low = &quot;#2CA25F&quot;,
      Moderate = &quot;#FDD835&quot;,
      High = &quot;#DE2D26&quot;
    )
  ) +
  labs(
    title = &quot;Distribution of Risk States Across Synthetic Data&quot;,
    x = &quot;Proportion of observations&quot;,
    y = NULL,
    fill = &quot;Risk state&quot;
  ) +
  theme_minimal(base_size = 12) +
  theme(
    legend.position = &quot;top&quot;,
    panel.grid.major.y = element_blank()
  )</pre>
</details>
<div class="cell-output-display">
<div>
<figure class="figure">
<p><a href="https://i2.wp.com/rworks.dev/posts/data-center-risk-model/index_files/figure-html/unnamed-chunk-13-1.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-2" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/data-center-risk-model/index_files/figure-html/unnamed-chunk-13-1.png?w=450&#038;ssl=1" class="img-fluid figure-img"  data-recalc-dims="1"></a></p>
</figure>
</div>
</div>
</div>
<p>This plot shows the nodes produce the most “High” risk proportions.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre>high_risk &lt;- synthetic %&gt;%
  select(all_of(risk_nodes)) %&gt;%
  summarise(
    across(
      everything(),
      ~ mean(.x == &quot;High&quot;)
    )
  ) %&gt;%
  pivot_longer(
    cols = everything(),
    names_to = &quot;Node&quot;,
    values_to = &quot;HighRiskProportion&quot;
  ) %&gt;%
  arrange(HighRiskProportion)

ggplot(
  high_risk,
  aes(
    x = HighRiskProportion,
    y = reorder(Node, HighRiskProportion)
  )
) +
  geom_col(fill = &quot;#DE2D26&quot;) +
  scale_x_continuous(
    labels = scales::percent_format(),
    limits = c(0, 1)
  ) +
  labs(
    title = &quot;Prevalence of High-Risk States&quot;,
    x = &quot;Observations classified as High&quot;,
    y = NULL
  ) +
  theme_minimal(base_size = 12)</pre>
</details>
<div class="cell-output-display">
<div>
<figure class="figure">
<p><a href="https://i0.wp.com/rworks.dev/posts/data-center-risk-model/index_files/figure-html/unnamed-chunk-14-1.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-3" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/data-center-risk-model/index_files/figure-html/unnamed-chunk-14-1.png?w=450&#038;ssl=1" class="img-fluid figure-img"  data-recalc-dims="1"></a></p>
</figure>
</div>
</div>
</div>
</section>
</section>
<section id="fit-the-dag" class="level2">
<h2 class="anchored" data-anchor-id="fit-the-dag">Fit the DAG</h2>
<p>So far, we have only been working with the raw synthetic data, evaluating the estimates of risk which were put into the model. We have not yet fit the Bayesian Network to the data. Fitting the data enables:</p>
<ul>
<li>Making Bayesian Network inferences</li>
<li>Making predictions</li>
<li>Estimating the conditional probability tables from the synthetic data</li>
</ul>
<p>Note that the method “bayes” used in the fit function means that the network is fit using “the classic Bayesian posterior estimator with a uniform prior matching that in the Bayesian Dirichlet equivalent (”bde”) score.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre># ------------------------------------------------------------
# 5. Fit the Bayesian Network
# ------------------------------------------------------------

fitted_bn &lt;- bn.fit(
  dag,
  data = synthetic,
  method = &quot;bayes&quot;,
  iss = 10
)</pre>
</details>
</div>
<section id="query-the-model" class="level3">
<h3 class="anchored" data-anchor-id="query-the-model">Query the Model</h3>
<p>This next code shows how to directly query the model. For example: What is the probability that overall community risk is “High” when Scale is hyperscale?</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre>## Inspect Overall Community Risk, or any other node directly from the object
query &lt;- cpquery(
  fitted_bn,
  event = (OverallCommunityRisk == &quot;High&quot;),
  evidence = (Scale == &quot;Hyperscale&quot;)
)

query</pre>
</details>
<div class="cell-output cell-output-stdout">
<pre>[1] 0.3472178</pre>
</div>
</div>
<p>Or, what is the probability that Quality of Life Risk when Distance = Adjacent?</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre>cpquery(
  fitted_bn,
  event = (QualityOfLifeRisk == &quot;High&quot;),
  evidence = (Distance == &quot;Adjacent&quot;)
)</pre>
</details>
<div class="cell-output cell-output-stdout">
<pre>[1] 0.4408929</pre>
</div>
</div>
</section>
<section id="make-predictions" class="level3">
<h3 class="anchored" data-anchor-id="make-predictions">Make Predictions</h3>
<p>Next we use the <code>bnlearn</code> predict function to bootstrap predictions of risk for each of the three risk states, “Low”, “Medium”, and “High”, over which probability distributions are computed for each node. The function call uses the default method method of prediction, “parents”, which computes the predicted values by “plugging in the new values for the parents of node in the local probability distribution of node extracted from fitted.”</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre>predicted &lt;- predict(
  fitted_bn,
  node = &quot;OverallCommunityRisk&quot;,
  data = synthetic,
  prob = TRUE,
  method = &quot;parents&quot;
)

# 1. Extract and transpose the probability matrix
probs &lt;- t(attr(predicted, &quot;prob&quot;))
# 2. Assign column names for clarity
colnames(probs) &lt;- c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;)
# 3. Generate summary statistics (Min, 1st Qu., Median, Mean, 3rd Qu., Max)
#summary(probs)
rounded_summary &lt;- sapply(as.data.frame(probs), summary)
rounded_summary &lt;- round(rounded_summary, digits = 4)
rounded_summary</pre>
</details>
<div class="cell-output cell-output-stdout">
<pre>           Low Moderate   High
Min.    0.0000   0.0001 0.0001
1st Qu. 0.0003   0.3333 0.0003
Median  0.0005   0.5000 0.0005
Mean    0.2069   0.5742 0.2189
3rd Qu. 0.3333   0.9990 0.3750
Max.    0.9998   0.9998 0.9998</pre>
</div>
</div>
<p>A first try to visualize the data might be to draw boxplots.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre># 1. Extract the probability matrix and convert to a data frame
prob_matrix &lt;- t(attr(predicted, &quot;prob&quot;))
colnames(prob_matrix) &lt;- c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;)
df_wide &lt;- as.data.frame(prob_matrix)

# 2. Reshape data from WIDE to LONG format for ggplot2
df_long &lt;- df_wide %&gt;%
  pivot_longer(
    cols = everything(), 
    names_to = &quot;Risk_Category&quot;, 
    values_to = &quot;Probability&quot;
  ) %&gt;%
  # Ensure the categories stay in a logical order (Low -&gt; Moderate -&gt; High)
  mutate(Risk_Category = factor(Risk_Category, levels = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;)))


# 3. Create the ggplot Boxplot
ggplot(df_long, aes(x = Risk_Category, y = Probability, fill = Risk_Category)) +
  # Draw the boxplots with custom outlier styling
  geom_boxplot(
    alpha = 0.7, 
    outlier.color = &quot;grey60&quot;, 
    outlier.size = 1.5, 
    outlier.shape = 16
  ) +
  # Overlay a diamond indicating the MEAN probability for each category
  stat_summary(fun = mean, geom = &quot;point&quot;, shape = 18, size = 4, color = &quot;black&quot;) +
  # Clean, minimal visual theme
  #theme_minimal(base_size = 13) +
  # Custom color palette (Green = Low, Yellow/Orange = Moderate, Red = High)
  scale_fill_manual(values = c(&quot;Low&quot; = &quot;#2ecc71&quot;, &quot;Moderate&quot; = &quot;#f39c12&quot;, &quot;High&quot; = &quot;#e74c3c&quot;)) +
  # Descriptive labels
  labs(
    title = &quot;Comparison of Risk Category Probabilities&quot;,
    subtitle = &quot;Boxplots showing medians and quartiles; black diamonds represent the mean&quot;,
    x = &quot;Risk Category&quot;,
    y = &quot;Predicted Probability (0 to 1)&quot;
  ) +
  # Remove the legend since the x-axis already labels the categories
  theme(legend.position = &quot;none&quot;)</pre>
</details>
<div class="cell-output-display">
<div>
<figure class="figure">
<p><a href="https://i2.wp.com/rworks.dev/posts/data-center-risk-model/index_files/figure-html/unnamed-chunk-19-1.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-4" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/data-center-risk-model/index_files/figure-html/unnamed-chunk-19-1.png?w=450&#038;ssl=1" class="img-fluid figure-img"  data-recalc-dims="1"></a></p>
</figure>
</div>
</div>
</div>
<p>However, while boxplots can convey some idea about what is going on with the prediction data, it is important to remember that the predictions are not independently distributed they are constrained: <img src="https://latex.codecogs.com/png.latex?P(%5Ctext%7BLow%7D)%20+%20P(%5Ctext%7BModerate%7D)%20+%20P%5Ctext%7B(High%7D)%20=%201">. Boxplots cannot convey the shifting trade-offs between the three dependent probability states across individual bootstrap runs.</p>
<p>A better choice would be computing a tertiary plot which can be interpreted as follows.</p>
<ul>
<li>The apexes indicate “Low” “Moderate” and “High” risk so dots and heat contours that gather tightly in the top corner would be predicting a universally high risk profile the bootstraps.</li>
<li>If the dots or contour heat map center lands square in the middle of the triangle then the network is predicting a uniform distribution (≈ 33.3% split across Low, Moderate, and High).</li>
<li>If there is a smear stretching along the bottom axis between Low and Moderate, it indicates that while uncertainty fluctuates, the network has confidently ruled out High risk outcomes.</li>
</ul>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre>library(ggtern)

# ----------------------------------------------------------------
# 1. Ingest Your bnlearn Bootstrap Matrix
# ----------------------------------------------------------------
prob_matrix &lt;- t(attr(predicted, &quot;prob&quot;))
colnames(prob_matrix) &lt;- c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;)
df_wide &lt;- as.data.frame(prob_matrix)

# ----------------------------------------------------------------
# 2. Modernized Ternary Contoured Density Heatmap (No Warnings)
# ----------------------------------------------------------------
plot_density &lt;- ggtern(data = df_wide, aes(x = Low, y = High, z = Moderate)) +
  # Computes and fills a 2D density surface inside the triangular space
  stat_density_tern(
    geom  = &quot;polygon&quot;, 
    # FIXED: Replaced deprecated ..level.. with the modern after_stat(level)
    aes(fill = after_stat(level), alpha = after_stat(level)), 
    n     = 200, 
    bdl   = 0.01,
    # FIXED: Mutes the out-of-bounds scale range warning safely
    na.rm = TRUE 
  ) +
  scale_fill_gradient(low = &quot;#ffeda0&quot;, high = &quot;#f03b20&quot;) +
  scale_alpha_continuous(range = c(0.2, 0.85)) + 
  geom_point(alpha = 0.2, size = 0.8, color = &quot;black&quot;) +
  theme_rgbw() +
  labs(
    title    = &quot;Density Contour of Predicted Community Risk Profile&quot;,
    subtitle = &quot;Heatmap showing where bootstrap predictions cluster&quot;,
    x        = &quot;Low Risk&quot;,
    y        = &quot;High Risk&quot;,
    z        = &quot;Moderate Risk&quot;,
    # FIX 1: Customizing the arrow labels around the triangle (Counterclockwise)
    xarrow   = &quot;Decreasing Risk&quot;,
    yarrow   = &quot;Increasing Risk&quot;,
    zarrow   = &quot;Increasing Risk&quot;
  ) +
  theme(
    plot.title      = element_text(face = &quot;bold&quot;, size = 14),
    legend.position = &quot;none&quot;,
    tern.axis.arrow.show = TRUE
    #plot.margin = margin(t = 10, r = 5, b = 10, l = 5, unit = &quot;pt&quot;) 
  )

print(plot_density)</pre>
</details>
<div class="cell-output-display">
<div>
<figure class="figure">
<p><a href="https://i2.wp.com/rworks.dev/posts/data-center-risk-model/index_files/figure-html/unnamed-chunk-20-1.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-5" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/data-center-risk-model/index_files/figure-html/unnamed-chunk-20-1.png?w=450&#038;ssl=1" class="img-fluid figure-img"  data-recalc-dims="1"></a></p>
</figure>
</div>
</div>
</div>
</section>
<section id="stack-ridgeline-probabilities-joyplots" class="level3">
<h3 class="anchored" data-anchor-id="stack-ridgeline-probabilities-joyplots">Stack Ridgeline Probabilities (Joyplots)</h3>
<p>A second appropriate visualization is to produce a ridgeline plot grouped by Scale which is the primary scenario driver. These plots stack smooth, partially transparent density “waves” on top of one another along a unified 0–100% horizontal scale to facilitate comparisons. The densities for “Low”, “Moderate”, and “High” predicted risk states are stacked vertically. The plot reveals subtle variations in skewness across the bootstrap trials.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre># library(ggplot2)
# library(ggridges)
# library(tidyr)

# ----------------------------------------------------------------
# 1. Process Your Exact bnlearn Bootstrap Matrix
# ----------------------------------------------------------------
# Extract the probability matrix and convert to a data frame as specified
prob_matrix &lt;- t(attr(predicted, &quot;prob&quot;))
colnames(prob_matrix) &lt;- c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;)
df_wide &lt;- as.data.frame(prob_matrix)

# Add a distinct identifier for each bootstrap replication row
df_wide$Bootstrap_ID &lt;- seq_len(nrow(df_wide))

# ----------------------------------------------------------------
# 2. Reshape Data for Ridge Profiling
# ----------------------------------------------------------------
# Pivot data into a long format optimized for ggplot2 tracking
df_long &lt;- pivot_longer(
  df_wide,
  cols      = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;),
  names_to  = &quot;Risk_State&quot;,
  values_to = &quot;Predicted_Probability&quot;
)

# Enforce vertical layout hierarchy (High on top, Low on bottom)
df_long$Risk_State &lt;- factor(df_long$Risk_State, levels = c(&quot;High&quot;, &quot;Moderate&quot;, &quot;Low&quot;))

# ----------------------------------------------------------------
# 3. Render Ridgeline Distribution Profiles
# ----------------------------------------------------------------
ggplot(df_long, aes(x = Predicted_Probability, y = Risk_State, fill = Risk_State)) +
  # Adds smooth density waves with the actual bootstrap data points marked underneath as tick marks
  geom_density_ridges(
    alpha = 0.75, scale = 1.4, color = &quot;white&quot;, 
    jittered_points = TRUE, point_shape = &quot;|&quot;, point_size = 2, point_alpha = 0.3
  ) +
  # Consistent color schema matching the network architecture
  scale_fill_manual(values = c(&quot;Low&quot; = &quot;#2b8cbe&quot;, &quot;Moderate&quot; = &quot;#feb24c&quot;, &quot;High&quot; = &quot;#f03b20&quot;)) +
  scale_x_continuous(labels = scales::percent_format(), limits = c(0, 1)) +
  labs(
    title    = &quot;Bootstrapped Target Predictions: Overall Community Risk&quot;,
    subtitle = paste(&quot;Density distribution profiles across&quot;, nrow(df_wide), &quot;bnlearn bootstrap iterations&quot;),
    x        = &quot;Predicted Probability Assigned to State&quot;,
    y        = &quot;Risk Classification Spectrum&quot;
  ) +
  theme_minimal(base_size = 12) +
  theme(
    legend.position  = &quot;none&quot;,
    plot.title       = element_text(face = &quot;bold&quot;),
    panel.grid.minor = element_blank()
  )</pre>
</details>
<div class="cell-output-display">
<div>
<figure class="figure">
<p><a href="https://i1.wp.com/rworks.dev/posts/data-center-risk-model/index_files/figure-html/unnamed-chunk-21-1.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-6" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/data-center-risk-model/index_files/figure-html/unnamed-chunk-21-1.png?w=450&#038;ssl=1" class="img-fluid figure-img"  data-recalc-dims="1"></a></p>
</figure>
</div>
</div>
</div>
</section>
</section>
<section id="contingency-tables" class="level2">
<h2 class="anchored" data-anchor-id="contingency-tables">Contingency Tables</h2>
<p>The details of the fitted distribution for all of the nodes are revealed by examining the contingency tables the individual nodes. These show the probability distributions of risk state for child nodes contingent on their parent nodes, To begin this examination, let’s first look at the tables for Housing Pressure which has only two parents: Workforce Demand and Housing availability. Notice that the columns of each table sum to 1. The first table considers the scenario where Workforce Demand is “Low”. Each column gives a value of the probability of Housing Availability for the “Low”, “Moderate” and “High” Housing Pressure risk states.</p>
<p>The (3,3) entry in each tables shows the probability of Severely Constrained Housing Pressure for a high risk scenario as Work Force Demand moves from “Low” to “Moderate”, and then “High”.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre># Extract the conditional-probability table

fitted_bn$HousingPressure$prob</pre>
</details>
<div class="cell-output cell-output-stdout">
<pre>, , WorkforceDemand = Low

               HousingAvailability
HousingPressure   Adequate      Tight SeverelyConstrained
       Low      0.82534532 0.34708510          0.11661093
       Moderate 0.15809619 0.58509638          0.67045708
       High     0.01655850 0.06781852          0.21293200

, , WorkforceDemand = Moderate

               HousingAvailability
HousingPressure   Adequate      Tight SeverelyConstrained
       Low      0.49983482 0.09918699          0.05124471
       Moderate 0.44720846 0.61065041          0.32834061
       High     0.05295672 0.29016260          0.62041467

, , WorkforceDemand = High

               HousingAvailability
HousingPressure   Adequate      Tight SeverelyConstrained
       Low      0.13366947 0.02910443          0.01941528
       Moderate 0.64492997 0.22603268          0.10475223
       High     0.22140056 0.74486289          0.87583249</pre>
</div>
</div>
<p>The problem with examining contingency tables is that the output generated by a Bayesian Network model for nodes with many parents is voluminous. For each node in the network, a table of contingent probabilities is generated for each set of factors of the parent nodes. For example, Housing Pressure has three levels (low, moderate and high) and two parents: Housing Availability and Workforce Demand. Housing Availability has three levels, adequate, severely constrained, and tight, while Workforce Demand has three levels: lower moderate and high. These lead to the manageable three tables above.</p>
<p>Overall Community Risk, on the other hand, has two 2,187 tables. (See the Appendix for the calculation). One way to deal with this situation is to flatten out the contingency tables into a data frame that contains a column for each parent variable. The following code flattens out the tables for Overall Community Risk, selects the rows “High” rows from the distribution, and sorts them by decreasing probability to produce a table with 6,561 rows. Each row can be considered to be a path through the space of all possible combinations of parent variables that contribute to Overall Community Risk. Each row Here are the top 5 paths.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre># Extract the conditional-probability table
prob_table &lt;- fitted_bn$OverallCommunityRisk$prob

# Convert the multidimensional table to a data frame
high_risk_probabilities &lt;- as.data.frame(prob_table)

#dfh &lt;- high_risk_probabilities
# the entire flat table for Overall Community Risk has 19,683 rows
# Rename the probability column
names(high_risk_probabilities)[ncol(high_risk_probabilities)] &lt;- &quot;Probability&quot;

# Keep only cases where OverallCommunityRisk is High
high_risk_probabilities &lt;- subset(
  high_risk_probabilities,
  OverallCommunityRisk == &quot;High&quot;
)

# high_risk_probabilities
# sort the combinations from the highest to the lowest high-risk probability:

high_risk_probabilities &lt;- high_risk_probabilities[
  order(high_risk_probabilities$Probability,decreasing = TRUE),]

# change names to print table
#original_names &lt;- names(high_risk_probabilities)

#Table with short column names and no row names for printing

HRP &lt;- high_risk_probabilities
names(HRP) &lt;- c(&quot;ocr&quot;, &quot;eco&quot;, &quot;ecr&quot;, &quot;env&quot;, &quot;hpr&quot;, &quot;qol&quot;, &quot;rel&quot;, &quot;spr&quot;, &quot;wsr&quot;, &quot;Prob&quot;)
rownames(HRP) &lt;- NULL
HRP$Prob &lt;- round(HRP$Prob, digits = 4)
head(HRP, 5)</pre>
</details>
<div class="cell-output cell-output-stdout">
<pre>   ocr      eco      ecr  env  hpr      qol      rel  spr  wsr   Prob
1 High Moderate Moderate High High     High     High High High 0.9998
2 High      Low     High High High Moderate Moderate High  Low 0.9997
3 High      Low Moderate High High     High Moderate High  Low 0.9997
4 High      Low Moderate High High     High     High High  Low 0.9997
5 High      Low Moderate High  Low Moderate Moderate  Low High 0.9997</pre>
</div>
<details class="code-fold">
<summary>Show the code</summary>
<pre># Change names back
#names(high_risk_probabilities) &lt;- original_names

#some useful redundant code
# risk_flat &lt;- flatten_bnlearn_cpt(
#   fitted_bn = fitted_bn,
#   child_name = &quot;OverallCommunityRisk&quot;
# )
# 
# head(risk_flat)
# 
# 
# # Descending
# sorted_risk_flat &lt;- risk_flat %&gt;%
#   arrange(desc(&quot;Probability&quot;))
# 
# head(sorted_risk_flat)</pre>
</details>
</div>
<p>Notice that the combinations comprising the first five paths all lead to similar risk score. In fact, the first 332 rows of the table produce a risk score above 0.99. This situation unfortunately eliminates the naive hope of easily identifying a few paths that could materially change Overall Community Risk. Here are the values for row 333.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre>HRP[333,]</pre>
</details>
<div class="cell-output cell-output-stdout">
<pre>     ocr eco  ecr      env      hpr      qol      rel      spr      wsr   Prob
333 High Low High Moderate Moderate Moderate Moderate Moderate Moderate 0.7999</pre>
</div>
</div>
<section id="radar-plot" class="level3">
<h3 class="anchored" data-anchor-id="radar-plot">Radar Plot</h3>
<p>Another way to gain some intuition about how these paths vary is through radar plots. The following plot picks out three random rows from the Overall Community Risk flat table ans plots them on a circular path leading to Overall Community Risk.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the radar plot code</summary>
<pre>### First or random
plot_radar &lt;- function(data, n_highlight = 5, selection_method = c(&quot;first&quot;, &quot;random&quot;), level_order = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;), tleg = 45) {
  selection_method &lt;- match.arg(selection_method)
  
  categorical_vars &lt;- c(
    &quot;OverallCommunityRisk&quot;, &quot;EconomicBenefit&quot;, &quot;ElectricityCostRisk&quot;,
    &quot;EnvironmentalDegradation&quot;, &quot;HousingPressure&quot;, &quot;QualityOfLifeRisk&quot;,
    &quot;ReliabilityRisk&quot;, &quot;ServicePressure&quot;, &quot;WaterSecurityRisk&quot;
  )
  
  required_vars &lt;- c(categorical_vars, &quot;Probability&quot;)
  missing_vars &lt;- setdiff(required_vars, names(data))
  if (length(missing_vars) &gt; 0) {
    stop(
      &quot;Missing columns: &quot;, paste(missing_vars, collapse = &quot;, &quot;)
    )
  }
  
  data$Probability &lt;- as.numeric(data$Probability)
  
  # Capture original dataframe row numbers before filtering and sorting
  plot_data &lt;- data %&gt;% 
    dplyr::mutate(orig_row = dplyr::row_number()) %&gt;%
    dplyr::select(dplyr::all_of(required_vars), orig_row) %&gt;% 
    dplyr::filter(dplyr::if_all(dplyr::all_of(required_vars), ~ !is.na(.x)))
  
  if (nrow(plot_data) == 0) {
    stop(&quot;There are no complete rows available for plotting.&quot;)
  }
  
  # Sort to isolate the highest-probability rows
  top_pool &lt;- plot_data %&gt;% 
    dplyr::arrange(dplyr::desc(Probability))
  
  n_top &lt;- min(n_highlight, nrow(top_pool))
  
  # Select paths based on user choice
  if (selection_method == &quot;first&quot;) {
    plot_data &lt;- top_pool %&gt;% dplyr::slice_head(n = n_top)
  } else {
    plot_data &lt;- top_pool %&gt;% dplyr::slice_sample(n = n_top)
  }
  
  plot_data &lt;- plot_data %&gt;% 
    dplyr::mutate(
      path = dplyr::row_number(),
      path_label = paste0(&quot;Top path &quot;, path)
    )
  
  # Convert Low, Moderate, High to 1, 2, 3
  for (v in categorical_vars) {
    unexpected &lt;- setdiff(
      unique(plot_data[[v]]), level_order
    )
    if (length(unexpected) &gt; 0) {
      stop(
        &quot;Unexpected level(s) in &quot;, v, &quot;: &quot;, paste(unexpected, collapse = &quot;, &quot;)
      )
    }
    plot_data[[v]] &lt;- as.numeric(
      factor(
        plot_data[[v]], levels = level_order, ordered = TRUE
      )
    )
  }
  
  n_axes &lt;- length(categorical_vars)
  angles &lt;- seq(
    from = pi / 2, to = pi / 2 - 2 * pi, length.out = n_axes + 1
  )[-(n_axes + 1)]
  
  axis_positions &lt;- data.frame(
    variable = categorical_vars,
    angle = angles,
    stringsAsFactors = FALSE
  )
  
  # Convert radar values to Cartesian coordinates
  radar_data &lt;- plot_data %&gt;% 
    tidyr::pivot_longer(
      cols = dplyr::all_of(categorical_vars),
      names_to = &quot;variable&quot;,
      values_to = &quot;value&quot;
    ) %&gt;% 
    dplyr::left_join(axis_positions, by = &quot;variable&quot;) %&gt;% 
    dplyr::mutate(
      x = value * cos(angle),
      y = value * sin(angle),
      path_label = factor(path_label, levels = paste0(&quot;Top path &quot;, seq_len(n_top)))
    )
  
  # Close each profile polygon by repeating its first point
  radar_data_closed &lt;- radar_data %&gt;% 
    dplyr::group_by(path, path_label) %&gt;% 
    dplyr::arrange(angle, .by_group = TRUE) %&gt;% 
    dplyr::slice(c(dplyr::row_number(), 1)) %&gt;% 
    dplyr::ungroup()
  
  # Polygonal grid and outer boundary
  grid_data &lt;- tidyr::crossing(
    radius = 1:3,
    axis_positions
  ) %&gt;% 
    dplyr::mutate(
      x = radius * cos(angle),
      y = radius * sin(angle)
    ) %&gt;% 
    dplyr::group_by(radius) %&gt;% 
    dplyr::arrange(angle, .by_group = TRUE) %&gt;% 
    dplyr::slice(c(dplyr::row_number(), 1)) %&gt;% 
    dplyr::ungroup()
  
  # Axis spokes
  spoke_data &lt;- axis_positions %&gt;% 
    dplyr::mutate(
      x = 3 * cos(angle),
      y = 3 * sin(angle)
    )
  
  highlight_colours &lt;- c(
    &quot;#D73027&quot;, &quot;#4575B4&quot;, &quot;#1A9850&quot;, &quot;#984EA3&quot;, &quot;#E66101&quot;
  )[seq_len(n_top)]
  
  colour_values &lt;- stats::setNames(highlight_colours, paste0(&quot;Top path &quot;, seq_len(n_top)))
  
  # Legend labels matching selection strategy with 5 significant digits &#038; row numbers
  legend_labels &lt;- plot_data %&gt;% 
    dplyr::arrange(path) %&gt;% 
    dplyr::mutate(
      label = paste0(
        &quot;[Row &quot;, orig_row, &quot;] (risk = &quot;, format(signif(Probability, 5), scientific = FALSE), &quot;)&quot;
      )
    ) %&gt;% 
    dplyr::pull(label)
  
  names(legend_labels) &lt;- paste0(&quot;Top path &quot;, seq_len(n_top))
  
  # Axis-label positions
  label_data &lt;- axis_positions %&gt;% 
    dplyr::mutate(
      x = 3.25 * cos(angle),
      y = 3.25 * sin(angle),
      hjust = dplyr::case_when(
        cos(angle) &gt; 0.1 ~ 0,
        cos(angle) &lt; -0.1 ~ 1,
        TRUE ~ 0.5
      ),
      vjust = dplyr::case_when(
        sin(angle) &gt; 0.1 ~ 0,
        sin(angle) &lt; -0.1 ~ 1,
        TRUE ~ 0.5
      )
    )
  
  ggplot2::ggplot() +
    ggplot2::geom_path(
      data = grid_data,
      ggplot2::aes(x = x, y = y, group = radius),
      colour = &quot;grey75&quot;, linewidth = 0.5
    ) +
    ggplot2::geom_segment(
      data = spoke_data,
      ggplot2::aes(x = 0, y = 0, xend = x, yend = y),
      colour = &quot;grey80&quot;, linewidth = 0.5
    ) +
    ggplot2::geom_polygon(
      data = radar_data_closed,
      ggplot2::aes(x = x, y = y, group = path, colour = path_label, fill = path_label),
      alpha = 0.08, linewidth = 0.5
    ) +
    ggplot2::geom_path(
      data = radar_data_closed,
      ggplot2::aes(x = x, y = y, group = path, colour = path_label),
      linewidth = 0.9, alpha = 0.85
    ) +
    ggplot2::geom_point(
      data = radar_data,
      ggplot2::aes(x = x, y = y, colour = path_label),
      size = 1.5, alpha = 0.9
    ) +
    ggplot2::geom_text(
      data = label_data,
      ggplot2::aes(x = x, y = y, label = variable, hjust = hjust, vjust = vjust),
      size = 3.5, fontface = &quot;bold&quot;
    ) +
    ggplot2::annotate(
      &quot;text&quot;, x = 0, y = 1, label = level_order[1], colour = &quot;grey45&quot;, size = 3, vjust = 1.5
    ) +
    ggplot2::annotate(
      &quot;text&quot;, x = 0, y = 2, label = level_order[2], colour = &quot;grey45&quot;, size = 3, vjust = 1.5
    ) +
    ggplot2::annotate(
      &quot;text&quot;, x = 0, y = 3, label = level_order[3], colour = &quot;grey45&quot;, size = 3, vjust = 1.5
    ) +
    ggplot2::scale_colour_manual(
      values = colour_values,
      breaks = names(legend_labels),
      labels = unname(legend_labels),
      name = NULL
    ) +
    ggplot2::scale_fill_manual(
      values = colour_values, guide = &quot;none&quot;
    ) +
    ggplot2::coord_equal(
      xlim = c(-4.5, 4.5), ylim = c(-3.5, 3.8), clip = &quot;off&quot;
    ) +
    ggplot2::labs(
      title = paste(
        &quot;Overall Community Risk radar profiles:&quot;, n_top, &quot;paths selected via&quot;, selection_method
      ),
      x = NULL, y = NULL
    ) +
    ggplot2::theme(
      legend.margin = ggplot2::margin(t = tleg, r = 0, b = 0, l = 0)
    )
}</pre>
</details>
</div>
<p>This code runs the plot code. The default settings produce the top three rows of the sorted flat file for Overall Community Risk. However, setting method = “random” will select three random paths (rows)</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code to run the plot</summary>
<pre>#df &lt;- sorted_risk_flat
set.seed(456)
df &lt;- high_risk_probabilities

plot_radar(
  data=df, n_highlight = 3, selection_method = &quot;random&quot;, level_order = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;), tleg = 195
)</pre>
</details>
<div class="cell-output-display">
<div>
<figure class="figure">
<p><a href="https://i0.wp.com/rworks.dev/posts/data-center-risk-model/index_files/figure-html/unnamed-chunk-26-1.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-7" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/data-center-risk-model/index_files/figure-html/unnamed-chunk-26-1.png?w=450&#038;ssl=1" class="img-fluid figure-img"  data-recalc-dims="1"></a></p>
</figure>
</div>
</div>
</div>
<p>Note that these paths are samples from the distribution of risk states of Overall Community Risk. The value for a “High” state sample need not be particularly high.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre>summary(high_risk_probabilities$Probability)</pre>
</details>
<div class="cell-output cell-output-stdout">
<pre>     Min.   1st Qu.    Median      Mean   3rd Qu.      Max. 
5.644e-05 5.073e-04 3.333e-01 2.859e-01 3.333e-01 9.998e-01 </pre>
</div>
</div>
</section>
</section>
<section id="high-risk-scenario" class="level2">
<h2 class="anchored" data-anchor-id="high-risk-scenario">High Risk Scenario</h2>
<p>In this section we alter the meta-parameters that determine the overall risk intensity in the synthetic data to produce a higher risk version of the synthetic data. The code below generates the new data and produces a summary of the risk specific nodes for the new data set. The table indicates a marked increase for the proportions of high risk among all of the risk specific nodes.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre># ================================================================
# Usage
# ================================================================
#
# A DAG named `dag` must exist first.

# Global scenario controls.
  # global_risk_shift
  # 0.00 = baseline moderate-risk scenario
  # Positive values increase risk throughout the network
  # Negative values decrease risk throughout the network
  
  #  parent_effect_strength
  # Controls how strongly parent risk affects child risk.
  # 1.00 is the default.
 
  # conditional_noise 
  # Controls random variation in conditional probabilities.
  # Larger values produce less deterministic relationships.

#
synthetic_2 &lt;- generate_synthetic_data(
  n = 5000,
  global_risk_shift = 0.20,
  parent_effect_strength = 1.00,
  conditional_noise = 0.10
)
#
# str(synthetic)
# head(synthetic)
# ================================================================

risk_nodes &lt;- c(
  &quot;GridStress&quot;,
  &quot;HousingPressure&quot;,
  &quot;ServicePressure&quot;,
  &quot;TrafficExposure&quot;,
  &quot;VisualLandUseExposure&quot;,
  &quot;WaterCompetition&quot;,
  &quot;ElectricityCostRisk&quot;,
  &quot;QualityOfLifeRisk&quot;,
  &quot;ReliabilityRisk&quot;,
  &quot;WaterSecurityRisk&quot;,
  &quot;OverallCommunityRisk&quot;
)

risk_summary &lt;- do.call(
  rbind,
  lapply(risk_nodes, function(node) {
    p &lt;- prop.table(table(synthetic_2[[node]]))

    data.frame(
      Node = node,
      Low = unname(p[&quot;Low&quot;]),
      Moderate = unname(p[&quot;Moderate&quot;]),
      High = unname(p[&quot;High&quot;])
    )
  })
)

risk_summary</pre>
</details>
<div class="cell-output cell-output-stdout">
<pre>                    Node    Low Moderate   High
1             GridStress 0.0698   0.3416 0.5886
2        HousingPressure 0.0606   0.2964 0.6430
3        ServicePressure 0.0596   0.3144 0.6260
4        TrafficExposure 0.0608   0.3348 0.6044
5  VisualLandUseExposure 0.0610   0.3366 0.6024
6       WaterCompetition 0.0598   0.3144 0.6258
7    ElectricityCostRisk 0.0732   0.3844 0.5424
8      QualityOfLifeRisk 0.0262   0.2422 0.7316
9        ReliabilityRisk 0.0556   0.2348 0.7096
10     WaterSecurityRisk 0.0748   0.3656 0.5596
11  OverallCommunityRisk 0.0194   0.1638 0.8168</pre>
</div>
</div>
<p>The next section of code fits the model to the new data displays the predicted values. As above, we use the <code>bnlearn</code> predict function to bootstrap predictions of risk for each of the three risk scenarios: “Low”, “Moderate”, and “High.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre># ------------------------------------------------------------
# 5. Fit the Bayesian Network
# ------------------------------------------------------------

fitted_bn_2 &lt;- bn.fit(
  dag,
  data = synthetic_2,
  method = &quot;bayes&quot;,
  iss = 10
)

## Inspect Overall Community Risk, or any other node directly from the object

predicted_2 &lt;- predict(
  fitted_bn_2,
  node = &quot;OverallCommunityRisk&quot;,
  data = synthetic_2,
  prob = TRUE
)

# 1. Extract and transpose the probability matrix
probs &lt;- t(attr(predicted_2, &quot;prob&quot;))
# 2. Assign column names for clarity
colnames(probs) &lt;- c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;)
# 3. Generate summary statistics (Min, 1st Qu., Median, Mean, 3rd Qu., Max)
#summary(probs)
rounded_summary &lt;- sapply(as.data.frame(probs), summary)
rounded_summary &lt;- round(rounded_summary, digits = 4)
rounded_summary</pre>
</details>
<div class="cell-output cell-output-stdout">
<pre>           Low Moderate   High
Min.    0.0000   0.0000 0.0003
1st Qu. 0.0000   0.0005 0.7777
Median  0.0002   0.1183 0.8767
Mean    0.0195   0.1638 0.8167
3rd Qu. 0.0068   0.2076 0.9434
Max.    0.9990   0.9995 0.9999</pre>
</div>
</div>
<p>Here we generate the density contour plot for the new risk scenario.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre>#library(ggtern)

# ----------------------------------------------------------------
# 1. Ingest Your bnlearn Bootstrap Matrix
# ----------------------------------------------------------------
prob_matrix &lt;- t(attr(predicted_2, &quot;prob&quot;))
colnames(prob_matrix) &lt;- c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;)
df_wide &lt;- as.data.frame(prob_matrix)

# ----------------------------------------------------------------
# 2. Modernized Ternary Contoured Density Heatmap (No Warnings)
# ----------------------------------------------------------------
plot_density &lt;- ggtern(data = df_wide, aes(x = Low, y = High, z = Moderate)) +
  # Computes and fills a 2D density surface inside the triangular space
  stat_density_tern(
    geom  = &quot;polygon&quot;,
    # FIXED: Replaced deprecated ..level.. with the modern after_stat(level)
    aes(fill = after_stat(level), alpha = after_stat(level)),
    n     = 200,
    bdl   = 0.01,
    # FIXED: Mutes the out-of-bounds scale range warning safely
    na.rm = TRUE
  ) +
  scale_fill_gradient(low = &quot;#ffeda0&quot;, high = &quot;#f03b20&quot;) +
  scale_alpha_continuous(range = c(0.4, 0.99)) +
  geom_point(alpha = 0.2, size = 0.8, color = &quot;black&quot;) +
  theme_rgbw() +
  labs(
    title    = &quot;Density Contour of Predicted Community Risk Profile&quot;,
    subtitle = &quot;Heatmap showing where bootstrap predictions cluster&quot;,
    x        = &quot;Low Risk&quot;,
    y        = &quot;High Risk&quot;,
    z        = &quot;Mod Risk&quot;,
    xarrow   = &quot;Decreasing Risk&quot;,
    yarrow   = &quot;Increasing Risk&quot;,
    zarrow   = &quot;Increasing Risk&quot;
  ) +
  theme(
    plot.title      = element_text(face = &quot;bold&quot;, size = 14),
    legend.position = &quot;none&quot;,
    tern.axis.arrow.show = TRUE
  )

plot_density</pre>
</details>
<div class="cell-output-display">
<div>
<figure class="figure">
<p><a href="https://i0.wp.com/rworks.dev/posts/data-center-risk-model/index_files/figure-html/unnamed-chunk-30-1.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-8" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/data-center-risk-model/index_files/figure-html/unnamed-chunk-30-1.png?w=450&#038;ssl=1" class="img-fluid figure-img"  data-recalc-dims="1"></a></p>
</figure>
</div>
</div>
<details class="code-fold">
<summary>Show the code</summary>
<pre># plot_density &lt;- ggtern(
#   data = df_wide,
#   aes(x = Low, y = High, z = Moderate)
# ) +
#   stat_density_tern(
#     geom = &quot;polygon&quot;,
#     aes(
#       fill = after_stat(level),
#       group = after_stat(level)
#     ),
#     n = 200,
#     bdl = 0.01,
#     na.rm = TRUE
#   ) +
#   scale_fill_gradient(
#     low = &quot;#ffeda0&quot;,
#     high = &quot;#f03b20&quot;
#   ) +
#   geom_point(
#     alpha = 0.2,
#     size = 0.8,
#     color = &quot;black&quot;
#   ) +
#   theme_rgbw() +
#   theme(
#     plot.title = element_text(face = &quot;bold&quot;, size = 14),
#     legend.position = &quot;none&quot;,
#     tern.axis.arrow.show = TRUE
#   )
# 
# plot_density</pre>
</details>
</div>
</section>
<section id="summary" class="level2">
<h2 class="anchored" data-anchor-id="summary">Summary</h2>
<p>This post presents a model that is intended to serve as a template for building Bayesian Networks to explore the risk posed to a community from building a data center. It implements <code>R</code> code that includes a robust DAG, a synthetic data generator that can be customized to reflect the initial probability distributions and risk intensity settings. In a modeling effort for a particular community, these could be elicited from community experts, tables and visualizations to evaluate multiple risk states.</p>
<p>We human beings are notoriously bad at intuitively arriving at the correct logical conclusions to even the simplest conditional probability arguments. It is envisioned that main use of tool implemented in this post would be to help people organize their beliefs about the risks of building a data center in their community in a way that would enable them to test their assumptions and explore the probable consequences.</p>
</section>
<section id="next-steps" class="level2">
<h2 class="anchored" data-anchor-id="next-steps">Next Steps</h2>
<p>In a follow up post, I hope to be able to provide more examples of additional inferences than can be made with this model.</p>
</section>
<section id="appendix-a-counting-tables" class="level2">
<h2 class="anchored" data-anchor-id="appendix-a-counting-tables">Appendix A: Counting Tables</h2>
<p>The following code counts the total number of tables generated by the model.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre>count_displayed_cpt_tables &lt;- function(fitted_bn) {
  
  result &lt;- lapply(
    names(fitted_bn),
    function(node_name) {
      
      prob &lt;- fitted_bn[[node_name]]$prob
      
      if (is.null(dim(prob))) {
        
        # Root node: a probability vector, not a 2D table
        data.frame(
          Node = node_name,
          CPTDimensions = length(prob),
          NumberOfTables = 1L,
          stringsAsFactors = FALSE
        )
        
      } else {
        
        dimensions &lt;- dim(prob)
        
        # For a 3D CPT, the printed tables are slices of the third dimension.
        # For higher-dimensional CPTs, count all slices after dimensions 1 and 2.
        number_of_tables &lt;- if (length(dimensions) &lt;= 2L) {
          1L
        } else {
          prod(dimensions[-c(1, 2)])
        }
        
        data.frame(
          Node = node_name,
          CPTDimensions = paste(dimensions, collapse = &quot; x &quot;),
          NumberOfTables = number_of_tables,
          stringsAsFactors = FALSE
        )
      }
    }
  )
  
  result &lt;- do.call(rbind, result)
  rownames(result) &lt;- NULL
  
  result
}

cpt_table_counts &lt;- count_displayed_cpt_tables(fitted_bn)

cpt_table_counts</pre>
</details>
<div class="cell-output cell-output-stdout">
<pre>                       Node                     CPTDimensions NumberOfTables
1          AccessToBenefits                         3 x 6 x 4              4
2        AirQualityExposure                     3 x 3 x 6 x 4             24
3          BackupGeneration                             3 x 4              1
4     ConstructionIntensity                             3 x 4              1
5                   Cooling                                 4              1
6                  Distance                                 6              1
7           EconomicBenefit                         3 x 3 x 4              4
8       ElectricityCostRisk                         3 x 6 x 3              3
9         ElectricityDemand                             4 x 4              1
10 EnvironmentalDegradation                     3 x 3 x 3 x 3              9
11             GridCapacity                                 3              1
12               GridStress                         3 x 4 x 3              3
13      HousingAvailability                                 3              1
14          HousingPressure                         3 x 3 x 3              3
15            LandFootprint                             3 x 4              1
16            NoiseExposure                     3 x 4 x 6 x 4             24
17     OverallCommunityRisk 3 x 3 x 3 x 3 x 3 x 3 x 3 x 3 x 3           2187
18        QualityOfLifeRisk         3 x 3 x 3 x 3 x 3 x 3 x 3            243
19          ReliabilityRisk                             3 x 3              1
20                    Scale                                 4              1
21          ServicePressure                         3 x 3 x 3              3
22          TrafficExposure                         3 x 3 x 6              6
23    VisualLandUseExposure                         3 x 6 x 3              3
24            Vulnerability                                 3              1
25        WaterAvailability                                 3              1
26         WaterCompetition                         3 x 3 x 3              3
27              WaterDemand                         3 x 4 x 4              4
28        WaterSecurityRisk                         3 x 3 x 3              3
29          WorkforceDemand                             3 x 4              1</pre>
</div>
<details class="code-fold">
<summary>Show the code</summary>
<pre>tot_tables &lt;- sum(cpt_table_counts$NumberOfTables)</pre>
</details>
</div>
<p>Altogether, the this model produces a total of 2539 tables.</p>
</section>
<section id="appendix-b-the-laplacian-radial-kernel" class="level2">
<h2 class="anchored" data-anchor-id="appendix-b-the-laplacian-radial-kernel">Appendix B: The Laplacian Radial Kernel</h2>
<p>This code plots the Laplacian radial kernel used to set risk for derived, non-root nodes in the algorithm to generate synthetic data.</p>
<p><img src="https://latex.codecogs.com/png.latex?P_%7B%5Ctext%7Braw%7D%7D(%5Ctext%7Bstate%7D)%20=%20e%5E%7B-c%20%5Ccdot%20%5Cvert%7B%7D%5Ctext%7BState%20Score%7D%20-%20%5Ctext%7BTarget%20Risk%7D%5Cvert%7B%7D%7D"> across our three standard derived states (Low = 0.15, Moderate = 0.50, High = 0.85). It dynamically plots how the final normalized probabilities shift as the calculated Target Risk moves from 0.00 to 1.00.</p>
<p>Child-state probabilities were generated using normalized exponential distance weights. For each state, the weight was defined as above where <img src="https://latex.codecogs.com/png.latex?%5Cvert%7B%7D%5Ctext%7BState%20Score%7D%20=%20r_s"> is the predefined risk score for state s. <img src="https://latex.codecogs.com/png.latex?%5Cvert%7B%7D%5Ctext%7BTargetRisk%7D"> is the parent-dependent target risk, and c controls concentration around the target. This is equivalent to applying a one-dimensional exponential, or Laplacian, radial kernel centered at <img src="https://latex.codecogs.com/png.latex?%5Cvert%7B%7D%5Ctext%7BTargetRisk%7D">, followed by normalization across child states.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre># ----------------------------------------------------------------
# 1. Define Core Simulation Parameters
# ----------------------------------------------------------------
concentration &lt;- 6
state_scores  &lt;- c(Low = 0.15, Moderate = 0.50, High = 0.85)
target_range  &lt;- seq(0.00, 1.00, by = 0.01)

# ----------------------------------------------------------------
# 2. Compute Normalized Probabilities over Target Risk Continuum
# ----------------------------------------------------------------
plot_data &lt;- data.frame()

for (target_risk in target_range) {
  # Step 3: Measure raw exponential distances
  raw_probs &lt;- exp(-concentration * abs(state_scores - target_risk))
  
  # Step 4: Normalize scores to sum to 1.00
  final_probs &lt;- raw_probs / sum(raw_probs)
  
  # Bind results for plotting
  row_df &lt;- data.frame(
    TargetRisk = target_risk,
    Low        = final_probs[&quot;Low&quot;],
    Moderate   = final_probs[&quot;Moderate&quot;],
    High       = final_probs[&quot;High&quot;]
  )
  plot_data &lt;- rbind(plot_data, row_df)
}

# Pivot data into a long format optimized for ggplot2 tracking
plot_data_long &lt;- pivot_longer(
  plot_data, 
  cols      = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;), 
  names_to  = &quot;State&quot;, 
  values_to = &quot;Probability&quot;
)

# Enforce clean factor ordering matching the network architecture
plot_data_long$State &lt;- factor(plot_data_long$State, levels = c(&quot;Low&quot;, &quot;Moderate&quot;, &quot;High&quot;))

# ----------------------------------------------------------------
# 3. Generate the RMarkdown Visualization
# ----------------------------------------------------------------
ggplot(plot_data_long, aes(x = TargetRisk, y = Probability, color = State)) +
  geom_line(size = 1.2) +
  # Visual reference lines matching the static state risk mappings
  geom_vline(xintercept = c(0.15, 0.50, 0.85), linetype = &quot;dashed&quot;, color = &quot;gray60&quot;, alpha = 0.7) +
  scale_color_manual(values = c(&quot;Low&quot; = &quot;#2b8cbe&quot;, &quot;Moderate&quot; = &quot;#feb24c&quot;, &quot;High&quot; = &quot;#f03b20&quot;)) +
  labs(
    title    = &quot;Dynamic Probability Shifts for Derived Non-Root Nodes&quot;,
    subtitle = paste0(&quot;Exponential RBF Kernel Behavior (Concentration = &quot;, concentration, &quot;)&quot;),
    x        = &quot;Calculated Target Risk Score (From Parents + Noise)&quot;,
    y        = &quot;Final State Generation Probability&quot;,
    color    = &quot;Assigned State&quot;
  ) +
  scale_x_continuous(breaks = seq(0, 1, 0.1)) +
  scale_y_continuous(labels = scales::percent_format(accuracy = 1)) </pre>
</details>
<div class="cell-output-display">
<div>
<figure class="figure">
<p><a href="https://i0.wp.com/rworks.dev/posts/data-center-risk-model/index_files/figure-html/pressure-1.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-9" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/data-center-risk-model/index_files/figure-html/pressure-1.png?w=450&#038;ssl=1" class="img-fluid figure-img"  data-recalc-dims="1"></a></p>
</figure>
</div>
</div>
<details class="code-fold">
<summary>Show the code</summary>
<pre>  #theme_minimal(base_size = 12) +
  # theme(
  #   plot.title         = element_text(face = &quot;bold&quot;, linewidth = 1),
  #   panel.grid.minor   = element_blank(),
  #   legend.position    = &quot;bottom&quot;
  #)</pre>
</details>
</div>
</section>
<section id="appendix-c-some-documentation-on-data-center-risks" class="level2">
<h2 class="anchored" data-anchor-id="appendix-c-some-documentation-on-data-center-risks">Appendix C: Some Documentation on Data Center Risks</h2>
<p>This section provides some references, mostly investigative reports, that support the general risk categories used in the model.</p>
<section id="electricity-grid-reliability-and-affordability" class="level4">
<h4 class="anchored" data-anchor-id="electricity-grid-reliability-and-affordability">Electricity, Grid Reliability, and Affordability</h4>
<ul>
<li><p><a href="https://earthjustice.org/" rel="nofollow" target="_blank">Earthjustice</a> reports that the traditional utility model socializes grid infrastructure costs across the entire customer base. When utilities race to build new substations, gas power plants, and transmission lines for tech facilities, standard ratepayers often absorb those costs.</p></li>
<li><p><a href="https://cepr.org/voxeu/columns/how-data-centres-affect-electricity-prices-and-whom" rel="nofollow" target="_blank">CEPR economic analysis</a> found that adding a single average hyperscale facility increases local residential power prices by roughly 4%, while adding ten facilities spikes residential rates by 13%.</p></li>
</ul>
<p><a href="https://www.brookings.edu/articles/confronting-and-addressing-rising-energy-bills-linked-to-data-centers/" rel="nofollow" target="_blank">Brookings Institution research</a> warns that rapid large-load data center growth is decoupling electricity bills from baseline inflation, compounding utility disconnection rates in high-density clusters like Northern Virginia. Nuance/Counter-Perspective: A working paper</p>
</section>
<section id="water-resource-demands" class="level4">
<h4 class="anchored" data-anchor-id="water-resource-demands">Water Resource Demands</h4>
<ul>
<li><p><a href="https://cnr.ncsu.edu/news/2026/09/data-center-boom-water-resources/" rel="nofollow" target="_blank">A hydrologist report</a> from North Carolina State University sates that data centers rely heavily on “open-loop evaporative cooling” systems that consume water and release it as vapor instead of returning it to the local system. Emphasizing that because data center demand “never sleeps,” it permanently removes millions of gallons daily and alters the baseline natural water balance of watersheds.</p></li>
<li><p><a href="https://www.ceres.org/resources/reports/drained-by-data-the-cumulative-impact-of-data-centers-on-regional-water-stress" rel="nofollow" target="_blank">Ceres Sustainability Report</a> predicts that “in the coming years, annual water use associated with data center electricity consumption is expected to increase by 400%, from an estimated 2.9 billion gallons to over 14.5 billion gallons – enough to supply the entire city of Scottsdale, Ariz. for over 2 years and that increases regional water stress by up to 17% annually in already strained basins.</p></li>
<li><p>The Next 10 report: <a href="https://www.next10.org/sites/default/files/2026-05/Data-Centers-Water-Use-Report.pdf" rel="nofollow" target="_blank">The Intersection of Data Center Development, Water Availability, and Environmental Justice in California</a> discusses the potential impact of data centers to California communities that are suffering water shortages and concludes among its top 1 findings that: “Water scarcity impacts extend beyond the footprint of any single data center, particularly when facilities rely on imported water or shared groundwater basins”</p></li>
</ul>
</section>
<section id="land-and-housing" class="level4">
<h4 class="anchored" data-anchor-id="land-and-housing">Land and Housing</h4>
<ul>
<li><p>According to the report <a href="https://www.wri.org/insights/us-data-center-growth-impacts" rel="nofollow" target="_blank">From Energy Use to Air Quality, the Many Ways Data Centers Affect US Communities</a> a national evaluation by the World Resources Institute (WRI), the average footprint of a modern data center footprint spans approximately 224 acres (roughly 450 football fields). Data centers often encroach into rural-zoned lands or agricultural areas.</p></li>
<li><p>The Brookings Institute report <a href="https://www.brookings.edu/articles/new-evidence-on-data-center-employment-effects/" rel="nofollow" target="_blank">New evidence on data center employment effects</a> claims that wages are unaffected by data center builds while home prices rise 2%-5%.</p></li>
</ul>
</section>
<section id="noise-and-air-quality-and-environmental-polution" class="level4">
<h4 class="anchored" data-anchor-id="noise-and-air-quality-and-environmental-polution">Noise and Air Quality and Environmental Polution</h4>
<ul>
<li><p><a href="https://www.theguardian.com/us-news/2026/sep/27/datacenter-developers-us-pollution-rules" rel="nofollow" target="_blank">An investigative report</a> by The Guardian exposed instances where tech developers skirted stringent clean air regulations by splitting massive facility configurations into multiple “minor” emission sources on paper to fast-track permitting.</p></li>
<li><p>The IEEE Report <a href="https://spectrum.ieee.org/data-centers-pollution" rel="nofollow" target="_blank">We Need to Talk About AI’s Impact on Public Health Data-center pollution is linked to asthma, heart attacks, and more</a> outlines a number of way that data centers adversely affect public health. For example, data center backup generators run on diesel fuel and emit 200 to 600 times more nitrogen oxides than an equivalent natural gas power plant.</p></li>
<li><p>The December 2024 report issued by the Virginia Joint Legislative Audit and Review Commission (JLARC), <a href="https://jlarc.virginia.gov/pdfs/reports/Rpt598.pdf" rel="nofollow" target="_blank">Data Centers in Virginia (Report 598)</a> found that:</p>
<ul>
<li>about one-third of operational data center properties in Virginia sit within 200 feet of residential zoned properties.</li>
<li>data centers emit a continuous, low-frequency hum generated by around-the-clock cooling infrastructure, HVAC systems, and mechanical fans. Because low-frequency sound waves travel further and penetrate residential structures more efficiently than higher-frequency sounds, they frequently trigger complaints regarding well-being and disrupted sleep patterns from nearby families</li>
</ul></li>
</ul>
</section>
<section id="governance-and-fairness" class="level4">
<h4 class="anchored" data-anchor-id="governance-and-fairness">Governance and Fairness</h4>
<p>The World Resources Institute Report cited above Using the Centers for Disease Control’s (CDC) Environmental Justice Index, the World Resources Institute proved that a disproportionate percentage of data center developments cluster in census tracts that suffer from above-median pre-existing environmental vulnerabilities (such as high baseline air pollution) and social vulnerabilities (such as lower average education and higher poverty rates).</p>


</section>
</section>

 
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://rworks.dev/posts/data-center-risk-model/"> R Works</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/a-bayesian-network-to-explore-data-center-risks/">A Bayesian Network to Explore Data Center Risks</a>]]></content:encoded>
					
		
		<enclosure url="https://rworks.dev/posts/data-center-risk-model/dag.png" length="0" type="image/png" />

		<post-id xmlns="com-wordpress:feed-additions:1">404210</post-id>	</item>
		<item>
		<title>Creating a permutation test, two ways</title>
		<link>https://www.r-bloggers.com/2026/10/creating-a-permutation-test-two-ways/</link>
		
		<dc:creator><![CDATA[Gavin Simpson]]></dc:creator>
		<pubDate>Mon, 12 Oct 2026 22:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://fromthebottomoftheheap.net/2026/10/13/permute-permutations-and-permutator/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>In my last post on permute, I looked at ordered set partitions and the new formula interface for plots and blocks. This time I want to show two helpers for the next part of writing a permutation test: taking those permutations and doing something...</p></div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/creating-a-permutation-test-two-ways/">Creating a permutation test, two ways</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://fromthebottomoftheheap.net/2026/10/13/permute-permutations-and-permutator/"> From the bottom of the heap — R posts</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>






<div class="no-row-height column-margin column-container"></div><p>In my last post on <strong>permute</strong>, I looked at ordered set partitions and the new formula interface for plots and blocks. This time I want to show two helpers for the next part of writing a permutation test: taking those permutations and doing something with them.</p>
<p><code>permutations()</code> gives you the shuffled values directly. <code>permutator()</code> gives you a function that hands out the next set of observation indices each time you call it. Both use the same permutation machinery underneath, so we can use them to build the same test in two slightly different ways.</p>
<p>These functions are in the <a href="https://github.com/gavinsimpson/permute" rel="nofollow" target="_blank">development version of permute</a>. To follow along, install it from my R-universe:</p>
<div class="cell">
<pre>install.packages(&quot;permute&quot;, repos = c(
  &quot;https://gavinsimpson.r-universe.dev&quot;,
  &quot;https://cloud.r-project.org&quot;
))</pre>
</div>
<section id="something-different-from-jackals" class="level2">
<h2 data-anchor-id="something-different-from-jackals">Something different from jackals</h2>
<p>Let’s use <a href="https://stat.ethz.ch/R-manual/R-devel/library/datasets/html/PlantGrowth.html" rel="nofollow" target="_blank"><code>PlantGrowth</code></a>, which comes with R. It records dried plant weights under a control and two treatment conditions:</p>
<div class="cell">
<pre>library(&quot;permute&quot;)
data(PlantGrowth, package = &quot;datasets&quot;)

aggregate(weight ~ group, data = PlantGrowth, FUN = mean)</pre>
<div class="cell-output cell-output-stdout">
<pre>  group weight
1  ctrl  5.032
2  trt1  4.661
3  trt2  5.526</pre>
</div>
</div>
<p>The group means differ, but is that more variation than we’d expect if plant weights were unrelated to the treatments? We can use the overall F statistic from a linear model to measure how different the groups are. Larger values mean more variation between groups relative to the variation within them.</p>
<p>I’ll wrap that calculation in a small function, because we’ll be doing it quite a few times:</p>
<div class="cell">
<pre>f_stat &lt;- function(weight, group) {
  fit &lt;- lm(weight ~ group)
  unname(summary(fit)$fstatistic[[&quot;value&quot;]])
}

observed &lt;- with(PlantGrowth, f_stat(weight, group))
observed</pre>
<div class="cell-output cell-output-stdout">
<pre>[1] 4.846088</pre>
</div>
</div>
<p>This is an overall test across the groups. It doesn’t tell us which particular treatments differ.</p>
<p>For the permutation test, we’ll leave the group labels in place and shuffle the weights among them. Under the null hypothesis, we’re assuming those weights are exchangeable across the treatment groups.</p>
</section>
<section id="give-me-the-shuffled-values" class="level2">
<h2 data-anchor-id="give-me-the-shuffled-values">Give me the shuffled values</h2>
<p>Usually, <code>shuffleSet()</code> returns permuted <em>indices</em>, which we then use to rearrange our data. <code>permutations()</code> does that indexing step for us:</p>
<div class="cell">
<pre>ctrl &lt;- how(nperm = 9999)

set.seed(42)
shuffled &lt;- permutations(PlantGrowth$weight, control = ctrl)

dim(shuffled)</pre>
<div class="cell-output cell-output-stdout">
<pre>[1] 9999   30</pre>
</div>
</div>
<p>Each row contains one shuffled version of the weight vector. Columns correspond to the original observation positions, so we can pair every row with the unchanged <code>PlantGrowth$group</code> vector. Here are the first few values from a few rows:</p>
<div class="cell">
<pre>as.matrix(shuffled)[seq_len(3), seq_len(5)]</pre>
<div class="cell-output cell-output-stdout">
<pre>     [,1] [,2] [,3] [,4] [,5]
[1,] 6.03 4.50 4.17 5.37 5.14
[2,] 4.81 5.87 5.12 5.29 4.53
[3,] 5.14 4.53 4.50 4.17 6.03</pre>
</div>
</div>
<p>Those are plant weights, ready to use. <code>as.matrix()</code> just removes the extra permutation metadata for this little look.</p>
<p>Now fit the model for each row in <code>shuffled</code> and collect the F statistics:</p>
<div class="cell">
<pre>null_values &lt;- apply(shuffled, 1, f_stat, group = PlantGrowth$group)</pre>
</div>
<p>That’s our permutation distribution. The <img src="https://latex.codecogs.com/png.latex?p">-value is the proportion of results at least as large as the observed F statistic, including the observed result itself:</p>
<div class="cell">
<pre>tol &lt;- sqrt(.Machine$double.eps)
p_value &lt;- (1 + sum(null_values &gt;= observed - tol)) /
  (length(null_values) + 1)

c(F = observed, p_value = p_value)</pre>
<div class="cell-output cell-output-stdout">
<pre>       F  p_value 
4.846088 0.014200 </pre>
</div>
</div>
<p>The small tolerance lets us count ties despite minor numerical differences in the model fits. Adding one to the numerator and denominator includes the observed statistic in this Monte Carlo test. We count the upper tail because larger F statistics are more extreme; there’s no need to double the result for a test of differences in either direction.</p>
<p>We can also have a look at the data and the permutation distribution:</p>
<div class="cell">
<pre>old_par &lt;- par(mfrow = c(1, 2))
boxplot(weight ~ group, data = PlantGrowth,
        xlab = &quot;Group&quot;, ylab = &quot;Dried plant weight&quot;,
        col = &quot;grey85&quot;)
hist(null_values, breaks = 40, main = &quot;&quot;,
     xlab = &quot;F statistic after shuffling weights&quot;,
     col = &quot;grey85&quot;, border = &quot;white&quot;)
abline(v = observed, col = &quot;firebrick&quot;, lwd = 2)
par(old_par)</pre>
<div class="cell-output-display">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://i2.wp.com/fromthebottomoftheheap.net/2026/10/13/permute-permutations-and-permutator/index_files/figure-html/plantgrowth-permutation-distribution-1.png?w=450&#038;ssl=1" class="img-fluid figure-img" alt="Two panels: boxplots of dried plant weight for the control and two treatments, and a histogram of F statistics after shuffling weights. The observed F statistic is marked by a red line in the right tail."  data-recalc-dims="1"></p>
<figcaption>Plant weights by group (left) and the permutation distribution of the F statistic (right). The red line marks the observed F statistic.</figcaption>
</figure>
</div>
</div>
</div>
<p>The observed value sits out in the right tail. For this unrestricted permutation test, there’s evidence of differences among the treatment groups. The p-value is estimated from random permutations, so it will vary with the seed and the number we request.</p>
<p><code>permutations()</code> works with an atomic vector, rather than a whole data frame. If you give it a factor, it returns the shuffled character labels. It also follows the usual <code>shuffleSet()</code> rules about checking and complete enumeration, so the number of returned rows can differ from the number requested for small permutation spaces. Use the size of the returned object in subsequent calculations, as we did above.</p>
</section>
<section id="one-permutation-at-a-time" class="level2">
<h2 data-anchor-id="one-permutation-at-a-time">One permutation at a time</h2>
<p>Sometimes a loop<sup>1</sup> is a more convenient place to fit a model, collect several results, or keep track of progress. That’s one situation where <code>permutator()</code> comes in.</p>
<p>Calling it creates a function. Calling <em>that</em> function returns the next permutation of observation indices. I’ll reset the seed so that we use the same set we used above:</p>
<div class="cell">
<pre>set.seed(42)
next_perm &lt;- permutator(nrow(PlantGrowth), control = ctrl)

null_iter &lt;- numeric(attr(next_perm, &quot;nperm&quot;))

for (b in seq_along(null_iter)) {
  i &lt;- next_perm()
  null_iter[b] &lt;- f_stat(PlantGrowth$weight[i], PlantGrowth$group)
}</pre>
</div>
<p>Here <code>i</code> contains indices, so <code>PlantGrowth$weight[i]</code> does the shuffling. The iterator’s <code>&quot;nperm&quot;</code> attribute tells us how many permutations are actually in the set; we use that to allocate the results vector.</p>
<p>Once we’ve used all of the permutations, another call returns <code>NULL</code>:</p>
<div class="cell">
<pre>next_perm()</pre>
<div class="cell-output cell-output-stdout">
<pre>NULL</pre>
</div>
</div>
<p>An iterator remembers how far you’ve got. If you run the loop again with this exhausted iterator, it won’t start over; create a fresh one first.</p>
<p>Did the two approaches give us the same F statistics?</p>
<div class="cell">
<pre>all.equal(null_values, null_iter)</pre>
<div class="cell-output cell-output-stdout">
<pre>[1] TRUE</pre>
</div>
</div>
<p>They did. We reset the seed and used the same design, so both helpers worked through the same underlying index permutations. We can calculate the <img src="https://latex.codecogs.com/png.latex?p">-value from the loop results in exactly the same way:</p>
<div class="cell">
<pre>p_iter &lt;- (1 + sum(null_iter &gt;= observed - tol)) /
  (length(null_iter) + 1)

c(values = p_value, iterator = p_iter)</pre>
<div class="cell-output cell-output-stdout">
<pre>  values iterator 
  0.0142   0.0142 </pre>
</div>
</div>
<p>One detail to keep in mind: <code>permutator()</code> generates and stores the permutation matrix when you create the iterator. It hands out rows one at a time, but it isn’t generating them lazily or removing the need to store the index matrix.</p>
</section>
<section id="reusing-a-set" class="level2">
<h2 data-anchor-id="reusing-a-set">Reusing a set</h2>
<p>You can also pass an existing index matrix to <code>permutator()</code>. This is useful when you want several calculations to use exactly the same permutations, or want to revisit a saved set later:</p>
<div class="cell">
<pre>set.seed(42)
indices &lt;- shuffleSet(nrow(PlantGrowth), control = ctrl)

next_perm &lt;- permutator(perms = indices)
i &lt;- next_perm()
head(PlantGrowth$weight[i])</pre>
<div class="cell-output cell-output-stdout">
<pre>[1] 6.03 4.50 4.17 5.37 5.14 6.11</pre>
</div>
</div>
<p>Creating another iterator over that matrix starts from the beginning:</p>
<div class="cell">
<pre>replay &lt;- permutator(perms = indices)
identical(i, replay())</pre>
<div class="cell-output cell-output-stdout">
<pre>[1] TRUE</pre>
</div>
</div>
<p>Supply either <code>perms</code> or the generation arguments such as <code>n</code>, <code>nset</code>, and <code>control</code>. The matrix passed to <code>perms</code> must contain observation indices, such as those returned by <code>shuffleSet()</code>. The shuffled weights from <code>permutations()</code> aren’t interchangeable with that matrix.</p>
<p>For a single vector and a short calculation, I like <code>permutations()</code> with <code>apply()</code>. When the calculation fits more naturally in a loop, or I want to reuse a saved set of indices, <code>permutator()</code> is handy. Both leave the choice of permutation design with <code>how()</code>, so the same workflow extends to restricted designs when the data call for them. The <a href="https://gavinsimpson.r-universe.dev/permute/doc/manual.html" rel="nofollow" target="_blank">package manual</a> has the details, and feedback is welcome on the <a href="https://github.com/gavinsimpson/permute/issues" rel="nofollow" target="_blank">issue tracker</a>.</p>


</section>


<div id="quarto-appendix" class="default"><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>Yes, I know <code>apply</code> is just a loop…<img src="https://s.w.org/images/core/emoji/13.0.0/72x72/21a9.png" alt="↩" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎</p></li>
</ol>
</section></div> 
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://fromthebottomoftheheap.net/2026/10/13/permute-permutations-and-permutator/"> From the bottom of the heap — R posts</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/creating-a-permutation-test-two-ways/">Creating a permutation test, two ways</a>]]></content:encoded>
					
		
		<enclosure url="https://fromthebottomoftheheap.net/assets/img/posts/permute-permutations-and-permutator.png" length="0" type="image/png" />

		<post-id xmlns="com-wordpress:feed-additions:1">404200</post-id>	</item>
		<item>
		<title>Beyond the course: Reflections from the Bioconductor–Physalia sponsorship programme</title>
		<link>https://www.r-bloggers.com/2026/10/beyond-the-course-reflections-from-the-bioconductor-physalia-sponsorship-programme/</link>
		
		<dc:creator><![CDATA[Carlo Pecoraro]]></dc:creator>
		<pubDate>Mon, 12 Oct 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://blog.bioconductor.org/posts/2026-10-12-physalia-sponsored-places/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>What difference can a sponsored place on a bioinformatics course make? For some researchers, it means access to training they would otherwise struggle to afford. For others, it is an opportunity to explore a new field, work through unfamili...</p></div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/beyond-the-course-reflections-from-the-bioconductor-physalia-sponsorship-programme/">Beyond the course: Reflections from the Bioconductor–Physalia sponsorship programme</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://blog.bioconductor.org/posts/2026-10-12-physalia-sponsored-places/"> Bioconductor community blog</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
 





<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://i1.wp.com/blog.bioconductor.org/posts/2026-10-12-physalia-sponsored-places/sponsored-places-map.jpg?w=578&#038;ssl=1" class="img-fluid quarto-figure quarto-figure-center figure-img" style="width:100.0%" alt="World map showing 18 sponsored course places across nine countries from November 2025 to October 2026: Nigeria 5; Benin, Egypt, India, Kenya and Nepal 2 each; Brazil, The Gambia and Tanzania 1 each." data-recalc-dims="1"></p>
</figure>
</div>
<p>What difference can a sponsored place on a bioinformatics course make? For some researchers, it means access to training they would otherwise struggle to afford. For others, it is an opportunity to explore a new field, work through unfamiliar analytical methods, or find new ways to approach an existing research question.</p>
<p>Through the <a href="https://blog.bioconductor.org/posts/2025-06-20-physalia-collaboration/" rel="nofollow" target="_blank">Bioconductor–Physalia collaboration</a>, Physalia offers two free places on eligible courses to researchers based in low- and middle-income countries (LMICs). The Bioconductor Community Manager coordinates the application process and works with the <a href="https://training.bioconductor.org/" rel="nofollow" target="_blank">Training Committee</a> co-chairs on selecting participants, while Bioconductor helps promote the courses through its community channels. Between November 2025 and October 2026, the programme provided 18 sponsored places across nine courses, covering topics ranging from RNA-seq and proteomics to microbiome analysis, epigenomics and network biology.</p>
<p>The aim is to make specialist training more accessible to researchers who might otherwise miss out on these opportunities. We asked participants what they found useful about their courses and how they planned to use what they had learned. Their responses offer a glimpse into the different ways the training connects with their work.</p>
<section id="making-training-accessible" class="level2">
<h2 class="anchored" data-anchor-id="making-training-accessible">Making training accessible</h2>
<p>Access was a recurring theme in the feedback. For Blessing Chioma Oselu, a PhD student and Bioinformatics Research Assistant at Covenant University in Nigeria, the opportunity to attend the <em>Single-cell RNA-seq Analysis with R/Bioconductor</em> course meant learning directly from experts and having structured materials to return to afterward.</p>
<p>“The access to expert teaching and the structured materials were the most valuable parts for me,” she wrote.</p>
<p>Blessing plans to continue working through the recorded sessions at her own pace. For researchers balancing training with other commitments, being able to revisit course material can be just as useful as attending the live sessions.</p>
<p>Another participant, from Benin, came to the <em>Epigenomics Data Analysis</em> course without previous experience in the subject. The step-by-step walkthroughs of methods including ChIP-seq and ATAC-seq helped make the material more manageable.</p>
<p>“I left the course at a clearly higher level than I started, and I feel ready to work with my own data when the time comes,” the participant reflected.</p>
<p>They also noted that opportunities like this are rare where they are based, and that support from Bioconductor and Physalia made a real difference.</p>
</section>
<section id="taking-new-methods-back-to-research" class="level2">
<h2 class="anchored" data-anchor-id="taking-new-methods-back-to-research">Taking new methods back to research</h2>
<p>Participants also described how the training related to questions they were already working on, or hoped to explore.</p>
<p>For Amira M. F. Abozaid, a PhD student and Assistant Lecturer at Alexandria University and E-JUST in Egypt, the <em>RNA-seq data analysis with R/Bioconductor</em> course provided a deeper understanding of the RNA-seq workflow, from quality control and normalization to differential expression and visualization.</p>
<p>Amira wrote that she had already started applying the workflows to her cancer genomics projects and sharing the skills with colleagues and students at her home institutions.</p>
<p>Other participants were looking at different applications. Idris Ishola Adejumobi, a Research Associate at the International Institute of Tropical Agriculture in Nigeria, described how the <em>Epigenomics Data Analysis</em> course broadened his understanding of how epigenetic variation can influence complex traits and complement genomic information in crop improvement. He also identified further training in the integration of epigenomic, genomic, and transcriptomic data as a useful next step for agricultural and plant-breeding research.</p>
<p>Rabiath Adigoun, a postdoctoral researcher at the University of Abomey-Calavi in Benin, highlighted the relevance of <em>Orchestrating Microbiome Analysis with Bioconductor</em> to her ongoing and future research on plant microbiomes.</p>
<p>“The expertise and methodological skills I have acquired will substantially strengthen my ongoing and future research on the plant microbiome in Benin and across Africa,” she wrote.</p>
<p>Sally Karimi Kinyua, a Research Assistant based in Kenya, attended the Network Analysis in Systems Biology with R/Bioconductor course. She hopes to apply what she learned about gene networks to research into mutations associated with malaria resistance. Sally also highlighted how the sponsored place gave her access to training that would otherwise have been out of reach because of limited resources.</p>
<p>These examples span different fields, but the underlying theme is similar: participants are looking for ways to connect computational methods with the biological questions they want to answer.</p>
</section>
<section id="learning-to-work-with-unfamiliar-data" class="level2">
<h2 class="anchored" data-anchor-id="learning-to-work-with-unfamiliar-data">Learning to work with unfamiliar data</h2>
<p>Sometimes, what matters most is finding a way into a subject that initially seems difficult.</p>
<p>A participant in the <em>Statistical Methods for Metatranscriptomics</em> course described how the practical sessions helped make complex concepts easier to follow. They particularly appreciated the explanations of foundational terminology before moving on to more technical material.</p>
<p>The course also introduced them to ways of analysing microbial gene expression data using computational tools and reproducible workflows. Coming from a bachelor’s degree background, they described it as one of their first deeper exposures to this area of bioinformatics.</p>
<p>Another participant, Binod Rayamajhee, a Research Fellow at the Kathmandu Research Institute for Biological Sciences in Nepal, highlighted the course’s combination of conceptual teaching and practical work with Bioconductor tools.</p>
<p>The training relates directly to his work in microbiome data analysis, and he anticipates applying the methods to study host–microbe interactions in bowel disease models.</p>
<p>Binod also hopes to share what he learned through workshops and one-to-one mentoring with colleagues and junior researchers, particularly those working in resource-limited settings.</p>
</section>
<section id="sharing-what-comes-next" class="level2">
<h2 class="anchored" data-anchor-id="sharing-what-comes-next">Sharing what comes next</h2>
<p>Knowledge sharing appeared in several participants’ reflections, whether through helping colleagues, teaching students, or bringing new analytical approaches into research groups.</p>
<p>Sandra Achieng Otieno, an MSc student at icipe in Kenya, attended the microbiome analysis course. She was particularly interested in learning how to structure microbiome data using TreeSummarizedExperiment and how different transformations can affect downstream analyses, including diversity estimation and ordination.</p>
<p>She hopes to apply these skills to research combining microbiome data with target gene expression and elemental analysis.</p>
<p>As Sandra put it, the course gave her “a clearer framework for integrating multi-omics biological data in a structured and reproducible way.”</p>
<p>For others, training may also open the door to contributing to Bioconductor itself. Marco Antonio Campanário Sampaio, a PhD student in Brazil who attended the Developing R/Bioconductor Packages for Genomics course, wrote, “I hope to become a contributor to the Bioconductor community soon.”</p>
<p>These are plans at different stages, and their longer-term outcomes remain to be seen. Still, it is useful to hear participants identify specific methods they want to try, datasets they want to analyse, and colleagues with whom they hope to share their knowledge.</p>
</section>
<section id="continuing-the-conversation" class="level2">
<h2 class="anchored" data-anchor-id="continuing-the-conversation">Continuing the conversation</h2>
<p>The feedback collected so far gives us a better sense of what participants value in specialist bioinformatics training. Expert teaching and well-organised materials matter, as do practical exercises that make unfamiliar methods easier to approach. Just as importantly, participants want to connect what they learn to their own research.</p>
<p>We would like to thank everyone who shared their reflections, along with the instructors and organisers who contributed to the courses. We’d also like to thank Laurah Nyasita Ondari for helping promote these opportunities through Bioconductor’s social media channels.</p>
<p>We hope to hear from participants again as they put their new skills into practice. In the meantime, their experiences offer a useful reminder of why access to training matters: it gives researchers more opportunities to learn new methods, explore their research questions, and contribute to the scientific communities around them.</p>
<p>For the full list of the Physalia courses and workshops, please visit <a href="https://www.physalia-courses.org/" rel="nofollow" target="_blank">www.physalia-courses.org</a>. To find out more about the collaboration, explore the <a href="https://training.bioconductor.org/collaborations/physalia/" rel="nofollow" target="_blank">Bioconductor-Physalia course listings</a>, including upcoming courses and how to apply for a sponsored place.</p>
<p>Interested in developing or teaching a Bioconductor-related course? Physalia welcomes ideas from potential instructors and is happy to discuss possible topics, content and formats. <a href="https://www.physalia-courses.org/contact/" rel="nofollow" target="_blank">Get in touch with Physalia</a> to explore an idea.</p>
<p><em>This article is also published on the <a href="https://www.physalia-courses.org/courses-workshops/blogpost/" rel="nofollow" target="_blank">Physalia website</a>.</em></p>


</section>

<p>
© 2026 Bioconductor. Content is published under <a href="https://creativecommons.org/licenses/by/4.0/" rel="nofollow" target="_blank">Creative Commons CC-BY-4.0 License</a> for the text and <a href="https://opensource.org/licenses/BSD-3-Clause" rel="nofollow" target="_blank">BSD 3-Clause License</a> for any code. | <a href="https://www.r-bloggers.com/" rel="nofollow" target="_blank">R-Bloggers</a>
</p> 
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://blog.bioconductor.org/posts/2026-10-12-physalia-sponsored-places/"> Bioconductor community blog</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/beyond-the-course-reflections-from-the-bioconductor-physalia-sponsorship-programme/">Beyond the course: Reflections from the Bioconductor–Physalia sponsorship programme</a>]]></content:encoded>
					
		
		<enclosure url="https://blog.bioconductor.org/posts/2026-10-12-physalia-sponsored-places/sponsored-places-map.jpg" length="0" type="image/jpeg" />

		<post-id xmlns="com-wordpress:feed-additions:1">404185</post-id>	</item>
		<item>
		<title>Ordered set partitions and formulas in permute</title>
		<link>https://www.r-bloggers.com/2026/10/ordered-set-partitions-and-formulas-in-permute/</link>
		
		<dc:creator><![CDATA[Gavin Simpson]]></dc:creator>
		<pubDate>Sun, 11 Oct 2026 22:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://fromthebottomoftheheap.net/2026/10/12/permute-partitions-and-formulas/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>I’ve been giving permute a bit of attention recently. Two additions should make it easier to set up permutation tests: an option for ordered set partitions, and formulas for specifying plots and blocks. That’s a rather grand name for the first ad...</p></div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/ordered-set-partitions-and-formulas-in-permute/">Ordered set partitions and formulas in permute</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://fromthebottomoftheheap.net/2026/10/12/permute-partitions-and-formulas/"> From the bottom of the heap — R posts</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>






<div class="no-row-height column-margin column-container"></div><p>I’ve been giving <strong>permute</strong> a bit of attention recently. Two additions should make it easier to set up permutation tests: an option for <em>ordered set partitions</em>, and formulas for specifying plots and blocks. That’s a rather grand name for the first addition, so let’s start with what it does.</p>
<p>These changes are in the <a href="https://github.com/gavinsimpson/permute" rel="nofollow" target="_blank">development version</a>. To follow along, install it from my R-universe:</p>
<div class="cell">
<pre>install.packages(&quot;permute&quot;, repos = c(
  &quot;https://gavinsimpson.r-universe.dev&quot;,
  &quot;https://cloud.r-project.org&quot;
))</pre>
</div>
<section id="same-groups-fewer-permutations" class="level2">
<h2 data-anchor-id="same-groups-fewer-permutations">Same groups, fewer permutations</h2>
<p>The <code>jackal</code> data that come with <strong>permute</strong> contain mandible lengths, in millimetres, for male and female golden jackals. They’re also used in the <a href="https://gavinsimpson.github.io/permute/articles/permutations.html" rel="nofollow" target="_blank">package vignette</a>. Let’s ask whether mean mandible length differs between the sexes.</p>
<div class="cell">
<pre>library(&quot;permute&quot;)
data(jackal, package = &quot;permute&quot;)

table(jackal$Sex)</pre>
<div class="cell-output cell-output-stdout">
<pre>
  Male Female 
    10     10 </pre>
</div>
</div>
<p>We could shuffle all the observation indices and use them to rearrange the sex labels. With the default <code>Within(type = &quot;free&quot;)</code> design, the number of possible index permutations is:</p>
<div class="cell">
<pre>numPerms(nrow(jackal), control = how(within = Within(type = &quot;free&quot;)))</pre>
<div class="cell-output cell-output-stdout">
<pre>[1] 2.432902e+18</pre>
</div>
</div>
<p>But swapping a pair of males, or a pair of females, leaves the group labels unchanged. If our statistic is a difference of means, we haven’t created a different result by doing that.</p>
<p>What we need are the distinct ways of assigning the observations to two labelled groups, keeping the group sizes fixed. That’s the ordered set partition. Here, <em>ordered</em> means the groups are distinguishable — male and female have different roles — rather than that observations have to be sorted inside each group.</p>
<p>The new option is <code>type = &quot;partition&quot;</code> which belongs in <code>Plots()</code>. I’ll use the new formula interface here too; <code>~ Sex</code> says to get the grouping variable from <code>data</code>, to which we pass <code>jackal</code>:</p>
<div class="cell">
<pre>ctrl &lt;- how(
  plots = Plots(strata = ~ Sex, type = &quot;partition&quot;),
  data = jackal
)

numPerms(nrow(jackal), control = ctrl)</pre>
<div class="cell-output cell-output-stdout">
<pre>[1] 184756</pre>
</div>
</div>
<p>That’s quite a reduction from all the possible permutations of the observation indices! In general, if the group sizes are <img src="https://latex.codecogs.com/png.latex?n_1,%20%5Cldots,%20n_K">, the number of distinct assignments is</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Cfrac%7Bn!%7D%7B%5Cprod_%7Bk=1%7D%5E%7BK%7D%20n_k!%7D.%0A"></p>
<p>The group sizes come from the data; they don’t have to be equal.</p>
<p>We still get observation indices back from <strong>permute</strong>, so the usual indexing workflow works:</p>
<div class="cell">
<pre>set.seed(42)
p &lt;- shuffle(nrow(jackal), control = ctrl)

table(jackal$Sex[p])</pre>
<div class="cell-output cell-output-stdout">
<pre>
  Male Female 
    10     10 </pre>
</div>
</div>
<p>The labels have been shuffled, but their counts haven’t changed. Within the returned index vector, observations that originally had the same label retain their relative order. This gives us one representative for each distinct arrangement of the labels, instead of all the index permutations that would produce it.</p>
<p>There’s an important difference from <code>Plots(type = &quot;free&quot;)</code>: that option moves whole plots as intact units. The partition option rearranges group membership. <code>how()</code> also selects <code>Within(type = &quot;none&quot;)</code> automatically for a partition design. Asking to shuffle within these groups as well would put back the redundant orderings we’re trying to leave out, so other within-plot types aren’t allowed.</p>
</section>
<section id="a-permutation-test-with-lm" class="level2">
<h2 data-anchor-id="a-permutation-test-with-lm">A permutation test with lm()</h2>
<p><em>permute</em> isn’t just for use with the <em>vegan</em> package — it was designed to be agnostic to the underlying model or test statistic. As an illustration, let’s fit a linear model to the jackal data:</p>
<div class="cell">
<pre>fit &lt;- lm(Length ~ Sex, data = jackal)
coef(fit)</pre>
<div class="cell-output cell-output-stdout">
<pre>(Intercept)   SexFemale 
      113.4        -4.8 </pre>
</div>
</div>
<p>The intercept is the mean for males, the reference level. The second coefficient is the female mean minus the male mean: females have mandibles that are, on average, 4.8 mm shorter in these data.</p>
<p>For a permutation test, we’ll keep the lengths where they are and shuffle the sex labels. We then refit the model for each assignment and save that same coefficient. Under the null hypothesis, we’re treating the labels as exchangeable among observations; this needs to be a reasonable assumption for the data we’re analysing.</p>
<div class="cell">
<pre>set.seed(42)
perms &lt;- shuffleSet(nrow(jackal), nset = 999, control = ctrl)

observed &lt;- unname(coef(fit)[2])
null &lt;- apply(perms, 1, function(i) {
  permuted &lt;- jackal
  permuted$Sex &lt;- jackal$Sex[i]
  unname(coef(lm(Length ~ Sex, data = permuted))[2])
})</pre>
</div>
<p><code>shuffleSet()</code> gives us a matrix with one assignment per row. We’re sampling a manageable set here, rather than fitting a model for every possible assignment.</p>
<p>Here’s what those coefficients look like, with the observed difference marked in red:</p>
<div class="cell">
<pre>hist(null, breaks = &quot;FD&quot;, main = &quot;&quot;,
     xlab = &quot;Female minus male mean mandible length (mm)&quot;,
     col = &quot;grey85&quot;, border = &quot;white&quot;)
abline(v = observed, col = &quot;firebrick&quot;, lwd = 2)</pre>
<div class="cell-output-display">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://i2.wp.com/fromthebottomoftheheap.net/2026/10/12/permute-partitions-and-formulas/index_files/figure-html/jackal-permutation-distribution-1.png?w=450&#038;ssl=1" class="img-fluid figure-img" alt="Histogram of differences in mean jackal mandible length after shuffling sex labels. The distribution is centred near zero, and a red vertical line marks the observed negative difference in the left tail."  data-recalc-dims="1"></p>
<figcaption>Permutation distribution of the female-minus-male mean mandible length. The red line marks the observed difference.</figcaption>
</figure>
</div>
</div>
</div>
<p>For a two-sided test, we count differences at least as large as the observed one in absolute value:</p>
<div class="cell">
<pre>tol &lt;- sqrt(.Machine$double.eps)
p_value &lt;- (1 + sum(abs(null) &gt;= abs(observed) - tol)) /
  (nrow(perms) + 1)

c(difference_mm = observed, p_value = p_value)</pre>
<div class="cell-output cell-output-stdout">
<pre>difference_mm       p_value 
       -4.800         0.005 </pre>
</div>
</div>
<p>The small tolerance includes ties that might otherwise be missed because of floating-point arithmetic in the model fits. Adding one to the numerator and denominator includes the observed result in this Monte Carlo test. The p-value is an estimate based on the sampled assignments, so it will vary with the random seed and the number requested.</p>
<p>If you don’t need to build a design explicitly, <code>shufflePartition(jackal$Sex)</code> and <code>shufflePartitionSet(jackal$Sex, nset = 999)</code> are handy shortcuts. There’s also <code>allPartitions()</code> for complete enumeration when the assignment space is small enough. As with <code>allPerms()</code>, it leaves out the observed assignment by default; use <code>control = how(observed = TRUE)</code> to include it.</p>
</section>
<section id="less-typing-with-formulas" class="level2">
<h2 data-anchor-id="less-typing-with-formulas">Less typing with formulas</h2>
<p>The formula interface works for both plot strata and blocks. To show the two together, let’s use the dune meadow data from <strong>vegan</strong>. <code>dune</code> contains the species data and <code>dune.env</code> contains the matching environmental variables.</p>
<p>Suppose we want to look at the association between vegetation and soil A1 horizon thickness, allowing comparisons only among sites with the same management and land use. We can express that restriction directly:</p>
<div class="cell">
<pre>library(&quot;vegan&quot;)
data(dune, package = &quot;vegan&quot;)
data(dune.env, package = &quot;vegan&quot;)

formula_ctrl &lt;- how(
  plots = Plots(strata = ~ Management / Use),
  blocks = ~ Management,
  data = dune.env
)</pre>
</div>
<p><code>~ Management / Use</code> creates a grouping factor for the observed combinations of management and land use. <code>Plots()</code> defaults to <code>type = &quot;none&quot;</code>, while <code>Within()</code> defaults to <code>&quot;free&quot;</code>, so sites are shuffled within those combinations. The blocks say that sites must also stay within their management category. The plot restriction already implies that here, but writing out both makes the hierarchy clear.</p>
<p>These restrictions assume sites are exchangeable within each management–use combination; grouping them this way doesn’t account for any remaining spatial dependence.</p>
<p>We can check where an actual shuffle sends the observations:</p>
<div class="cell">
<pre>set.seed(42)
i &lt;- shuffle(nrow(dune.env), control = formula_ctrl)

all(getBlocks(formula_ctrl)[i] == getBlocks(formula_ctrl))</pre>
<div class="cell-output cell-output-stdout">
<pre>[1] TRUE</pre>
</div>
<pre>all(getStrata(formula_ctrl)[i] == getStrata(formula_ctrl))</pre>
<div class="cell-output cell-output-stdout">
<pre>[1] TRUE</pre>
</div>
</div>
<p>Both the blocks and plot groups are respected. We can use the design directly in a <strong>vegan</strong> analysis. Here I partial out the management–use groups in an RDA, then test the additional association with A1 horizon thickness:</p>
<div class="cell">
<pre>env &lt;- dune.env
env$group &lt;- getStrata(formula_ctrl)
ord &lt;- rda(dune ~ A1 + Condition(group), data = env)

set.seed(42)
anova(ord, permutations = formula_ctrl)</pre>
<div class="cell-output cell-output-stdout">
<pre>Permutation test for rda under reduced model
Blocks:  Management 
Plots: Management/Use, plot permutation: none
Permutation: free
Number of permutations: 199

Model: rda(formula = dune ~ A1 + Condition(group), data = env)
         Df Variance      F Pr(&gt;F)
Model     1   4.3492 1.3265  0.225
Residual  8  26.2298              </pre>
</div>
</div>
<p>There’s little evidence of an additional association in this particular analysis. Some of the groups contain only one site, so those sites can’t move at all under this design. Restrictions matter, both for what question we’re asking and for how much information is available to answer it.</p>
<p>A couple of details are worth keeping in mind when writing these formulas. They describe grouping factors, not model terms: <code>~ Management + Use</code>, <code>~ Management:Use</code>, and <code>~ Management / Use</code> all produce groups from the combinations of those variables. The slash doesn’t create an extra level in <strong>permute</strong>’s hierarchy.</p>
<p>Also, when <code>Plots()</code> is inside <code>how()</code>, supply <code>data</code> to <code>how()</code>, as above. If you’re constructing <code>Plots()</code> on its own, it can take its own data argument:</p>
<div class="cell">
<pre>plot_groups &lt;- Plots(strata = ~ Management / Use, data = dune.env)</pre>
</div>
<p>The existing factor interface still works, so there’s no need to rewrite old code. But I find the formulas easier to read, especially when grouping variables need to be combined. You can find the details in the <a href="https://gavinsimpson.github.io/permute/reference/how.html" rel="nofollow" target="_blank">design documentation</a>, and I’d welcome feedback on either addition through the <a href="https://github.com/gavinsimpson/permute/issues" rel="nofollow" target="_blank">issue tracker</a>.</p>
<p>There are also new helpers for working with the resulting permutations: <code>permutations()</code> returns shuffled values directly, while <code>permutator()</code> lets you step through a set of permutations. I’ll show those in a follow-up post.</p>


</section>

 
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://fromthebottomoftheheap.net/2026/10/12/permute-partitions-and-formulas/"> From the bottom of the heap — R posts</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/ordered-set-partitions-and-formulas-in-permute/">Ordered set partitions and formulas in permute</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404187</post-id>	</item>
		<item>
		<title>Going back to the basics: the correlation coefficient</title>
		<link>https://www.r-bloggers.com/2026/10/going-back-to-the-basics-the-correlation-coefficient-2/</link>
		
		<dc:creator><![CDATA[Andrea Onofri]]></dc:creator>
		<pubDate>Thu, 08 Oct 2026 22:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://www.statforbiology.com/posts/general_Correlation_ALookAt.html</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>In statistics, dependence or association is any statistical relationship, whether causal or not, between two random variables or bivariate data. It is often measured by the Pearson’s correlation coefficient:</p>
<p>Other measures of correlation can be...</p></div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/going-back-to-the-basics-the-correlation-coefficient-2/">Going back to the basics: the correlation coefficient</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://www.statforbiology.com/posts/general_Correlation_ALookAt.html"> Statforbiology</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
 





<p>In statistics, dependence or association is any statistical relationship, whether causal or not, between two random variables or bivariate data. It is often measured by the Pearson’s correlation coefficient:</p>
<p><img src="https://latex.codecogs.com/png.latex?%5Crho%20_%7BX,Y%7D%20=%5Ctextrm%7Bcorr%7D%20(X,Y)%20=%20%5Cfrac%20%7B%5Ctextrm%7Bcov%7D(X,Y)%20%7D%7B%20%5Csigma_X%20%5Csigma_Y%20%7D%20=%20%5Cfrac%7B%20%5Csum_%7Bi%20=%201%7D%5En%20%5B(X%20-%20%5Cmu_X)(Y%20-%20%5Cmu_Y)%5D%20%7D%7B%20%5Csigma_X%20%5Csigma_Y%20%7D"></p>
<p>Other measures of correlation can be thought of, such as the Spearman <img src="https://latex.codecogs.com/png.latex?%5Crho"> rank correlation coefficient or Kendall <img src="https://latex.codecogs.com/png.latex?%5Ctau"> rank correlation coefficient.</p>
<section id="assumptions-for-the-pearson-correlation-coefficient" class="level1">
<h1>Assumptions for the Pearson Correlation Coefficient</h1>
<p>The Pearson correlation coefficients makes a few assumptions, which should be carefully checked.</p>
<ol type="1">
<li><strong>Interval-level measurement:</strong> Both variables should be measured on a quantitative scale.</li>
<li><strong>Random sampling:</strong> Each subject in the sample should contribute one value on <img src="https://latex.codecogs.com/png.latex?X"> and one value on <img src="https://latex.codecogs.com/png.latex?Y">. The values for both variables should represent a random sample drawn from the population of interest.</li>
<li><strong>Linearity:</strong> The relationship between <img src="https://latex.codecogs.com/png.latex?X"> and <img src="https://latex.codecogs.com/png.latex?Y"> should be linear.</li>
<li><strong>Bivariate normal distribution:</strong> This means that (i) the values of <img src="https://latex.codecogs.com/png.latex?X"> follow a normal distribution at each level of <img src="https://latex.codecogs.com/png.latex?Y">, and (ii) the values of <img src="https://latex.codecogs.com/png.latex?Y"> follow a normal distribution at each level of <img src="https://latex.codecogs.com/png.latex?X">.</li>
</ol>
</section>
<section id="hypothesis-testing" class="level1">
<h1>Hypothesis testing</h1>
<p>It is possible to test whether <img src="https://latex.codecogs.com/png.latex?r%20=%200"> against the alternative $ r $. The test is based on the test statistic:</p>
<p><img src="https://latex.codecogs.com/png.latex?%20T%20=%20%5Cfrac%7Br%20%5Csqrt%7Bn%20-%202%7D%7D%7B%5Csqrt%7B1%20-%20r%5E2%7D%7D"> which follows a Student’s <img src="https://latex.codecogs.com/png.latex?t"> distribution with <img src="https://latex.codecogs.com/png.latex?n-2"> degrees of freedom.</p>
<p>Let’s take the two variables <code>cyl</code> and <code>mpg</code> from the <code>mtcars</code> data frame. For these variables, the Pearson’s correlation index is (please note, that the Spearman’s correlation doefficient might be preferred for these variables, although I neglected this for the sake of simplicity):</p>
<div class="cell">
<pre>r &lt;- cor(mtcars$cyl, mtcars$gear)
r
## [1] -0.4926866</pre>
</div>
<p>The T statistic is:</p>
<div class="cell">
<pre>T &lt;- r * sqrt(32 - 2) / sqrt(1 - r^2)
T
## [1] -3.101051</pre>
</div>
<p>The p-value for the null hypothesis is:</p>
<div class="cell">
<pre>2 * pt(T, 30)
## [1] 0.004173297</pre>
</div>
<p>which is clearly highly significant. The null can be rejected.</p>
<p>When performing this test, it is important to note that the individuals from whom the pairs of measurements were taken must be independent. If they are not, the ‘t-test’ is invalid. I discuss this aspect in another post on my blog.</p>
</section>
<section id="correlation-in-r" class="level1">
<h1>Correlation in R</h1>
<p>We have already seen that we can use the standard cor(matrix, method = …) function. To obtain significance levels as well, we can use the <code>rcorr()</code> function from the ‘Hmisc’ package:</p>
<div class="cell">
<pre># Correlations with significance levels
library(Hmisc)
corr2 &lt;- rcorr(as.matrix(mtcars), type=&quot;pearson&quot;)
print(corr2$r, digits = 2)
##        mpg   cyl  disp    hp   drat    wt   qsec    vs     am  gear   carb
## mpg   1.00 -0.85 -0.85 -0.78  0.681 -0.87  0.419  0.66  0.600  0.48 -0.551
## cyl  -0.85  1.00  0.90  0.83 -0.700  0.78 -0.591 -0.81 -0.523 -0.49  0.527
## disp -0.85  0.90  1.00  0.79 -0.710  0.89 -0.434 -0.71 -0.591 -0.56  0.395
## hp   -0.78  0.83  0.79  1.00 -0.449  0.66 -0.708 -0.72 -0.243 -0.13  0.750
## drat  0.68 -0.70 -0.71 -0.45  1.000 -0.71  0.091  0.44  0.713  0.70 -0.091
## wt   -0.87  0.78  0.89  0.66 -0.712  1.00 -0.175 -0.55 -0.692 -0.58  0.428
## qsec  0.42 -0.59 -0.43 -0.71  0.091 -0.17  1.000  0.74 -0.230 -0.21 -0.656
## vs    0.66 -0.81 -0.71 -0.72  0.440 -0.55  0.745  1.00  0.168  0.21 -0.570
## am    0.60 -0.52 -0.59 -0.24  0.713 -0.69 -0.230  0.17  1.000  0.79  0.058
## gear  0.48 -0.49 -0.56 -0.13  0.700 -0.58 -0.213  0.21  0.794  1.00  0.274
## carb -0.55  0.53  0.39  0.75 -0.091  0.43 -0.656 -0.57  0.058  0.27  1.000
print(corr2$P, digits = 2)
##          mpg     cyl    disp      hp    drat      wt    qsec      vs      am
## mpg       NA 6.1e-10 9.4e-10 1.8e-07 1.8e-05 1.3e-10 1.7e-02 3.4e-05 2.9e-04
## cyl  6.1e-10      NA 1.8e-12 3.5e-09 8.2e-06 1.2e-07 3.7e-04 1.8e-08 2.2e-03
## disp 9.4e-10 1.8e-12      NA 7.1e-08 5.3e-06 1.2e-11 1.3e-02 5.2e-06 3.7e-04
## hp   1.8e-07 3.5e-09 7.1e-08      NA 1.0e-02 4.1e-05 5.8e-06 2.9e-06 1.8e-01
## drat 1.8e-05 8.2e-06 5.3e-06 1.0e-02      NA 4.8e-06 6.2e-01 1.2e-02 4.7e-06
## wt   1.3e-10 1.2e-07 1.2e-11 4.1e-05 4.8e-06      NA 3.4e-01 9.8e-04 1.1e-05
## qsec 1.7e-02 3.7e-04 1.3e-02 5.8e-06 6.2e-01 3.4e-01      NA 1.0e-06 2.1e-01
## vs   3.4e-05 1.8e-08 5.2e-06 2.9e-06 1.2e-02 9.8e-04 1.0e-06      NA 3.6e-01
## am   2.9e-04 2.2e-03 3.7e-04 1.8e-01 4.7e-06 1.1e-05 2.1e-01 3.6e-01      NA
## gear 5.4e-03 4.2e-03 9.6e-04 4.9e-01 8.4e-06 4.6e-04 2.4e-01 2.6e-01 5.8e-08
## carb 1.1e-03 1.9e-03 2.5e-02 7.8e-07 6.2e-01 1.5e-02 4.5e-05 6.7e-04 7.5e-01
##         gear    carb
## mpg  5.4e-03 1.1e-03
## cyl  4.2e-03 1.9e-03
## disp 9.6e-04 2.5e-02
## hp   4.9e-01 7.8e-07
## drat 8.4e-06 6.2e-01
## wt   4.6e-04 1.5e-02
## qsec 2.4e-01 4.5e-05
## vs   2.6e-01 6.7e-04
## am   5.8e-08 7.5e-01
## gear      NA 1.3e-01
## carb 1.3e-01      NA</pre>
</div>
<p>We can also use these functions with two separate matrices to obtain the correlations between each column of one matrix and each column of the other:</p>
<div class="cell">
<pre># Correlation matrix from mtcars
x &lt;- mtcars[1:3]
y &lt;- mtcars[4:6]
cor(x, y)
##              hp       drat         wt
## mpg  -0.7761684  0.6811719 -0.8676594
## cyl   0.8324475 -0.6999381  0.7824958
## disp  0.7909486 -0.7102139  0.8879799</pre>
</div>
</section>
<section id="relationship-to-slope-in-linear-regression" class="level1">
<h1>Relationship to slope in linear regression</h1>
<p>The correlation coefficient and the slope in linear regression share some similarities, as both describe how <img src="https://latex.codecogs.com/png.latex?Y"> changes with <img src="https://latex.codecogs.com/png.latex?X">. However, in correlation, we deal with two random variables, whereas in simple linear regression we treat <img src="https://latex.codecogs.com/png.latex?X"> as fixed and <img src="https://latex.codecogs.com/png.latex?Y"> as a random variable, with <img src="https://latex.codecogs.com/png.latex?Y"> regarded as a function of <img src="https://latex.codecogs.com/png.latex?X"> (and not the other way around).</p>
<p>Despite their distinct interpretations, it is useful to examine the algebraic relationship between the correlation coefficient and the regression slope. Let’s simulate a dataset with two variables drawn from a multivariate normal distribution, with means of 10 and 2, respectively, and a variance-covariance matrix of:</p>
<div class="cell">
<pre>library(MASS)
cov &lt;- matrix(c(2.20, 0.48, 0.48, 0.20), 2, 2)
cov
##      [,1] [,2]
## [1,] 2.20 0.48
## [2,] 0.48 0.20</pre>
</div>
<p>We use the <code>mvrnomr()</code> function to generate the dataset.</p>
<div class="cell">
<pre>set.seed(1234)
dataset &lt;- data.frame( mvrnorm(n=10, mu = c(10, 2), Sigma = cov) )
names(dataset) &lt;- c(&quot;X&quot;, &quot;Y&quot;)
dataset
##            X        Y
## 1  11.756647 2.547203
## 2   9.522180 2.199740
## 3   8.341254 1.862362
## 4  13.480005 2.772031
## 5   9.428296 1.573435
## 6   9.242788 1.861756
## 7  10.817449 2.343918
## 8  10.749047 2.451999
## 9  10.780400 2.436263
## 10 11.480301 1.590436</pre>
</div>
<p>The correlation coefficient and slope are as follows:</p>
<div class="cell">
<pre>r &lt;- with(dataset, cor(X, Y))
b1 &lt;- coef( lm(Y ~ X, data=dataset) )[2]
r
## [1] 0.6372927
b1
##         X 
## 0.1785312</pre>
</div>
<p>The formula for the slope is:</p>
<p><img src="https://latex.codecogs.com/png.latex?b_1%20=%20%5Cfrac%7B%20%5Csum_%7Bi%20=%201%7D%5En%20%5Cleft%5B%20(%20X-%5Cmu_X%20)(%20Y-%5Cmu_Y%20)%5Cright%5D%20%7D%7B%20%5Csigma%5E2_X%20%7D%20"></p>
<p>From this, we see that:</p>
<p><img src="https://latex.codecogs.com/png.latex?%20r%20=%20b_1%20%5Cfrac%7B%5Csigma_X%7D%7B%20%5Csigma_Y%20%7D"></p>
<p>and:</p>
<p><img src="https://latex.codecogs.com/png.latex?%20b_1%20=%20r%20%5Cfrac%7B%5Csigma_Y%7D%7B%5Csigma_X%7D"></p>
<p>Indeed:</p>
<div class="cell">
<pre>sigmaX &lt;- with(dataset, sd(X) )
sigmaY &lt;- with(dataset, sd(Y) )
b1 * sigmaX / sigmaY 
##         X 
## 0.6372927
r * sigmaY / sigmaX
## [1] 0.1785312</pre>
</div>
<p>It is also easy to see that the correlation coefficient is equivalent to the regression slope of standardized <img src="https://latex.codecogs.com/png.latex?Y"> against standardised <img src="https://latex.codecogs.com/png.latex?X">:</p>
<div class="cell">
<pre>Yst &lt;- with(dataset, scale(Y, scale=T) )
summary( lm(Yst ~ I(scale(X, scale = T) ), data = dataset) )
## 
## Call:
## lm(formula = Yst ~ I(scale(X, scale = T)), data = dataset)
## 
## Residuals:
##       Min        1Q    Median        3Q       Max 
## -0.082006 -0.067143 -0.036850  0.009214  0.237923 
## 
## Coefficients:
##                         Estimate Std. Error t value Pr(&gt;|t|)  
## (Intercept)            1.065e-18  3.478e-02   0.000   1.0000  
## I(scale(X, scale = T)) 1.785e-01  7.633e-02   2.339   0.0475 *
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.11 on 8 degrees of freedom
## Multiple R-squared:  0.4061, Adjusted R-squared:  0.3319 
## F-statistic: 5.471 on 1 and 8 DF,  p-value: 0.04748</pre>
</div>
</section>
<section id="intra-class-correlation-icc" class="level1">
<h1>Intra-class correlation (ICC)</h1>
<p>This is a related yet distinct concept. While Pearson’s correlation coefficient is used for bivariate data—where two different variables are observed on the same independent units—intraclass correlation arises when the same variable is repeatedly observed on the same units (e.g., across different times or spaces), so that each unit acts as a grouping factor that creates clusters of measurements.</p>
<p>Let’s illustrate this with an example. Consider the following data table:</p>
<div class="cell">
<pre>#|echo: false
m1 &lt;- c(10, 12, 13, 16)
m2 &lt;- c(21, 20, 17, 19)
dtab &lt;- cbind(m1, m2)
row.names(dtab) &lt;- paste0(&quot;g&quot;, 1:4)
knitr::kable(dtab)</pre>
<div class="cell-output-display">
<table class="caption-top table table-sm table-striped small">
<thead>
<tr class="header">
<th style="text-align: left;"></th>
<th style="text-align: right;">m1</th>
<th style="text-align: right;">m2</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td style="text-align: left;">g1</td>
<td style="text-align: right;">10</td>
<td style="text-align: right;">21</td>
</tr>
<tr class="even">
<td style="text-align: left;">g2</td>
<td style="text-align: right;">12</td>
<td style="text-align: right;">20</td>
</tr>
<tr class="odd">
<td style="text-align: left;">g3</td>
<td style="text-align: right;">13</td>
<td style="text-align: right;">17</td>
</tr>
<tr class="even">
<td style="text-align: left;">g4</td>
<td style="text-align: right;">16</td>
<td style="text-align: right;">19</td>
</tr>
</tbody>
</table>
</div>
</div>
<p>When g1, g2, g3, and g4 represent four independent units (e.g., four maize plants) on which two variables, m1 and m2 (e.g., weight and height), were measured, we are in the realm of Pearson’s correlation. On the other hand, when g1, g2, g3, and g4 are entities on which multiple measurements of the same variable (e.g., yield) are taken, we enter the realm of intraclass correlation. In this second scenario, we are not assessing joint variability between two different variables; rather, we want to determine whether belonging to the same group (‘proximity’) makes observations more similar to each other than observations from different groups.</p>
<div class="cell">
<div class="cell-output-display">
<div id="fig-icc" class="quarto-float quarto-figure quarto-figure-center anchored">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-icc-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<img src="https://i2.wp.com/www.statforbiology.com/Figures/TypesOfCorrelation_modified.jpg?w=578&#038;ssl=1" class="img-fluid figure-img" style="width:85.0%" data-recalc-dims="1">
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig" id="fig-icc-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure 1: Confronto concettuale tra Pearson e ICC
</figcaption>
</figure>
</div>
</div>
</div>
<p>The most common intraclass correlation coefficient is:</p>
<p><img src="https://latex.codecogs.com/png.latex?ICC%20=%20%7B%5Cdisplaystyle%20%7B%5Cfrac%20%7B%5Csigma%20_%7B%5Calpha%20%7D%5E%7B2%7D%7D%7B%5Csigma%20_%7B%5Calpha%20%7D%5E%7B2%7D+%5Csigma%20_%7B%5Cvarepsilon%20%7D%5E%7B2%7D%7D%7D.%7D"></p>
<p>where <img src="https://latex.codecogs.com/png.latex?%5Csigma%20_%7B%5Calpha%20%7D%5E%7B2%7D"> is the between-group variance component and <img src="https://latex.codecogs.com/png.latex?%5Csigma%20_%7B%5Cvarepsilon%20%7D%5E%7B2%7D"> is the within-group variance component (or, more clearly, the variance of observations within a group). For balanced data, those two components can be obtained from one-way ANOVA, considering that there are four ‘replicates’ per group:</p>
<div class="cell">
<pre>y &lt;- c(10, 12, 13, 16, 21, 20, 17, 19)
group &lt;- factor( rep(c(&quot;A&quot;, &quot;B&quot;), each = 4) )
nmeasures &lt;- 4
mod &lt;- lm(y ~ group)
anova(mod)
## Analysis of Variance Table
## 
## Response: y
##           Df Sum Sq Mean Sq F value   Pr(&gt;F)   
## group      1   84.5  84.500  18.436 0.005128 **
## Residuals  6   27.5   4.583                    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
sigma2_alpha &lt;- (84.5 - 4.583)/nmeasures
sigma2_epsilon &lt;- 4.583
icc &lt;- sigma2_alpha/(sigma2_alpha + sigma2_epsilon)
icc
## [1] 0.8134129</pre>
</div>
<p>In general, if subjects in one group are very similar (small <img src="https://latex.codecogs.com/png.latex?%5Csigma_%7B%5Cvarepsilon%7D">), while different groups vary widely (high <img src="https://latex.codecogs.com/png.latex?%5Csigma_%7B%5Calpha%7D">), the ICC is high. Conversely, if measurements are just as similar across different groups as they are within the same group, the ICC is low. In theory, the ICC cannot be negative, as variance components are expected to be positive. In practice, however, the ICC calculated via the method of moments shown above can turn out negative when the group mean square is lower than the residual mean square from the ANOVA. Such negative values are usually constrained to zero or interpreted as indicating an absence of positive clustering; in both cases, intraclass correlation remains a meaningful tool.</p>
<p>ICC is crucial and is most often studied using linear mixed models. But… that is a story for another day…</p>
<p>Thanks for reading, and happy coding!</p>
<p>(P.S. If you enjoyed this post, check out my book “Field Research Methods in Agriculture: An Introduction with R” via the link below!)</p>
<p>Prof. Andrea Onofri<br>
Department of Agricultural, Food and Environmental Sciences<br>
University of Perugia (Italy)<br>
Send comments to: <a href="mailto:andrea.onofri@unipg.it" rel="nofollow" target="_blank">andrea.onofri@unipg.it</a></p>
<p><a href="https://www.awin1.com/cread.php?awinmid=26429&#038;awinaffid=2675822&#038;ued=https%3A%2F%2Flink.springer.com%2Fbook%2F10.1007%2F978-3-032-08199-5" rel="nofollow" target="_blank"><img src="https://i0.wp.com/www.statforbiology.com/Figures/Email_Signature_978-3-032-08199-5.png?w=578&#038;ssl=1" alt="Book cover" class="cover" align="center" data-recalc-dims="1"></a></p>
<hr>
<p>This post was originally published on 2019-02-07</p>


</section>

 
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://www.statforbiology.com/posts/general_Correlation_ALookAt.html"> Statforbiology</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/going-back-to-the-basics-the-correlation-coefficient-2/">Going back to the basics: the correlation coefficient</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404214</post-id>	</item>
		<item>
		<title>Validating data analyses written by coding agents</title>
		<link>https://www.r-bloggers.com/2026/10/validating-data-analyses-written-by-coding-agents/</link>
		
		<dc:creator><![CDATA[Seascapemodels]]></dc:creator>
		<pubDate>Thu, 08 Oct 2026 13:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://www.seascapemodels.org/posts/2026-10-09-validating-analyses-by-coding-agents/</guid>

					<description><![CDATA[<p>Claude Code (and many other agents) has been getting so good at ecological analysis that I find I’m increasingly tempted not to directly review the code it writes. In fact, it finds more mistakes in my code than I find in its code these days.<br />
So...</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/validating-data-analyses-written-by-coding-agents/">Validating data analyses written by coding agents</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://www.seascapemodels.org/posts/2026-10-09-validating-analyses-by-coding-agents/"> Seascapemodels</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
 





<p>Claude Code (and many other agents) has been getting so good at ecological analysis that I find I’m increasingly tempted not to directly review the code it writes. In fact, it finds more mistakes in my code than I find in its code these days.</p>
<p>So I need a way to check the validity of the code it writes. Here’s the strategy for this I’ve been developing. Its a work in progress.</p>
<section id="specification" class="level3">
<h3 class="anchored" data-anchor-id="specification">Specification</h3>
<p>Start with a clear written specification for the data analysis or modelling. This should include equations for the modelling. This ensures I understand what I want the analysis to do (even if its not what I get initially).</p>
<p>Claude is very helpful at this stage to review it for logical errors.</p>
</section>
<section id="adversarial-code-review" class="level3">
<h3 class="anchored" data-anchor-id="adversarial-code-review">Adversarial code review</h3>
<p>This is a no brainer. Use the agent, or ideally another provider’s agent, to review the code for errors.</p>
</section>
<section id="data-processing-checks" class="level3">
<h3 class="anchored" data-anchor-id="data-processing-checks">Data processing checks</h3>
<p>Instruct Claude to check all data processing steps and fail loudly if inconsistencies are found. That’s probably sufficient these days, but if you want to be thorough you can think about what you need at each stage and tell it to check for that.</p>
<p>For example, make sure the final dataframe used in the model has N rows, or that the number of unique survey IDs == the number of rows in the dataframe.</p>
<p>There are various R packages to support this like <code>pointblank</code> and <code>validate</code>.</p>
</section>
<section id="testing-against-simulated-data-with-known-truth" class="level3">
<h3 class="anchored" data-anchor-id="testing-against-simulated-data-with-known-truth">Testing against simulated data with known truth</h3>
<p>Often in ecology we are fitting generative models, that is, you can simulate data right out of the model you are fitting. So invert your specification of the model to be a specification that simulates data with a known model. Then run the simulated data through your workflow to see if it recovers the known parameters.</p>
<p>If you are doing probabilistic inference you can also check other common verification statistics like coverage (the 95% CIs should cover the point estimate in 95% of randomized datasets).</p>
<p>Create the data simulation in a separate Claude and R environment, to ensure independence. Make sure the AI memory storage options are turned off, or set to incognito mode, to ensure no leakage of information through memory files too.</p>
<p>There are also many data simulation functions as R packages or within R packages. For instance, many advanced modelling packages also provide a simulation function. You can use these to further reduce the chances of errors in your simulation step</p>
</section>
<section id="other-simulation-tests" class="level3">
<h3 class="anchored" data-anchor-id="other-simulation-tests">Other simulation tests</h3>
<p>You can also do checks where you muddle the real data directly and run it through your workflow. For instance, you should get the same result if you reshuffle rows of your data-frame randomly.</p>
<p>If you reshuffle values in the covariate columns independently of the response, then the effects of covariate should be 0 on average. If your workflow is about model selection, then enforcing shared rank ordering of a covariate and the response you should (almost certainly) find that covariate pops up in every model selected by the AIC.</p>
</section>
<section id="test-multiple-methods" class="level3">
<h3 class="anchored" data-anchor-id="test-multiple-methods">Test multiple methods</h3>
<p>In the simplest case, the same model fitted with different implementations should return the same results. For example compare results from <code>lmer</code> <code>lme</code> and <code>glmmTMB</code>. You can get claude to fire up sub-agents to write a script for each one independently.</p>
<p>In more complex cases it can still be useful to do fit different models and compare results, even in the models have slightly different meanings. This may reveal errors, but it may also reveal places where your model assumptions are not robust. For instance, do different model selection methods result in the same model?</p>
</section>
<section id="whats-next" class="level3">
<h3 class="anchored" data-anchor-id="whats-next">What’s next?</h3>
<p>Part of the problem is that once you start using an agent it will write very complex R code. Claude Code has this obsession with making absolutely generalizable scripts. For instance, it commonly writes functions just to identify the root file path for a key data file.</p>
<p>The other part of its complexity is adding numerous print statements to the R scripts, so it can see intermediate results in the terminal output. I normally do these interactively so they stay out of my scripts.</p>
<p>All this extra code means it becomes even more tedious to review its work. Its like a double edged sword, the agent is powerful, but it makes more review work.</p>
<p>So its clear to me that we soon won’t bother reading the code they write. That means we need to adapt how we approach coding and data analysis, and especially ensure we have checks in place that we understand the code we are producing and understand its correct.</p>
<p>This list is my start at that.</p>
<p>Send a comment if you have other suggestions for ways to test code written by agents.</p>


</section>

 
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://www.seascapemodels.org/posts/2026-10-09-validating-analyses-by-coding-agents/"> Seascapemodels</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/validating-data-analyses-written-by-coding-agents/">Validating data analyses written by coding agents</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404155</post-id>	</item>
		<item>
		<title>The Pit of Success, and What a Data Science Language Would Need to Make It</title>
		<link>https://www.r-bloggers.com/2026/10/the-pit-of-success-and-what-a-data-science-language-would-need-to-make-it/</link>
		
		<dc:creator><![CDATA[Econometrics and Free Software]]></dc:creator>
		<pubDate>Thu, 08 Oct 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://b-rodrigues.github.io/posts/posts/2026-10-08-pit-success.html</guid>

					<description><![CDATA[<p>Rico Mariani, a long-time performance engineer at Microsoft, coined the idea of the pit of success. A system has a pit of success when the natural, lazy way of using it leads to good outcomes. You don’t need heroics or perfect memory. You fall in...</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/the-pit-of-success-and-what-a-data-science-language-would-need-to-make-it/">The Pit of Success, and What a Data Science Language Would Need to Make It</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://b-rodrigues.github.io/posts/posts/2026-10-08-pit-success.html"> Econometrics and Free Software</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
 




<p>Rico Mariani, a long-time performance engineer at Microsoft, coined the idea of the <em>pit of success</em>. A system has a pit of success when the natural, lazy way of using it leads to good outcomes. You don’t need heroics or perfect memory. You fall into the right result, and climbing out to do something wrong takes deliberate effort.</p>
<p>I came across this framing in a talk that I was recently recommended and I further recommend it to anyone interested in the topic: <a href="https://youtu.be/US8QG9I1XW0" rel="nofollow" target="_blank">Functional architecture &#8211; The pits of success &#8211; Mark Seemann</a>. It put a name to something I had been circling around for years.</p>
<p>I had been arguing a version of the same point long before I knew the term. In my 2022 post titled <a href="https://brodrigues.co/posts/2022-11-30-pipelines-as.html" rel="nofollow" target="_blank">Functional programming explains why containerization is needed for reproducibility</a>, I argued that a pipeline is only as reproducible as the functions it is built from, and that even a pure function hides inputs such as the R version, the package versions, and the operating system. Containers are one way to make those hidden inputs explicit. In another 2022 post on <a href="https://brodrigues.co/posts/2022-05-26-safer_programs.html" rel="nofollow" target="_blank">learnings from functional programming</a>, I made a related case for writing functions that fail early and never reach into the global environment. Both posts were, in a sense, attempts to build a pit of success out of habits and tooling. Neither was framed that way, and I think the framing is more useful.</p>
<section id="pits-of-failure-are-everywhere-in-data-science" class="level2">
<h2 class="anchored" data-anchor-id="pits-of-failure-are-everywhere-in-data-science">Pits of failure are everywhere in data science</h2>
<p>Most of the tools we use for analysis are pits of failure. You can be meticulous and still end up with an analysis that cannot be rerun, because the meticulousness is entirely up to you.</p>
<p>Think about how a typical script behaves. It reads a file by path, and nothing says that the path is an input. It uses a package, and nothing records which version. It reassigns a variable halfway through, so the value at the bottom of the file depends on what happened at the top. It converts a column from text to numbers without complaint, and quietly turns the bad entries into missing values. Each of these is convenient in the moment. Together, they make the result depend on things nobody wrote down.</p>
<p>Good practice is then a set of instructions: pin your dependencies, avoid globals, check your inputs, document your steps. These are road signs. They tell you where to go, and they leave the driving to you. Under deadline pressure, road signs get ignored, and not out of malice. Only a speed bump would make you drive more responsibly.</p>
</section>
<section id="what-makes-a-pit-of-success" class="level2">
<h2 class="anchored" data-anchor-id="what-makes-a-pit-of-success">What makes a pit of success</h2>
<p>If the goal is to make the disciplined path the easy one, we need to ask what the language itself would have to do. If reproducibility depends on remembering a checklist, then reproducibility is a convention. If the language makes the checklist unnecessary, reproducibility becomes a property of the program. From my own experience of building and using R packages, and from thinking about where analyses usually go wrong, I think a language designed for this purpose would need at least the points raised in <a href="https://brodrigues.co/posts/2026-04-03-tproject.html" rel="nofollow" target="_blank">this blog post from earlier this year</a>, but let me rephrase them here again.</p>
<p><strong>The program should be a flat text file.</strong> Notebooks have become popular in data science, but I think they are a serious mistake. They mix code, output, and state in a format that is hard to diff, review, and reuse, and they encourage the hidden-state problems that make analyses irreproducible. Notebooks suffer from exactly the same problem that gets blamed on Excel. The main argument for notebooks is that they make exploration easy. But that says more about the shortcomings of the alternatives than about any real strength of notebooks. Environments like RStudio and Positron (or Emacs) already make interactive work with data natural, and they do it while keeping the program in a plain text file that can be run, tested, and versioned like any other code.</p>
<p><strong>Reproducibility should be the default, not an option.</strong> If pinning an environment requires a separate file that someone has to remember to update, it will drift. A language that declares its runtime and dependencies as part of the program, and builds them the same way every time, removes a decision from the analyst. You cannot opt out of it by forgetting.</p>
<p><strong>Inputs should be declared, not discovered.</strong> A program should not be able to read a file or an environment variable that it does not name. When every dependency is visible in the code, the tooling can see the whole picture: which step depends on which, and what changes when an input changes. Undeclared inputs become a build error rather than a silent difference between two machines. This is the same point as the hidden-inputs argument from my containerization post, moved from the operating system into the language.</p>
<p><strong>Hidden state should be hard to create.</strong> Immutable bindings, explicit reassignment when you really need it, and no implicit global state make a result depend only on its inputs. This is also what makes caching safe. If a step is a pure function of its declared inputs, the system can reuse its previous output with confidence, and nobody has to wonder whether a cached result is stale.</p>
<p><strong>Types and schemas should fail loudly.</strong> Silent coercion is one of the most common ways data goes wrong without anyone noticing. A language built for analysis should refuse to turn text into numbers without being asked, should make missing values explicit rather than inventing them, and should check that a column a downstream step needs actually exists before anything runs.</p>
<p><strong>An analysis should be laid out as a pipeline.</strong> Traditional scripts tend to morph into spaghetti very quickly: steps depend on each other in ways that are hard to see, and a change in one place can silently affect results three sections later. Writing the analysis as an explicit pipeline, with each step a named stage that takes declared inputs and produces declared outputs, makes it much easier to debug a failing step, reuse results that have not changed, and hand the work over to someone (or some<em>thing</em>) else.</p>
<p><strong>The whole graph should be checkable before it runs.</strong> If the pipeline is a first-class object that the language can inspect, you can catch missing columns, broken references, and cycles in milliseconds. Waiting three hours for a simulation to discover a typo in a column name is a failure of design, not of the analyst.</p>
<p><strong>Errors should be values.</strong> When a step fails, the rest of the pipeline should not necessarily stop. An error that is captured as data can be inspected, reported, and handled, and independent branches can keep going. This turns debugging from reading a stack trace from code you didn’t write into reading a result that tells you where the problem is.</p>
<p><strong>Interoperability should not require glue code.</strong> Real analysis mixes R, Python, Julia, SQL, and whatever comes next. Every hand-written CSV export between two of them is a place where types get lost and assumptions go unrecorded. A shared, typed interchange format between runtimes removes a whole category of errors and a lot of boilerplate.</p>
<p><strong>Feedback should be structured.</strong> Error messages that are only meant for a human reading a terminal are hard for tools to act on. If the language reports problems in a machine-readable form, the same checks serve analysts, continuous integration, and increasingly, AI coding assistants that write much of the code.</p>
<p>What makes this set of properties attractive is that the same features that make an analysis reproducible also make it easier for an LLM to work with. An LLM is much better at modifying a program when its inputs are explicit, its dependencies are named, its state is visible, and its errors are structured. In that sense, good language design doesn’t just put guardrails around the human analyst. It gives the machine a better representation of what the analyst is trying to do.</p>
<p>This suggests an interesting convergence: the language we would want to make data science safer for humans is also close to the language we would want to make data science easier for AI to operate on. A language built around these principles could be a pit of success for both the human analyst and the AI assistant helping them. Instead of pulling in different directions, the two would be working with the same constraints.</p>
</section>
<section id="defaults-constraints-and-choice-architecture" class="level2">
<h2 class="anchored" data-anchor-id="defaults-constraints-and-choice-architecture">Defaults, constraints, and choice architecture</h2>
<p>There is also a useful way to think about this from economics. We often distinguish between telling people what they ought to do and changing the rules and incentives under which they make decisions. A recommendation such as “pin your dependencies” or “don’t rely on global state” is a rule in the first sense: it asks the analyst to remember and follow a good practice. Defaults, constraints, and institutions work differently. They change what is easy to do, what is costly to do, and what is possible to do at all.</p>
<p>This is closely related to the idea of choice architecture in behavioural economics, and to the broader observation that the design of an environment can shape behaviour without requiring people to consciously follow a set of instructions. In software, the architecture is unusually literal: a language can make some actions easy, others difficult, and some impossible to express without making the programmer explicitly acknowledge what they are doing.</p>
<p>That suggests a useful question for data science. Instead of asking how we can get analysts to consistently follow reproducibility best practices, what if we designed the language so that following those practices was the easiest way to write a program?</p>
</section>
<section id="design-tools-that-make-falling-into-a-pit-of-success-easy" class="level2">
<h2 class="anchored" data-anchor-id="design-tools-that-make-falling-into-a-pit-of-success-easy">Design tools that make falling into a pit of success easy</h2>
<p>But have you tried asking someone who does none of this to adopt these practices? People may agree with every point, but applying them consistently takes effort and frankly brings very little immediate reward. Investing early in good practice makes life much easier later, but “later” is hard to sell to someone with a deadline yesterday.</p>
<p>So what is the alternative? Rather than persuading people to jump into the pit, we can build tools that make falling into it the default. I’ve been working on a domain-specific language called T that aims at exactly this. The idea is that by simply using it, you end up in a pit of success. I won’t go into the details here, since you can read more about it <a href="https://brodrigues.co/posts/2026-04-03-tproject.html" rel="nofollow" target="_blank">here</a> and <a href="https://brodrigues.co/posts/2026-08-02-t_devlog.html" rel="nofollow" target="_blank">here</a>. What I want to discuss is why LLMs make this kind of tool practical to build.</p>
<p>Historically, building a language or a tool was a large project that only a few people could justify. LLMs lower that cost dramatically. Someone with experience in a domain can now build a small tool that solves one specific problem well, without a team or a long development cycle. <em>Taste</em>, knowing which problems matter and which habits are worth enforcing, is exactly what a domain expert brings and what an LLM does not supply on its own.</p>
<p>I don’t know yet how widely T will be adopted. But I think it is a good example of the pit-of-success idea applied to data science, and the broader point is that the tools themselves can carry the discipline, so that individual analysts don’t have to. The future will tell whether that bet pays off.</p>
</section>
<section id="where-the-pit-needs-walls-and-an-exit" class="level2">
<h2 class="anchored" data-anchor-id="where-the-pit-needs-walls-and-an-exit">Where the pit needs walls and an exit</h2>
<p>A pit of success can become a trap if it is the only way to work. Exploration is messy by nature. You try a model, plot something odd, throw away three approaches before finding the one that works. A language that forbids all of that makes people go back to their old tools, usually a notebook on the side, and then the discipline lives in two places at once.</p>
<p>So the language needs a deliberate place for exploration that does not contaminate production. Exploration should be easy and interactive, with the results promoted into the strict pipeline once they stabilise. The pit should catch you when you go into production, not when you’re trying to think. This is the part I find hardest to get right, and I suspect any language that ignores it will be abandoned quickly. For now, the way this works in T is that you have much more freedom to try things from its REPL. But I suspect that much of this exploratory work will be LLM-driven in the future. Interactive data exploration may move from “playing around” with the data to asking an LLM to do things for you and show you the results.</p>
<p>If that happens, interactive exploration may become less important as a feature of the language itself. The important requirement would instead be that the language provides a good interface for an LLM to explore the data and then turn what it discovers into a reproducible pipeline.</p>
<p>I think much of the answer lies in the tools we already use. Editors such as Emacs and Positron or VS Code already give analysts a place to explore interactively while writing plain text files that can later be run as a pipeline. What a new language needs is an editor integration that makes this transition smooth: completions, diagnostics, and navigation working in the same window as the exploratory code. That integration is a solvable engineering task, and LLMs are now quite capable of producing the plugins, language servers, or tree-sitter grammars it requires. One question is whether the language makes the move from exploration to production feel like tidying up rather than rewriting from scratch, which is, again, the typical life-cycle of an analysis. Quick and dirty exploration in a Notebook, clean rewrite in a script (sometimes even in another language). T solves this by forcing the analysis to be a pipeline that runs in a reproducible environment <em>by design</em>.</p>
</section>
<section id="what-the-pit-cannot-do" class="level2">
<h2 class="anchored" data-anchor-id="what-the-pit-cannot-do">What the pit cannot do</h2>
<p>A language, even one supported by an LLM, can make reproducibility cheap. It cannot make the analysis correct. A misspecified regression run reproducibly is still misspecified, and catching that remains the work of a statistician and a reviewer. Nor can a language change the culture around it. People need to believe that rebuilding last year’s figures is worth the time, and someone needs to ask whether they still can.</p>
<p>The pit of success makes the right thing easy. It does not decide what the right thing is, and it does not make anyone want to jump in. Those are still human jobs.</p>
<p>This is roughly the bet behind T, though I think the general ideas matter more than any one implementation. If you have thought about what a data science language should refuse to let you do, I would be interested to hear what you would put on that list.</p>


</section>

 
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://b-rodrigues.github.io/posts/posts/2026-10-08-pit-success.html"> Econometrics and Free Software</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/the-pit-of-success-and-what-a-data-science-language-would-need-to-make-it/">The Pit of Success, and What a Data Science Language Would Need to Make It</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404127</post-id>	</item>
		<item>
		<title>Bayesian workflow [book review]</title>
		<link>https://www.r-bloggers.com/2026/10/bayesian-workflow-book-review/</link>
		
		<dc:creator><![CDATA[xi'an]]></dc:creator>
		<pubDate>Wed, 07 Oct 2026 22:26:44 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://xianblog.fr/?p=62923</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; "> “This original, thought-provoking, and transforming, book is much much more than an implementation manual for Bayesian Data Analysis, even though it shares almost the same perspective. (The first sentence of the book states that the authors’ `conceptions of statistical practice, and of Bayesian statistics, have changed over the years’.) By ...</div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/bayesian-workflow-book-review/">Bayesian workflow [book review]</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://xianblog.fr/2026/10/08/bayesian-workflow-book-review/"> R – Xi&#039;an&#039;s Og</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
<blockquote>
<p style="text-align: right;"><a href="http://https//avehtari.github.io/Bayesian-Workflow/" rel="nofollow" target="_blank"><img loading="lazy" data-recalc-dims="1" fetchpriority="high" decoding="async" data-attachment-id="62926" data-permalink="https://xianblog.fr/2026/10/08/bayesian-workflow-book-review/screenshot-2026-05-26-at-16-35-22-bayesian-workflow-book-website-bayesian-workflow-book/" data-orig-file="https://i0.wp.com/xianblog.fr/wp-content/uploads/2026/05/screenshot-2026-05-26-at-16-35-22-bayesian-workflow-book-website-e28093-bayesian-workflow-book.png?fit=1585%2C646&#038;ssl=1" data-orig-size="1585,646" data-comments-opened="1" data-image-title="Screenshot 2026-05-26 at 16-35-22 Bayesian Workflow book Website – Bayesian Workflow book" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/xianblog.fr/wp-content/uploads/2026/05/screenshot-2026-05-26-at-16-35-22-bayesian-workflow-book-website-e28093-bayesian-workflow-book.png?fit=450%2C183&#038;ssl=1" class="aligncenter wp-image-62926" src="https://i0.wp.com/xianblog.fr/wp-content/uploads/2026/05/screenshot-2026-05-26-at-16-35-22-bayesian-workflow-book-website-e28093-bayesian-workflow-book.png?resize=450%2C183&#038;ssl=1" alt="" width="450" height="183" /></a><br />
<em><span style="color: #ff6600;">“This original, thought-provoking, and transforming, book is much much more than an implementation manual for Bayesian Data Analysis, even though it shares almost the same perspective. (The first sentence of the book states that the authors’ `conceptions of statistical practice, and of Bayesian statistics, have changed over the years’.) By providing a modus vivendi for undertaking Bayesian modelling from scratch in realistic settings where models are not magicked out of the blue, the authors explicit and rationalise the many steps required by such a bottom-up modelling protocol (`not a checklist, not a cookbook’, and not a flowchart!) in real situations. The contents read very well and very smoothly, with a seamless conjunction of intuition, modelling advices, computational details, and comparison tools. While unsurprisingly Bayesian, the perspective adopted therein remains both open and inclusive, with a welcome humility about the limitations and challenges of Bayesian workflows. This book should thus appeal to and profit a wide variety of readers, as providing guidance through an extensive collection of highly detailed examples, with shared code and exercises.”</span></em></p>
</blockquote>
<p dir="ltr" style="text-align: justify;"><strong>T</strong>his <a href="https://amzn.to/4iMYnF3" rel="nofollow" target="_blank">book</a> proposes a <em>modus vivendi</em> for Bayesian modelling in applied, realistic Bayesian analysis, where models are not magicked out of the blue. It thus emphases iterative model building, model checking, computational troubleshooting, and simulated-data experimentation, filling a gap that looks glaring in retrospect. It particularly targets users and developers of Stan, with code excerpts in R and Stan. It consists of four parts:</p>
<ol dir="ltr">
<li>background on Bayesian methods and computational tools;</li>
<li>the Bayesian workflow proper, namely building a statistical model from its components, together with its assessment tools;</li>
<li>the computational aspects of fitting models, diagnosing convergence and assessing calibration;</li>
<li>case studies.</li>
</ol>
<p dir="ltr" style="text-align: justify;">I was eagerly waiting for the <a href="https://amzn.to/4iMYnF3" rel="nofollow" target="_blank">book</a>, as I knew Andrew, Aki, and Richard had been working on it for a few years. (The quote above is the blurb I wrote upon request from the publisher.)</p>
<p dir="ltr" style="text-align: justify;">The tenets of <a href="https://amzn.to/4iMYnF3" rel="nofollow" target="_blank">BaWoFlo</a>—if I may resort to this acronym!—are (i) fitting multiple models, (ii) applying methods repeatedly, and (iii) resorting to simulated-data experiments, which should not come as a surprise to readers of <a href="https://amzn.to/4hfACnV" rel="nofollow" target="_blank">BDA</a>. As noted in the introduction, the protocol exposed therein can also benefit non-Bayesian experimenters. This agrees with the highly moderate, “M-open”, agnostic approach to Bayesianism adopted by the authors (“there is no safe haven”). I also welcome and share their humble perspective about the limitations and challenges of Bayesian workflows.</p>
<p dir="ltr" style="text-align: justify;">Examples are treated in full detail, with successive modelling and computational choices profusely commented, which is a big plus for such a practical book. This starts as early as Chapter 4, with a multiple-choice exam example. Indeed, there cannot be general principles or a generic theory that would make the approach foolproof. See, e.g., “<em>A data model is not just a ‘likelihood’</em>” (p.70), as when the data model is not fully generative. I very much liked the section on choosing priors (5.6), and the very rich graphs (see, e.g., Chapter 8) for assessing the impact of prior and likelihood, as well as for predictive checks. In coherent continuation of the authors’ earlier work, the <a href="https://amzn.to/4iMYnF3" rel="nofollow" target="_blank">book</a> advocates LOO methods and model stacking rather than model averaging. (With a surprisingly anti-Ockham perspective in Section 9.7.)</p>
<p dir="ltr" style="text-align: justify;">The MCMC coverage is unsurprising, with \(\hat R\) at the forefront. Chapter 12, on using fast experiments to detect fitting or computational issues, is very nice. The <a href="https://amzn.to/4iMYnF3" rel="nofollow" target="_blank">book</a> builds on the immense corpus of work achieved by the authors over the decades (for the most senior ones!). By contrast, the chapter on approximate solutions (13) is way too short, and the same goes for those on calibration and software development.</p>
<p dir="ltr" style="text-align: justify;">The <a href="https://amzn.to/4iMYnF3" rel="nofollow" target="_blank">book</a> is very US-centric, unsurprisingly given Andrew’s focus on political science. Some sections are reminiscent of Andrew’s blog entries (or the opposite). The (football) World Cup example was initiated when Andrew was in France, during the 2014 World Cup, and as a result (?) the names of the teams are in French! One chapter also reanalyses the birthdate data displayed on the cover of <a href="https://amzn.to/4hfACnV" rel="nofollow" target="_blank">BDA</a>.</p>
<p dir="ltr" style="text-align: justify;">Mileage varies on the applied chapters, depending on the example. A dog chapter is followed by a cat chapter! Not that the (stat)dog experiment was in any way enjoyable, especially for the dogs. Maybe the cats were running it! And then come chapters on roaches and sharks. There is also a frightening flowchart (Fig. 2.1)! And the <a href="https://amzn.to/4iMYnF3" rel="nofollow" target="_blank">book</a> ends with an appendix on going through <a href="https://amzn.to/4hfACnV" rel="nofollow" target="_blank">BDA</a> to better understand <a href="https://amzn.to/4iMYnF3" rel="nofollow" target="_blank">BaWoFlo</a></p>
<p dir="ltr"><em class="zhvwWe"><span class="yADgie">[The usual disclaimer applies, namely that this review is likely to appear later in CHANCE, in my book reviews column.</span></em><em class="zhvwWe"><span class="yADgie">]</span></em><span class="WBgIic Wg1cdb notranslate"><span class="NMq1me"><span aria-hidden="true"> </span></span></span></p>

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://xianblog.fr/2026/10/08/bayesian-workflow-book-review/"> R – Xi&#039;an&#039;s Og</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/bayesian-workflow-book-review/">Bayesian workflow [book review]</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404119</post-id>	</item>
		<item>
		<title>A Collaboration Fest for research software interoperability</title>
		<link>https://www.r-bloggers.com/2026/10/a-collaboration-fest-for-research-software-interoperability/</link>
		
		<dc:creator><![CDATA[Kevin Rue-Albrecht]]></dc:creator>
		<pubDate>Wed, 07 Oct 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://blog.bioconductor.org/posts/2026-10-07-biofair-cofest/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>On 15-17 September 2026, members of the Bioconductor, Galaxy and nf-core communities came together at The Open University in Milton Keynes for the BioFAIR Data to Discovery and BioFAIR Collaboration Fest (CoFest) joint event.<br />
The three-day event...</p></div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/a-collaboration-fest-for-research-software-interoperability/">A Collaboration Fest for research software interoperability</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://blog.bioconductor.org/posts/2026-10-07-biofair-cofest/"> Bioconductor community blog</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
 





<p>On 15-17 September 2026, members of the <a href="https://www.bioconductor.org/" rel="nofollow" target="_blank">Bioconductor</a>, <a href="https://galaxyproject.org/" rel="nofollow" target="_blank">Galaxy</a> and <a href="https://nf-co.re/" rel="nofollow" target="_blank">nf-core</a> communities came together at <a href="https://www.open.ac.uk/" rel="nofollow" target="_blank">The Open University</a> in Milton Keynes for the <a href="https://training.galaxyproject.org/training-material/events/2026-07-06-biofair.html" rel="nofollow" target="_blank">BioFAIR Data to Discovery</a> and <a href="https://github.com/BiocCodingCollaborations/BioFAIR2026_Sprint" rel="nofollow" target="_blank">BioFAIR Collaboration Fest (CoFest)</a> joint event.</p>
<p>The three-day event combined hands-on training with collaborative work on research software interoperability. It was jointly organised by <a href="https://biofair.uk/fellows/" rel="nofollow" target="_blank">BioFAIR Fellow</a> <a href="https://orcid.org/0000-0001-6979-6930" rel="nofollow" target="_blank">Marisa Loach</a> and <a href="https://www.software.ac.uk/news/ssi-fellow-awarded-biofair-pathfinder-project" rel="nofollow" target="_blank">BioFAIR Pathfinder</a> project lead and <a href="https://www.software.ac.uk/fellowship-programme/kevin-rue-albrecht" rel="nofollow" target="_blank">Institute for Research Software 2026 Fellow</a> <a href="https://orcid.org/0000-0003-3899-3872" rel="nofollow" target="_blank">Kevin Rue-Albrecht</a>.</p>
<p>The CoFest brought together contributors based in the UK, Ireland, Germany, India and the USA, with both in-person and remote participation.</p>
<p>Marisa led the <strong>Data to Discovery</strong> training, introducing participants to single-cell analysis using the European instance of the Galaxy platform (<a href="https://usegalaxy.eu/" class="uri" rel="nofollow" target="_blank">https://usegalaxy.eu/</a>), while Kevin led the hybrid <strong>CoFest</strong> focused on making methods developed as Bioconductor R packages more readily accessible through infrastructures such as Galaxy and nf-core.</p>
<p><em>A <a href="https://galaxyproject.org/news/2026-09-24-biofair-cofest/" rel="nofollow" target="_blank">Galaxy community recap</a> of the event also covers the training and CoFest from the Galaxy perspective.</em></p>
<section id="the-challenge-making-methods-reusable-across-ecosystems" class="level2">
<h2 class="anchored" data-anchor-id="the-challenge-making-methods-reusable-across-ecosystems">The challenge: making methods reusable across ecosystems</h2>
<p>There are numerous communities in the bioinformatics space focusing on different aspects like code development, making non-code GUIs, creating tutorials and documentation, and more. Some of these are Bioconductor, Galaxy and nf-core, which play complementary roles in bioinformatics. <strong>Bioconductor</strong> provides a large ecosystem of R packages implementing analysis methods; <strong>Galaxy</strong> provides an accessible environment for running and sharing reproducible analyses; and <strong>nf-core</strong> provides a community framework for portable <a href="https://www.nextflow.io/" rel="nofollow" target="_blank">Nextflow</a> pipelines.</p>
<p>The challenge is that making the same method available across these environments often requires separate wrappers, metadata and maintenance. The <strong>BioFAIR Pathfinder</strong> project behind the event is exploring ways to reduce that duplication, including tooling for adapting Bioconductor methods as Galaxy tools and nf-core modules.</p>
<p>A major focus of the CoFest was <a href="https://blog.bioconductor.org/posts/2026-08-21-biocjobs/" rel="nofollow" target="_blank">BiocJobs</a>, an approach being developed that allows analysis tasks to be described alongside Bioconductor packages and used to generate representations for different workflow systems.</p>
</section>
<section id="putting-the-idea-into-practice" class="level2">
<h2 class="anchored" data-anchor-id="putting-the-idea-into-practice">Putting the idea into practice</h2>
<p>Rather than only <em>discussing</em> interoperability, participants <em>applied</em> this approach to an <a href="https://kevinrue.github.io/biofair-pathfinder-2026/tutorial-singlecell-bioconductor/" rel="nofollow" target="_blank">exemplar single-cell analysis workflow</a>.</p>
<p>The group mapped parts of that workflow to reusable package-level ‘BiocJobs’ and tested the path from descriptions maintained alongside Bioconductor packages to generated workflow components. Work covered steps including data import, quality control, normalisation, dimensionality reduction and marker analysis.</p>
<p>This gave contributors a concrete way to test the process, improve documentation, and identify what would be needed to make it easier for others to contribute.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://i0.wp.com/blog.bioconductor.org/posts/2026-10-07-biofair-cofest/participants.jpg?w=578&#038;ssl=1" class="img-fluid quarto-figure quarto-figure-center figure-img" alt="Photo of participants working during the CoFest." data-recalc-dims="1"></p>
</figure>
</div>
<p>On the final day, we captured the workflow we had been working towards in a single diagram: from a commit to a Bioconductor package repository, through the automatic building and publishing of tool wrappers, to their use in Galaxy, nf-core and other workflow environments.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://i0.wp.com/blog.bioconductor.org/posts/2026-10-07-biofair-cofest/biocjobs-workflow.jpg?w=578&#038;ssl=1" class="img-fluid quarto-figure quarto-figure-center figure-img" alt="Workflow diagram illustrating the contribution process and automated building and dissemination of Bioconductor tool wrappers across other software ecosystems." data-recalc-dims="1"></p>
</figure>
</div>
<p>Three days were not enough to fully implement the workflow captured in the diagram &#8211; nor was that the goal. Instead, the CoFest demonstrated the process on a small number of selected workflow steps, while documenting how others could contribute. This created a foundation for onboarding new contributors and, over time, enabling more contributors to create wrappers for other widely used Bioconductor methods.</p>
</section>
<section id="what-we-learned" class="level2">
<h2 class="anchored" data-anchor-id="what-we-learned">What we learned</h2>
<p>One of the clearest lessons was that interoperability is not just about writing software that connects one platform to another.</p>
<p>It also depends on deciding where information should live, who is best placed to maintain it, and how contributions can fit naturally into the practices of each community. Bringing Bioconductor package developers, Galaxy contributors, and nf-core developers into the same room made it possible to look at the process end to end rather than from the perspective of a single ecosystem.</p>
<p>The event also reinforced the value of working from a concrete scientific use case. Using a real single-cell workflow gave participants something shared to build, test and discuss, while exposing questions that would have been difficult to anticipate from design discussions alone. The <a href="https://docs.google.com/document/d/1eKNACIPJf6vLIzBrE-8yrtCrYcmiXGMzYhBouuoVxR0/edit?usp=sharing" rel="nofollow" target="_blank">CoFest notes</a> capture several of those emerging questions, particularly around how reusable jobs should be structured and composed.</p>
</section>
<section id="what-comes-next" class="level2">
<h2 class="anchored" data-anchor-id="what-comes-next">What comes next</h2>
<p>The CoFest was intended as a starting point rather than a one-off sprint.</p>
<p>The immediate next step is to continue refining the workflow demonstrated during the event: improving the tooling and documentation, testing it on additional Bioconductor methods, and making it easier for new contributors to take part.</p>
<p>We are also planning follow-up events in the first quarter of 2027. These will provide an opportunity to bring the communities together again, build on the examples developed in Milton Keynes and involve more contributors in extending the approach to other widely used methods.</p>
<p>The longer-term aim is simple: make it easier for software developed in one research community to become useful, sustainable building blocks in another.</p>
<p>Interested in getting involved? <a href="https://community-bioc.zulipchat.com/join/zsfbiqh64t3nq5ldrmv72b4w/" rel="nofollow" target="_blank">Join the Bioconductor Zulip and the #biofair2026-workflow-sprint channel</a> and follow the BiocJobs topic for ongoing discussion.</p>


</section>

<p>
© 2026 Bioconductor. Content is published under <a href="https://creativecommons.org/licenses/by/4.0/" rel="nofollow" target="_blank">Creative Commons CC-BY-4.0 License</a> for the text and <a href="https://opensource.org/licenses/BSD-3-Clause" rel="nofollow" target="_blank">BSD 3-Clause License</a> for any code. | <a href="https://www.r-bloggers.com/" rel="nofollow" target="_blank">R-Bloggers</a>
</p> 
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://blog.bioconductor.org/posts/2026-10-07-biofair-cofest/"> Bioconductor community blog</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/a-collaboration-fest-for-research-software-interoperability/">A Collaboration Fest for research software interoperability</a>]]></content:encoded>
					
		
		<enclosure url="https://blog.bioconductor.org/posts/2026-10-07-biofair-cofest/biocjobs-workflow.jpg" length="0" type="image/jpeg" />

		<post-id xmlns="com-wordpress:feed-additions:1">404099</post-id>	</item>
		<item>
		<title>Connecting to Eureka2 using a university&#8217;s Microsoft Authenticator based login</title>
		<link>https://www.r-bloggers.com/2026/10/connecting-to-eureka2-using-a-universitys-microsoft-authenticator-based-login/</link>
		
		<dc:creator><![CDATA[https://pacha.dev/blog]]></dc:creator>
		<pubDate>Tue, 06 Oct 2026 23:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://pacha.dev/blog/2026/10/07/index.html</guid>

					<description><![CDATA[<p>Connecting to the computing cluster using a VPN tunnel</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/connecting-to-eureka2-using-a-universitys-microsoft-authenticator-based-login/">Connecting to Eureka2 using a university’s Microsoft Authenticator based login</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://pacha.dev/blog/2026/10/07/index.html"> https://pacha.dev/blog</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
<p><em>Before the main content:</em> * <em>With all the funding cuts to PhD studies, I’d appreciate if you can donate <a href="https://buymeacoffee.com/pacha" rel="nofollow" target="_blank">here</a>.</em></p>
<p>I am using <a href="https://omarchy.org/" rel="nofollow" target="_blank">Omarchy</a>, a Linux distribution based on Arch for those that want efficiency, and it is similar to Manjaro minus the desktop effects that use considerable resources.</p>
<p>At Surrey and probably other universities in the UK, we require to use Microsoft Authenticator to log to the VPN.</p>
<p>Today I had to connect to <a href="https://docs.pages.surrey.ac.uk/research_computing/hpc/clusters/eureka2.html" rel="nofollow" target="_blank">Eureka2</a>, a powerful computing cluster, using my personal laptop. The official guide <a href="https://www.surrey.ac.uk/sites/default/files/2022-06/globalprotect-instructions-for-personal-device.pdf" rel="nofollow" target="_blank">Working off campus with GlobalProtect</a> instructs to download Global Protect from the Surrey App Store, a service that has been retired.</p>
<p>I installed <a href="https://en.wikipedia.org/wiki/OpenConnect" rel="nofollow" target="_blank">Open Connect</a> which is a FOSS alternative:</p>
<pre>sudo yay -S openconnect</pre>
<p>I had to adjust my email settings <a href="https://mysignins.microsoft.com/security-info" rel="nofollow" target="_blank">here</a>, and verify that the default sign-in method is ‘Microsoft Authenticator – notification’.</p>
<p>Then I was able to connect using:</p>
<pre>sudo openconnect --protocol=gp --usergroup=gateway vpn.surrey.ac.uk</pre>
<p>After a few tries, what works it to use the full username (not email name) with ‘@surrey.ac.uk’. What I mean is that my university account is something like <em>ab12345</em> when my email <em>firstname.lastname</em>, and so I used <em>ab12345@surrey.ac.uk</em>. For the password I had to use the password that I use for Surrey Learn (not the email password) followed by the authenticator timed PIN number.</p>
<p>I was finally able to see:</p>
<pre>ESP session established with server
ESP tunnel connected; exiting HTTPS mainloop.</pre>
<p>And finally I could type this in another terminal:</p>
<pre>ssh mv00622@eureka2.surrey.ac.uk</pre>
<p>Which shows:</p>
<pre>    +++++++++++++++++++++++++++++++++++++++++
      Welcome to Surrey's Eureka2 HPC Cluster
             Based on Rocky 8
    +++++++++++++++++++++++++++++++++++++++++

Documentation for HPC users is at below site:
https://docs.pages.surrey.ac.uk/research_computing/</pre>
<p>I hope this helps. I had to spend over half an hour figuring out the steps by reading different sources and I hope this helps other people.</p>
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://pacha.dev/blog/2026/10/07/index.html"> https://pacha.dev/blog</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/connecting-to-eureka2-using-a-universitys-microsoft-authenticator-based-login/">Connecting to Eureka2 using a university’s Microsoft Authenticator based login</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404105</post-id>	</item>
		<item>
		<title>{renv}, {rix}, rv, uvr, ir: An explosion of R package and environment managers</title>
		<link>https://www.r-bloggers.com/2026/10/renv-rix-rv-uvr-ir-an-explosion-of-r-package-and-environment-managers/</link>
		
		<dc:creator><![CDATA[Isabella Velásquez]]></dc:creator>
		<pubDate>Tue, 06 Oct 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://rworks.dev/posts/r-managers/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>There has been an explosion of package/environment/platform managers in the R ecosystem recently,1 and R users are really reaping the rewards of so many ripe and riveting tools. But, I’ve found it rough2 to keep track of them all and understand ...</p></div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/renv-rix-rv-uvr-ir-an-explosion-of-r-package-and-environment-managers/">{renv}, {rix}, rv, uvr, ir: An explosion of R package and environment managers</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://rworks.dev/posts/r-managers/"> R Works</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
 





<p>There has been an explosion of package/environment/platform managers in the R ecosystem recently,<sup>1</sup> and R users are really reaping the rewards of so many ripe and riveting tools. But, I’ve found it rough<sup>2</sup> to keep track of them all and understand what they are and how they are different!</p>
<p>I suspect I am not the only one. My colleague Libby Heeren and I recently held a <a href="https://pos.it/dslab" rel="nofollow" target="_blank">Data Science Lab</a> with Eric Nantz to showcase {rix} and Nix (which was awesome! Keep an eye out on the <a href="https://www.youtube.com/playlist?list=PL9HYL-VRX0oSeWeMEGQt0id7adYQXebhT" rel="nofollow" target="_blank">Posit YouTube channel</a> for when the recording is published), and the first three or so questions from the audience were all about how {rix} compares to X ({renv}, rv, uvr), and so I thought it’d be helpful to write a short summary of five tools that I’ve heard about: <a href="https://rstudio.github.io/renv/" rel="nofollow" target="_blank">{renv}</a>, <a href="https://docs.ropensci.org/rix/" rel="nofollow" target="_blank">{rix}</a>, <a href="https://a2-ai.github.io/rv-docs/" rel="nofollow" target="_blank">rv</a>, <a href="https://nbafrank.github.io/uvr/" rel="nofollow" target="_blank">uvr</a>, and <a href="https://r-lib.github.io/ir/" rel="nofollow" target="_blank">ir</a>.<sup>3</sup></p>
<p>While they’re all about tracking or isolating dependencies, they operate at a different “levels” from each other. <a href="https://en.wikipedia.org/wiki/TL;DR" rel="nofollow" target="_blank">TL;DR</a>:</p>
<ul>
<li>{rix} snapshots the entire system environment</li>
<li>{renv}, rv, and uvr manage R package dependencies within a single R project</li>
<li>ir handles dependencies for a single R script rather than a project</li>
</ul>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="https://i0.wp.com/rworks.dev/posts/r-managers/images/managers.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-2" title="Graphic design is my passion." rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/r-managers/images/managers.png?w=578&#038;ssl=1" class="img-fluid quarto-figure quarto-figure-center figure-img" alt="Graphic design is my passion." data-recalc-dims="1"></a></p>
</figure>
</div>
<figcaption>Graphic design is my passion.</figcaption>
</figure>
</div>
<p>I don’t want to/can’t pass judgement on which is the best or which I recommend, because I honestly haven’t had the opportunity to explore all of them fully (a very diplomatic way of saying that I haven’t yet used all of them in a real-world context), but I would love to hear everybody else’s opinions on which they think is the best tool because I imagine people have them! <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f600.png" alt="😀" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p>It’s an unsatisfactory proxy for quality/experience, but here is the GitHub star trajectory of each:</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://api.star-history.com/chart?repos=rstudio/renv%2Cropensci/rix%2Ca2-ai/rv%2Cnbafrank/uvr%2Cr-lib/ir&#038;type=date&#038;legend=top-left.png" class="img-fluid figure-img"></p>
<figcaption>Star History Chart</figcaption>
</figure>
</div>
<p>(Mind the <a href="https://en.wikipedia.org/wiki/Recency_bias" rel="nofollow" target="_blank">recency bias</a>).</p>
<p>With that, let’s begin, going chronologically by release!</p>
<section id="renv" class="level2">
<h2 class="anchored" data-anchor-id="renv"><strong>{renv}</strong></h2>
<p><a href="https://github.com/rstudio/renv" rel="nofollow" target="_blank">Repo</a> | <a href="https://rstudio.github.io/renv/" rel="nofollow" target="_blank">Docs</a> | <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" />1.2k GitHub stars | <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Packages <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> R version <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Environments</p>
<section id="use-case" class="level3">
<h3 class="anchored" data-anchor-id="use-case">Use case</h3>
<p>This is a long-established package supported by Posit (creators of RStudio) used for <strong>project-local R package management</strong>.</p>
</section>
<section id="how-to-use-renv" class="level3">
<h3 class="anchored" data-anchor-id="how-to-use-renv">How to use {renv}</h3>
<p>{renv} is iterative and <em>retrospective</em>: you install the packages first, and then take a snapshot.</p>
<ol type="1">
<li>Install the {renv} package:</li>
</ol>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre>Terminal</pre>
</div>
<pre>install.packages(&quot;renv&quot;)</pre>
</div>
<ol start="2" type="1">
<li>Within a project, initialize {renv}:</li>
</ol>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre>Console</pre>
</div>
<pre>renv::init()</pre>
</div>
<ol start="3" type="1">
<li>Install packages:</li>
</ol>
<p>Install packages the regular way, i.e., <code>install.packages()</code>.</p>
<p>When you install packages, they will be installed into a project-specific library.</p>
<ol start="4" type="1">
<li>Snapshot your library’s packages into a lock file:</li>
</ol>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre>Console</pre>
</div>
<pre>renv::snapshot()</pre>
</div>
<p>This lockfile captures the exact versions of those packages:</p>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre>renv.lock</pre>
</div>
<pre>{
  &quot;R&quot;: {
    &quot;Version&quot;: &quot;4.4.2&quot;,
    &quot;Repositories&quot;: [
      {
        &quot;Name&quot;: &quot;CRAN&quot;,
        &quot;URL&quot;: &quot;https://cloud.r-project.org&quot;
      }
    ]
  },
  &quot;Packages&quot;: {
    &quot;dplyr&quot;: {
      &quot;Package&quot;: &quot;dplyr&quot;,
      &quot;Version&quot;: &quot;1.1.4&quot;,
      &quot;Source&quot;: &quot;Repository&quot;,
      &quot;Repository&quot;: &quot;CRAN&quot;,
      &quot;Hash&quot;: &quot;fedd9d00c2944ff00a0e2696ccf048ec&quot;
    },
    &quot;ggplot2&quot;: {
      &quot;Package&quot;: &quot;ggplot2&quot;,
      &quot;Version&quot;: &quot;3.5.1&quot;,
      &quot;Source&quot;: &quot;Repository&quot;,
      &quot;Repository&quot;: &quot;CRAN&quot;,
      &quot;Hash&quot;: &quot;44c6a2f8202d5b7e6c72a95ca7ea1795&quot;
    }
  }
}</pre>
</div>
<ol start="5" type="1">
<li>Iterate:</li>
</ol>
<p>As you continue adding packages, continue snapshotting to update the lock file.</p>
</section>
<section id="how-to-share-using-renv" class="level3">
<h3 class="anchored" data-anchor-id="how-to-share-using-renv">How to share using {renv}</h3>
<p>When someone wants to run your project using the packages stored in its {renv} lockfile, they can restore the library on their machine by running <code>renv::restore()</code>.</p>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre>Console</pre>
</div>
<pre>renv::restore()</pre>
</div>
</section>
<section id="some-considerations" class="level3">
<h3 class="anchored" data-anchor-id="some-considerations">Some considerations</h3>
<p>Because {renv} is iterative and retrospective, you can accidentally install packages that conflict with the packages that you had installed earlier.</p>
<p>While {renv} tracks your R version, it intentionally does not manage it. If you need to switch R versions, you’ll want a tool like <a href="https://rig.r-lib.org/" rel="nofollow" target="_blank">rig</a>. See the <a href="https://rstudio.github.io/renv/articles/renv.html#caveats" rel="nofollow" target="_blank">renv documentation</a> for the full list of caveats.</p>
</section>
</section>
<section id="rix" class="level2">
<h2 class="anchored" data-anchor-id="rix"><strong>{rix}</strong></h2>
<p><a href="https://github.com/ropensci/rix" rel="nofollow" target="_blank">Repo</a> | <a href="https://docs.ropensci.org/rix/" rel="nofollow" target="_blank">Docs</a> | <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" />381 GitHub stars | <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Packages <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> R version <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Environments</p>
<section id="use-case-1" class="level3">
<h3 class="anchored" data-anchor-id="use-case-1">Use case</h3>
<p>{rix} operates one level up from the other tools on this list. Instead of managing packages within an R installation, {rix} uses <a href="https://nixos.org/" rel="nofollow" target="_blank">Nix</a>, a general-purpose package manager, to snapshot <strong>an entire software environment</strong>: R itself, packages, <em>and</em> any system-level dependencies they need.</p>
</section>
<section id="how-to-use-rix" class="level3">
<h3 class="anchored" data-anchor-id="how-to-use-rix">How to use {rix}</h3>
<p>{rix} is retrospective but in a different sense than {renv}. You declare the environment you want up front, and Nix builds it from scratch (pulling from a package index rather than your currently-installed library).</p>
<ol type="1">
<li>Install Nix and the {rix} package:</li>
</ol>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre>Terminal</pre>
</div>
<pre>install.packages(&quot;rix&quot;)</pre>
</div>
<ol start="2" type="1">
<li>Declare your environment:</li>
</ol>
<p>Write a small script (e.g., <code>generate_env.R</code>) describing the R version and packages you need:</p>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre>generate_env.R</pre>
</div>
<pre>library(rix)

rix(
  r_ver = &quot;4.4.2&quot;,
  r_pkgs = c(&quot;dplyr&quot;, &quot;ggplot2&quot;),
  project_path = &quot;.&quot;
)</pre>
</div>
<p>Running this script generates a <code>default.nix</code> file, a plain-text description of the environment.</p>
<ol start="3" type="1">
<li>Build the environment:</li>
</ol>
<p>Running <code>nix-build</code> creates the environment stored in <code>/nix/store</code>:</p>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre>Terminal</pre>
</div>
<pre>nix-build</pre>
</div>
<p>Then, you can drop into this environment interactively by running <code>nix-shell</code>.</p>
<p>From there, you can work normally by launching R, writing scripts, rendering Quarto docs, etc.</p>
<p>When you want to add a new package, edit <code>generate_env.R</code> and rerun that script to regenerate <code>default.nix</code>, and then run <code>nix-build</code>/<code>nix-shell</code>again.</p>
</section>
<section id="how-to-share-using-rix" class="level3">
<h3 class="anchored" data-anchor-id="how-to-share-using-rix">How to share using {rix}</h3>
<p>Share the <code>default.nix</code> file (typically committed alongside your project). Anyone with Nix installed can rebuild the exact same environment, byte for byte, regardless of their operating system, by running <code>nix-build</code> or <code>nix-shell</code> against that file.</p>
</section>
<section id="some-considerations-1" class="level3">
<h3 class="anchored" data-anchor-id="some-considerations-1">Some considerations</h3>
<p>Because {rix} depends on Nix, there’s a bigger lift required to install and learn Nix itself. In exchange, you get reproducibility that extends beyond R packages to the R version and even system dependencies.</p>
</section>
</section>
<section id="rv" class="level2">
<h2 class="anchored" data-anchor-id="rv"><strong>rv</strong></h2>
<p><a href="https://github.com/A2-ai/rv" rel="nofollow" target="_blank">Repo</a> | <a href="https://a2-ai.github.io/rv-docs/" rel="nofollow" target="_blank">Docs</a> | <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" />378 GitHub stars | <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Packages <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> R version <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Environments</p>
<section id="use-case-2" class="level3">
<h3 class="anchored" data-anchor-id="use-case-2">Use case</h3>
<p>This is a relatively new tool by A2-ai, written in the ever-so-popular <a href="https://rust-lang.org/" rel="nofollow" target="_blank">Rust</a> language, used for <strong>project-local R package management</strong>.</p>
<p>It supports multiple package repositories with per-package source control (e.g., forcing a specific package to build from source vs. binary), and claims large speed improvements (20x+) over {renv} for installs.</p>
<p>rv is <em>declarative</em>: you write your desired packages and versions into a config file, and rv resolves the entire dependency graph upfront before installing anything.</p>
</section>
<section id="how-to-use-rv" class="level3">
<h3 class="anchored" data-anchor-id="how-to-use-rv">How to use rv</h3>
<ol type="1">
<li>Install rv:</li>
</ol>
<p>rv is a standalone command-line tool, not an R package. There are a few installation instructions; <a href="https://a2-ai.github.io/rv-docs/intro/installation/" rel="nofollow" target="_blank">see the documentation for more</a>. I have Homebrew on my Mac, so I can run:</p>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre>Terminal</pre>
</div>
<pre>brew install rv-r</pre>
</div>
<ol start="2" type="1">
<li>Start a new project:</li>
</ol>
<p>Start a new project with <code>rv init</code>. For example, running the below creates a new folder called <code>hello_world</code>:</p>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre>Terminal</pre>
</div>
<pre>rv init hello_world</pre>
</div>
<p>If you open up the <code>hello_world</code> folder, you’ll see that rv created various files and folders in your project folder:</p>
<pre>hello_world/
  rv/
  library/
  scripts/
    activate.R
    rvr.R
  .gitignore
  .Rprofile
  rproject.toml</pre>
<ol start="3" type="1">
<li>Update config file:</li>
</ol>
<p>I don’t want to just repeat the documentation because <a href="https://a2-ai.github.io/rv-docs/intro/getting-started/" rel="nofollow" target="_blank">it’s quite excellent</a>, but essentially, you would fill in your packages as dependencies in <code>rproject.toml</code>:</p>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre>rproject.toml</pre>
</div>
<pre>r_version = &quot;4.4.2&quot;

# A list of packages to install and any additional configuration
# Examples:
    # &quot;dplyr&quot;,
    # {name = &quot;dplyr&quot;, repository = &quot;CRAN&quot;},
    # {name = &quot;dplyr&quot;, git = &quot;https://github.com/tidyverse/dplyr.git&quot;, tag = &quot;v1.1.4&quot;},
dependencies = [
    &quot;dplyr&quot;,
    &quot;ggplot2&quot;
]</pre>
</div>
<ol start="4" type="1">
<li>Install packages:</li>
</ol>
<p>Then, run <code>rv sync</code> to install the packages in the config file, as well as all of their dependencies:</p>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre>Terminal</pre>
</div>
<pre>rv sync</pre>
</div>
<p>This resolves the dependency graph and writes the exact versions to an <code>rv.lock</code> file, something like:</p>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre>rv.lock</pre>
</div>
<pre>[[packages]]
name = &quot;dplyr&quot;
version = &quot;1.1.4&quot;
repository = &quot;CRAN&quot;
source = &quot;Repository&quot;
hash = &quot;fedd9d00c2944ff00a0e2696ccf048ec&quot;

[[packages]]
name = &quot;ggplot2&quot;
version = &quot;3.5.1&quot;
repository = &quot;CRAN&quot;
source = &quot;Repository&quot;
hash = &quot;44c6a2f8202d5b7e6c72a95ca7ea1795&quot;</pre>
</div>
</section>
<section id="how-to-share-using-rv" class="level3">
<h3 class="anchored" data-anchor-id="how-to-share-using-rv">How to share using rv</h3>
<p>If someone wants to open your rv project, they can clone your folder (including the .toml) and then run <code>rv init</code>.</p>
</section>
<section id="some-considerations-2" class="level3">
<h3 class="anchored" data-anchor-id="some-considerations-2">Some considerations</h3>
<p>Because rv is declarative and resolves the whole dependency graph upfront, it’s less forgiving of “install whatever you need as you go” exploratory work that {renv} accommodates; you’re expected to know (or figure out) your dependencies before you can sync.</p>
<p>You may notice that rv’s <code>rproject.toml</code> has an <code>r_version</code> field, but this does not actually manage the R version. Like {renv}, rv does not download, install, or switch R versions for you (again, you’d want a tool like rig for that).</p>
<p>If you want a single tool that tracks both your R packages and R version, and can actually switch R versions for you, you may want to check out…</p>
</section>
</section>
<section id="uvr" class="level2">
<h2 class="anchored" data-anchor-id="uvr"><strong>uvr</strong></h2>
<p><a href="https://github.com/nbafrank/uvr" rel="nofollow" target="_blank">Repo</a> | <a href="https://nbafrank.github.io/uvr/" rel="nofollow" target="_blank">Docs</a> | <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" />350 GitHub stars | <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Packages <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> R version <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Environments</p>
<section id="use-case-3" class="level3">
<h3 class="anchored" data-anchor-id="use-case-3">Use case</h3>
<p>uvr is built by nbafrank, an independent developer. Like rv, it’s also written in Rust, and it is explicitly modeled on Python’s <a href="https://docs.astral.sh/uv/" rel="nofollow" target="_blank">uv</a>, a.k.a., <a href="https://emily.space/posts/251023-uv" rel="nofollow" target="_blank">the best thing to happen to the Python ecosystem in a decade</a>.</p>
<p>It bundles <strong>R package management and R version management</strong> (two things that {renv} and rv treat separately), backed by a single lockfile (<code>uvr.lock</code>) that is meant to be the “source of truth” for a project.</p>
</section>
<section id="how-to-use-uvr" class="level3">
<h3 class="anchored" data-anchor-id="how-to-use-uvr">How to use uvr</h3>
<p>uvr, like rv, is <em>declarative</em>: you add packages (and optionally an R version constraint) to a manifest, and uvr resolves and locks the whole dependency graph before installing anything.</p>
<ol type="1">
<li>Install uvr:</li>
</ol>
<p>uvr is a standalone binary, not an R package (though a companion R package, <a href="https://github.com/nbafrank/uvr-r" rel="nofollow" target="_blank">uvr-r</a>, lets you drive it from the R console instead of the terminal).</p>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre>Terminal</pre>
</div>
<pre>curl -fsSL https://raw.githubusercontent.com/nbafrank/uvr/main/install.sh | sh</pre>
</div>
<ol start="2" type="1">
<li>Start a new project:</li>
</ol>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre>Terminal</pre>
</div>
<pre>mkdir my-project && cd my-project
uvr init --r-version &quot;&gt;=4.4.2&quot;</pre>
</div>
<p>This creates a <code>uvr.toml</code> manifest in the project folder.</p>
<ol start="3" type="1">
<li>Add packages:</li>
</ol>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre>Terminal</pre>
</div>
<pre>uvr add dplyr ggplot2</pre>
</div>
<p>Each <code>add</code> updates <code>uvr.toml</code> and re-resolves <code>uvr.lock</code>. (uvr also supports Bioconductor and GitHub sources, e.g. <code>uvr add DESeq2 --bioc</code> or <code>uvr add user/repo@main</code>.)</p>
<ol start="4" type="1">
<li>Install everything from the lockfile:</li>
</ol>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre>Terminal</pre>
</div>
<pre>uvr sync</pre>
</div>
<p>This is what generates and keeps <code>uvr.lock</code> up to date, capturing both the R version and package versions, something like:</p>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre>uvr.lock</pre>
</div>
<pre>r-version = &quot;4.4.2&quot;

[[packages]]
name = &quot;dplyr&quot;
version = &quot;1.1.4&quot;
source = &quot;cran&quot;
hash = &quot;fedd9d00c2944ff00a0e2696ccf048ec&quot;

[[packages]]
name = &quot;ggplot2&quot;
version = &quot;3.5.1&quot;
source = &quot;cran&quot;
hash = &quot;44c6a2f8202d5b7e6c72a95ca7ea1795&quot;</pre>
</div>
<ol start="5" type="1">
<li>Run scripts inside the isolated environment:</li>
</ol>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre>Terminal</pre>
</div>
<pre>uvr run analysis.R</pre>
</div>
</section>
<section id="how-to-share-using-uvr" class="level3">
<h3 class="anchored" data-anchor-id="how-to-share-using-uvr">How to share using uvr</h3>
<p>Commit <code>uvr.toml</code> and <code>uvr.lock</code> alongside your project. Someone who wants to reproduce your environment (including your pinned R version) clones the repo and runs:</p>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre>Terminal</pre>
</div>
<pre>uvr sync --frozen</pre>
</div>
<p><code>--frozen</code> will fail if the lockfile is stale.</p>
</section>
</section>
<section id="ir" class="level2">
<h2 class="anchored" data-anchor-id="ir"><strong>ir</strong></h2>
<p><a href="https://github.com/r-lib/ir" rel="nofollow" target="_blank">Repo</a> | <a href="https://r-lib.github.io/ir/" rel="nofollow" target="_blank">Docs</a> | <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" />65 GitHub stars | <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Packages <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> R version <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Environments</p>
<section id="use-case-4" class="level3">
<h3 class="anchored" data-anchor-id="use-case-4">Use case</h3>
<p>ir operates one level <em>down</em> from the project-level tools. It’s built for single scripts or Quarto documents rather than whole projects. It’s a command-line tool for running portable R scripts and rendering Quarto documents whose package requirements (and optionally an R version) live inside the file itself for <strong>single-file workflows</strong> that don’t quite need a project, but still need to be easy to share and rerun later.</p>
</section>
<section id="how-to-use-ir" class="level3">
<h3 class="anchored" data-anchor-id="how-to-use-ir">How to use ir</h3>
<ol type="1">
<li>Install ir:</li>
</ol>
<p>Follow the <a href="https://github.com/r-lib/ir" rel="nofollow" target="_blank">installation instructions in the repo</a>:</p>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre>Terminal</pre>
</div>
<pre>curl -fsSL https://raw.githubusercontent.com/r-lib/ir/main/scripts/install.sh | sh</pre>
</div>
<ol start="2" type="1">
<li>Declare requirements in the file itself:</li>
</ol>
<p>Add frontmatter (for <code>.qmd</code>/<code>.Rmd</code>) or a header comment (for <code>.R</code> scripts) listing the packages and, optionally, an R version or a Posit Package Manager snapshot date:</p>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre>script.R</pre>
</div>
<pre>#!/usr/bin/env -S ir run
#| packages:
#|   - dplyr&gt;=1.0
#|   - ggplot2@3.5.1
#| isolated: true
#| exclude-newer: &quot;2025-05-15&quot;

library(dplyr)
library(ggplot2)

mtcars |&gt; count(cyl, gear) |&gt; ggplot(aes(x = cyl, y = gear)) + geom_col()</pre>
</div>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre>report.qmd</pre>
</div>
<pre>---
title: My report
ir:
  packages:
    - dplyr&gt;=1.0
    - ggplot2@3.5.1
  isolated: true
  exclude-newer: 2025-05-15
---</pre>
</div>
<ol start="3" type="1">
<li>Run or render the file:</li>
</ol>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre>Terminal</pre>
</div>
<pre>ir run script.R
ir render report.qmd</pre>
</div>
<p>When you run or render the file, ir resolves the requirements, prepares cached package libraries, and launches R or Quarto with a runtime ready to use.</p>
</section>
<section id="how-to-share-using-ir" class="level3">
<h3 class="anchored" data-anchor-id="how-to-share-using-ir">How to share using ir</h3>
<p>Send the single <code>.R</code> or <code>.qmd</code> file (there’s no separate lockfile or project directory to keep track of). On first use, ir prepares its own resolver tooling in its cache, so the recipient doesn’t need to pre-install {pak} or {renv}; they can run <code>ir run</code> or <code>ir render</code> and ir takes care of resolving and caching the right packages.</p>
</section>
<section id="some-considerations-3" class="level3">
<h3 class="anchored" data-anchor-id="some-considerations-3">Some considerations</h3>
<p>ir is the newest tool here (first released mid-2026) and intentionally narrow in scope: it solves reproducibility for a <em>single</em> file, not a whole project. So, it’s a complement to, not a replacement for, {renv}/{rix}/rv/uvr when you actually do need a shared project-level environment.</p>
</section>
</section>
<section id="even-more-package-and-environment-managers" class="level2">
<h2 class="anchored" data-anchor-id="even-more-package-and-environment-managers">Even more package and environment managers</h2>
<p>This is just the surface of what is available out there. The amazing Novica Nakov has created a <a href="https://r-devs.com/" rel="nofollow" target="_blank">new R tooling webpage</a> to track new R tools as they show up. So, if you want to follow an evolving list, I recommend keeping an eye out there!</p>
<p>While doing my research, I ran into a few more tools that I considered adding to this post and decided to not. Here are what they are, and why I left them out:</p>
<ul>
<li><a href="https://devenv.sh/" rel="nofollow" target="_blank">devenv</a>: a general-purpose Nix-based dev environment manager, not specifically for R. It shares {rix}’s core mechanism but doesn’t have {rix}’s R-specific niceties like date-pinned CRAN/Bioconductor snapshots.</li>
<li><a href="https://github.com/rrepo-org/rpx" rel="nofollow" target="_blank">rpx</a>, which I discovered on <a href="https://www.reddit.com/r/rstats/comments/1wjvk8h/rpx_v200_why_package_managers_need_backtracking/" rel="nofollow" target="_blank">this Reddit thread</a> as I was writing this: I didn’t add it because I’d waffled on finishing this post for too long and it would have messed up my Spelling Bee game. If anybody wants to write about it for R Works, let me know!</li>
<li><a href="https://github.com/r-lib/rig" rel="nofollow" target="_blank">rig</a>: a command-line tool for installing, listing, and switching between multiple R versions on your machine. I didn’t add it because there’s nothing to share with others (it’s more a personal R version manager), but as noted earlier, can be used to conform to the pinned R versions tracked in tools like {renv} or rv.</li>
</ul>


</section>


<div id="quarto-appendix" class="default"><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>Their names create a fun <a href="https://mywordle.strivemath.com/spellbee?l=rgtvgira" rel="nofollow" target="_blank">Spelling Bee game</a>:</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="https://i0.wp.com/rworks.dev/posts/r-managers/images/spelling-bee.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-1" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/r-managers/images/spelling-bee.png?w=578&#038;ssl=1" class="img-fluid quarto-figure quarto-figure-center figure-img" style="width:40.0%" alt="A Spelling Bee game made up of the letters RENUIX" data-recalc-dims="1"></a></p>
</figure>
</div>
<img src="https://s.w.org/images/core/emoji/13.0.0/72x72/21a9.png" alt="↩" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎</li>
<li id="fn2"><p><a href="https://docs.astral.sh/ruff/" rel="nofollow" target="_blank">ruff</a>? <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f609.png" alt="😉" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/13.0.0/72x72/21a9.png" alt="↩" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎</p></li>
<li id="fn3"><p>Since I’m listing both R packages and CLI tools, I am distinguishing them by wrapping the R packages in <code>{}</code>.<img src="https://s.w.org/images/core/emoji/13.0.0/72x72/21a9.png" alt="↩" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎</p></li>
</ol>
</section></div> 
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://rworks.dev/posts/r-managers/"> R Works</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/renv-rix-rv-uvr-ir-an-explosion-of-r-package-and-environment-managers/">{renv}, {rix}, rv, uvr, ir: An explosion of R package and environment managers</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404091</post-id>	</item>
		<item>
		<title>Goodbye R-Forge, hello Codeberg</title>
		<link>https://www.r-bloggers.com/2026/10/goodbye-r-forge-hello-codeberg/</link>
		
		<dc:creator><![CDATA[Achim Zeileis]]></dc:creator>
		<pubDate>Mon, 05 Oct 2026 22:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://www.zeileis.org/news/rforge_codeberg/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>    After 20 years of service the R-Forge package hosting platform will be retiring soon. Codeberg is a modern alternative that shares many of the ideas and values that R-Forge was built on. A tutorial for transitioning to Codeberg (or other Git-...</p></div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/goodbye-r-forge-hello-codeberg/">Goodbye R-Forge, hello Codeberg</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://www.zeileis.org/news/rforge_codeberg/"> Achim Zeileis</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>

    
    <p>After 20 years of service the R-Forge package hosting platform will be retiring soon. Codeberg is a modern alternative that shares many of the ideas and values that R-Forge was built on. A tutorial for transitioning to Codeberg (or other Git-based platforms) is provided.</p>
    
    
    <h2 id="you-say-goodbye">You say goodbye</h2>

<p><a href="https://r-forge.r-project.org/" rel="nofollow" target="_blank"><strong>R-Forge</strong></a> was created 20 years ago after the
<a href="https://www.r-project.org/conferences/useR-2006/" rel="nofollow" target="_blank">2nd useR! conference in 2006</a>.
<a href="https://statmath.wu.ac.at/~hornik/" rel="nofollow" target="_blank">Kurt Hornik</a> had the idea that
<em>“R should have its own SourceForge!”</em> (note that GitHub did not yet exist at the time).
However, the goal was that this code sharing platform should be
created by the R community for the R community.
The implementation was led by <a href="https://www.linkedin.com/in/stefan-theussl" rel="nofollow" target="_blank">Stefan Theussl</a> in the following months
using <a href="https://en.wikipedia.org/wiki/GForge" rel="nofollow" target="_blank">GForge/FusionForge</a>, a fork of the software behind SourceForge.
Hosting and funding was provided by <a href="https://www.wu.ac.at/" rel="nofollow" target="_blank">WU Wirtschaftsuniversität Wien</a> and
<a href="https://www.r-project.org/" rel="nofollow" target="_blank">The R Project</a>.</p>

<p>R-Forge offers a Subversion (SVN) repository for collaborative development of R packages along with various other
services that were state of the art at the time: daily package builds and checks, forums, mailing lists, bug trackers, web pages.
A short introduction was published in the inaugural issue of The R Journal:
Theußl &#038; Zeileis (2009). “Collaborative Software Development Using R-Forge.” <em>The R Journal</em>, <strong>1</strong>(1):9-14.
<a href="https://doi.org/10.32614/RJ-2009-007" rel="nofollow" target="_blank">doi:10.32614/RJ-2009-007</a></p>

<p>As the FusionForge system is no longer maintained, R-Forge will be retired in the next months, switching to a read-only site.
The system was somewhat dated anyways: The website has a certain “00s” vibe (see the screenshot of my profile page below)
and it lacks services that became standard over the last two decades like issues, pull requests, and continuous integration.</p>

<p><img src="https://i0.wp.com/www.zeileis.org/assets/posts/2026-10-06-rforge_codeberg/zeileis-rforge.png?w=578&#038;ssl=1" alt="Screenshot of Achim Zeileis' user page on R-Forge" data-recalc-dims="1" /></p>

<h2 id="i-say-hello">I say hello</h2>

<p><a href="https://codeberg.org/" rel="nofollow" target="_blank"><strong>Codeberg</strong></a> is a non-profit, community-led effort that provides services to free and open-source projects,
such as Git hosting (using Forgejo), pages, continuous integration, etc.
It is run by the non-profit organization Codeberg e.V. based in Berlin, Germany, which contributors
can (but do not have to) <a href="https://join.codeberg.org/" rel="nofollow" target="_blank">become a member</a> of.
<em>(I joined this summer.)</em></p>

<p>Thus, Codeberg shares many of the ideas and values, that R-Forge was built on, but provides
state-of-the-art open-source tools for collaborative development of R packages (and beyond).
For a first impression of the look and feel, see the screenshot of my profile page below.</p>

<p><img src="https://i2.wp.com/www.zeileis.org/assets/posts/2026-10-06-rforge_codeberg/zeileis-codeberg.png?w=578&#038;ssl=1" alt="Screenshot of Achim Zeileis' user page on Codeberg" data-recalc-dims="1" /></p>

<h2 id="i-say-go-go-go">I say go go go</h2>

<p><strong>For transitioning from R-Forge to Codeberg</strong> &#8211; or some other Git-based code hosting platform &#8211; 
<a href="https://retostauffer.org/" rel="nofollow" target="_blank">Reto Stauffer</a> has written up a <a href="https://codeberg.org/zeileis/c403/issues/1" rel="nofollow" target="_blank">step-by-step guide</a>.
The most important steps are also listed below.</p>

<h4 id="1-svn-checkout-from-r-forge">1. SVN checkout from R-Forge</h4>

<p>The starting point for the transition is the SVN repository from
R-Forge that you probably have already checked out. For illustration we are using the <code>c403</code> R package
that is one of the packages contained in the <a href="https://r-forge.r-project.org/projects/exams/" rel="nofollow" target="_blank">exams</a> project on R-Forge.</p>

<pre>svn checkout svn://R-Forge.R-project.org/svnroot/exams/
</pre>

<h4 id="2-list-of-r-forge-contributors">2. List of R-Forge contributors</h4>

<p>To find all SVN usernames who ever contributed to the SVN on R-Forge repository
(in this example, to the folder <code>exams/pkg/c403</code>) the command <code>svn log -q</code> can be used. Using the following shell command
a text file with the unique names can be created. Note that <code>exams/pkg/c403</code> needs to be replaced for other projects
and the output file name can be adjusted too:</p>

<pre>(cd exams/pkg/c403 && svn log -q | grep -e '^r' | awk 'BEGIN { FS = &quot;|&quot; } ; { print $2 }' | sort | uniq) &gt; \
  authors_c403.txt
</pre>

<p>This creates the text file <code>authors_c403.txt</code> which will look something like:</p>

<pre>  azeileis
  retos
</pre>

<p>This file needs to be modified by adding the full names and e-mail addresses of the contributors.
<em>Important:</em> The leading spaces need to be removed in each line for the subsequent commands to work.</p>

<pre>azeileis = Achim Zeileis &lt;Achim.Zeileis@R-project.org&gt;
retos = Reto Stauffer &lt;Reto.Stauffer@uibk.ac.at&gt;
</pre>

<p>Provided the profile pages on R-Forge are up to date for the users, it is also possible to scrape
this information, see the optional step 2*. below.</p>

<h4 id="3-clone-a-git-version-of-the-svn">3. Clone a Git version of the SVN</h4>

<p>Using <code>git-svn</code> a Git version of the SVN can be easily created.
For this step it is crucial that all users are listed in the authors file created in the previous
step, otherwise <code>git-svn</code> will fail.</p>

<pre>git svn clone svn://R-Forge.R-project.org/svnroot/exams/pkg/c403 \
    --authors-file=authors_c403.txt --no-metadata \
    git-c403
</pre>

<p>Again, adjust the URL of the SVN, the authors file name, and the name of the git repository (<code>git-c403</code>)
as needed.</p>

<h4 id="4-optional-filter-files-in-the-git-repository">4. Optional: Filter files in the Git repository</h4>

<p>This step is not needed for the <code>c403</code> example but is particularly useful when migrating a full
R-Forge repository (rather than just a <code>pkg</code> sub-folder). In that case
it might be useful to filter out certain files or directories such as the initial <code>README</code> file
auto-created by R-Forge or the <code>www</code> sub-directory (typically just containing original PHP file or
a auto-generated web page from <code>pkgdown</code> or <code>altdoc</code>).</p>

<p>Such files can be filtered, i.e., erased completely from the repository history, using <code>git-filter-repo</code>:</p>

<pre>git-filter-repo --force --path 'README' --invert-paths
git-filter-repo --force --path 'www' --invert-paths
</pre>

<h4 id="5-import-git-repository-on-codeberg">5. Import Git repository on Codeberg</h4>

<p>First, create a new project on Codeberg, e.g., <a href="https://codeberg.org/zeileis/c403" rel="nofollow" target="_blank">https://codeberg.org/zeileis/c403</a>. The project should
be completely empty without any initial commit.</p>

<p>Then, all left to do then is to tell the Git repository where its new home is (again adjust all names
correspondingly):</p>

<pre>cd git-c403
git remote add origin ssh://git@codeberg.org/zeileis/c403.git
git push --set-upstream origin main
</pre>

<p>Analogous commands can be used for setting the origin to a project on GitHub, GitLab, etc.</p>

<p>All history will be immediately available in the new repository. If the authors have accounts on
Codeberg, their profiles will be linked based on the e-mail address(es) associated with their
Codeberg account.</p>

<h4 id="6-package-builds-and-checks-via-r-universe">6. Package builds and checks via R-universe</h4>

<p>To automatically build and check the development versions of the R package on
Codeberg, the most convenient solution is to use the
<a href="https://r-universe.dev/" rel="nofollow" target="_blank">R-universe</a> service
developed by Jeroen Ooms and provided by rOpenSci.</p>

<p>In short, to make use of it, you need a GitHub account and create a repository
like <a href="https://github.com/zeileis/zeileis.r-universe.dev" rel="nofollow" target="_blank">zeileis.r-universe.dev</a>.
This should contain a <code>packages.json</code> file with the names and repository URLs
of one or more R packages, e.g.,</p>

<pre>[
    {
        &quot;package&quot;: &quot;c403&quot;,
        &quot;url&quot;: &quot;https://codeberg.org/zeileis/c403&quot;
    }
]
</pre>

<p>As a final step, the R-universe app has to be installed on the same GitHub
account. See the <a href="https://docs.r-universe.dev/publish/set-up.html" rel="nofollow" target="_blank">R-universe set up</a>
documentation for more details.</p>

<h4 id="7-codeberg-pages">7. Codeberg pages</h4>

<p>If you want to add a project page for your package, e.g., generated via
<a href="https://cran.r-project.org/package=altdoc" rel="nofollow" target="_blank">altdoc</a> or
<a href="https://cran.r-project.org/package=pkgdown" rel="nofollow" target="_blank">pkgdown</a>, then
<a href="https://codeberg.page/" rel="nofollow" target="_blank">https://codeberg.page/</a> can be used.</p>

<p>The <code>c403</code> package does not have a page, yet, but for an example see
<a href="https://zeileis.codeberg.page/topmodels/" rel="nofollow" target="_blank">https://zeileis.codeberg.page/topmodels/</a> which is generated from
the <a href="https://codeberg.org/zeileis/topmodels/" rel="nofollow" target="_blank">topmodels</a> R package
using <code>altdoc</code> in combination with <a href="https://quarto.org/" rel="nofollow" target="_blank">quarto</a>.</p>

<p>All that needs to be done to create the page manually, is to create an
empty branch <code>pages</code> via <code>git switch --orphan pages</code>, adding the static
site files, and pushing them to the repository.</p>

<p>Alternatively, it is also possible to use 
<a href="https://docs.codeberg.org/codeberg-pages/forgejo-actions/" rel="nofollow" target="_blank">Forgejo actions</a>
to do this automatically.</p>

<h4 id="8-update-descriptions-and-links">8. Update descriptions and links</h4>

<p>Finally, inform users where the sources of the package can be found, i.e., update
the <code>DESCRIPTION</code> (especially the <code>URL</code> and <code>BugReports</code> fields) and in the <code>NEWS</code>.
Additionally, a <code>README.md</code> file would be expected in a project while on R-Forge
this was often not used (because R-Forge would be aware of the <code>DESCRIPTION</code> instead).
Finally, it is recommended to add a link from the old R-Forge project to the new
Codeberg project (possibly also in the <code>www</code> folder). To assure that none of the
contributors can make modifications in the old R-Forge SVN, the permissions
on R-Forge can be switched to read-only (typically for the <em>Senior Developer</em> role).</p>

<h4 id="optional-addition-2-scrape-author-names-and-e-mail-addresses">Optional addition: 2.* Scrape author names and e-mail addresses</h4>

<p>In step 2, after the file with the author names (<code>authors_c403.txt</code> in the example above) has been created,
one can try to automate the addition of proper names and e-mail addresses. This is particularly useful
if there are more than just a handful of users. It works when the R-Forge user pages list the proper names
and the (current) e-mail addresses.</p>

<p>The function below can be called in R as</p>

<pre>rforge_authors(&quot;c403&quot;)
</pre>

<p>It first reads the <code>authors_c403.txt</code> file, looks up all users on R-Forge
(using the <a href="https://cran.r-project.org/package=rvest" rel="nofollow" target="_blank">rvest</a> package),
and then updates the <code>authors_c403.txt</code> file accordingly.</p>

<pre>rforge_authors &lt;- function(x) {
  txt &lt;- sprintf(&quot;authors_%s.txt&quot;, x)
  if (file.exists(txt)) {
    a &lt;- readLines(txt)
    a &lt;- gsub(&quot; &quot;, &quot;&quot;, a)
  } else {
    stop(sprintf(&quot;Please use 'svn log' to set up an '%s' file with all user names&quot;, txt))
  }

  for (i in seq_along(a)) {  
    doc &lt;- a[i] |&gt;
      sprintf(fmt = &quot;https://r-forge.r-project.org/users/%s/&quot;) |&gt;
      rvest::read_html()
    n &lt;- doc |&gt;
      rvest::html_table() |&gt;
      lapply(as.data.frame) |&gt;
      lapply(subset, subset = X1 == &quot;Real Name:&quot;, select = &quot;X2&quot;, drop = TRUE) |&gt;
      unlist() |&gt;
      unique()
    if (length(n) &lt; 1L) n &lt;- &quot;FIXME&quot;
    e &lt;- doc |&gt;
      rvest::html_table() |&gt;
      lapply(as.data.frame) |&gt;
      lapply(subset, subset = X1 == &quot;Email Address:&quot;, select = &quot;X2&quot;, drop = TRUE) |&gt;
      unlist() |&gt;
      unique() |&gt;
      gsub(pattern = &quot; @nospam@ &quot;, replacement = &quot;@&quot;, fixed = TRUE)
    if (length(e) &lt; 1L) e &lt;- &quot;FIXME&quot;
    a[i] &lt;- sprintf(&quot;%s = %s &lt;%s&gt;&quot;, a[i], n, e)
  }
  writeLines(a, txt)
  invisible(a)
}
</pre>

    
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://www.zeileis.org/news/rforge_codeberg/"> Achim Zeileis</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/goodbye-r-forge-hello-codeberg/">Goodbye R-Forge, hello Codeberg</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404089</post-id>	</item>
		<item>
		<title>World crude oil stocks by @ellis2013nz</title>
		<link>https://www.r-bloggers.com/2026/10/world-crude-oil-stocks-by-ellis2013nz/</link>
		
		<dc:creator><![CDATA[free range statistics - R]]></dc:creator>
		<pubDate>Mon, 05 Oct 2026 16:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://freerangestats.info/blog/2026/10/06/world-crude-stocks</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; "> How much crude oil does the world have available? Inquiring minds want to know but it is surprisingly difficult to get a single number. We hear more about relative levels “less than at any point since XXXX” and changes “has dropped X million barrels in...</div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/world-crude-oil-stocks-by-ellis2013nz/">World crude oil stocks by @ellis2013nz</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://freerangestats.info/blog/2026/10/06/world-crude-stocks"> free range statistics - R</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
<p>How much crude oil does the world have available? Inquiring minds want to know but it is surprisingly difficult to get a single number. We hear more about relative levels “less than at any point since XXXX” and changes “has dropped X million barrels in the last month” than estimates of the absolute level. The International Energy Agency (IEA) publishes a lot of information on inventory levels and changes, but consolidated global stock estimates are, as far as I can see, not publicly available.</p>

<p>However, the Joint Organizations Data Initiative (JODI) does publish monthly country level estimates, reported to it voluntarily by countries. JODI partners are the APEC Energy Working Group, Eurostat, GECF, IEA, the Latin American and Caribbean Energy Organization (OLACDE), OPEC and the UN Statistical Division. It’s a very reputable set of partners! And JODI’s data are available to download.</p>

<p>Only 38 countries have data for every month JODI collects data, but this includes the USA, Japan, UK, the key European countries, and a few other big holders like Saudi Arabia, South Korea, Taiwan and Nigeria. The key omissions include, unfortunately but unsurprisingly, China, Russia, and Venezuela.. The data are also a bit old—latest data relating to July 2026 as at the time of writing, 6 October 2026, so a lag of 2-3 months. But it’s much better than nothing.</p>

<p>The point of today’s post is to get me the chart below, showing the best picture we can of the stocks of crude oil of the countries that do report those stocks, compared to a rough estimate of 85 million barrels per day of global crude refinery throughput:</p>

<object type="image/svg+xml" data="https://freerangestats.info/img/0334-world-crude-stocks.svg" width="450"><img src="https://i1.wp.com/freerangestats.info/img/0334-world-crude-stocks.png?w=450&#038;ssl=1" data-recalc-dims="1" /></object>

<p>That chart is now part of my <a href="https://freerangestats.info/fuel-crisis/index.html" rel="nofollow" target="_blank">fuel crisis monitoring page</a> which gets updated at least weekly.</p>

<p>The overall story is that we indeed have less crude oil in storage than at any time since these records began in the early 2000s; but it’s still around 20 days of refinery cover. And at the rate that it’s going down, it will take nearly two years to get down to just 10 days of refinery cover. Actually, I’d expect market panic to set in at around 15 days of cover, but I’m being conservative here because I’m interested in the point where the world really has run out of any buffer against supply disruptions.</p>

<p>OK, the rest of the blog is mostly about just how I got that data, made some choices about excluding some countries, and drew the final chart.</p>

<p>First, downloading the data. There’s a separate CSV for each year, going back to 2002. Here’s how I download all those and import them into R</p>

<figure class="highlight"><pre>library(tidyverse)
library(glue)
library(janitor)
library(countrycode)

# Set to FALSE if you don't want to download the latest 2026 data. Will need to
# update all this workflow once we get to 2027 data.
update_2026 &lt;- TRUE

#-----------------Downloads----------------

if (update_2026) {
  dir.create(&quot;fuel-crisis&quot;, showWarnings = FALSE)
  # Download current year. This will get more complete month by month.
  df &lt;- here(&quot;fuel-crisis/jodi-2026.csv&quot;)
  url &lt;- &quot;https://www.jodidata.org/_resources/files/downloads/oil-data/annual-csv/primary/primaryyear2026.csv&quot;
  download.file(url, destfile = df)
}


# Download historical data; only needed to be done once
if (!file.exists(here(&quot;fuel-crisis/jodi-2025.csv&quot;))) {
  for (y in 2002:2025) {
    df &lt;- here(glue(&quot;fuel-crisis/jodi-{y}.csv&quot;))
    url &lt;- glue(
      &quot;https://www.jodidata.org/_resources/files/downloads/oil-data/annual-csv/primary/{y}.csv&quot;
    )
    download.file(url, destfile = df)
  }
}

#----------------Import data------------------

# Import all the data to R:
jodi_hist_l &lt;- list()
for (i in 1:25) {
  df &lt;- here(glue(&quot;fuel-crisis/jodi-{i+2001}.csv&quot;))
  jodi_hist_l[[i]] &lt;- read.csv(df) |&gt; as_tibble()
}

jodi_hist &lt;- bind_rows(jodi_hist_l) |&gt;
  clean_names()

# see https://www.jodidata.org/_resources/files/downloads/oil-data/jodi-oil-wdb-item-names-ver2017.pdf
# for guide on what everything is
# CLOSTLV is closing stocks
# CRUDEOIL is just crude
# TOTCRUDE also includes NGL and refinery feedstocks, additives and other hydrocarbons and has a lot more missing data.</pre></figure>

<p>Now that I’ve got the data, I want to understand the missingness. At one point I contemplated imputing the missing values of various countries, and hence I made a big wide version of the data with the idea that I might model crude stocks based on movement in some of the other numerous variables in the data. I eventually abandoned this when I realised how little meaningful information there was on eg China and Russia to do this meaningfully. But I can still use the <code>jodi_wide</code> data set created below for most of my plots:</p>

<figure class="highlight"><pre>#-----------explore which countries missing---------------------
jodi_wide &lt;- jodi_hist |&gt;
  #  filter(energy_product == &quot;CRUDEOIL&quot;) |&gt;
  mutate(
    dimensions = paste(energy_product, flow_breakdown, unit_measure, sep = &quot;|&quot;)
  ) |&gt;
  mutate(obs_value = as.numeric(obs_value)) |&gt;
  select(ref_area, time_period, dimensions, obs_value) |&gt;
  drop_na() |&gt;
  spread(dimensions, obs_value, fill = NA) |&gt;
  rename(crude = `CRUDEOIL|CLOSTLV|KBBL`) |&gt;
  complete(ref_area, time_period, fill = list(crude = NA)) |&gt;
  mutate(
    country = countrycode(
      ref_area,
      origin = &quot;iso2c&quot;,
      destination = &quot;country.name.en&quot;
    )
  ) |&gt;
  mutate(
    ref_area = fct_relevel(ref_area, &quot;US&quot;),
    date = ym(time_period),
    date_n = as.numeric(date)
  )

last_date &lt;- jodi_wide |&gt;
  filter(!is.na(crude)) |&gt;
  summarise(ld = max(date)) |&gt;
  mutate(ld = format(ld, &quot;%B %Y&quot;)) |&gt;
  pull(ld)

the_caption &lt;- glue(
  &quot;Source: JODI-OIL World Database. Most recent data is for {last_date}.&quot;
)


jodi_wide |&gt;
  ggplot(aes(x = date, y = crude / 1000, colour = ref_area)) +
  geom_line() +
  theme(legend.position = &quot;none&quot;) +
  scale_y_continuous(label = comma) +
  labs(
    x = &quot;&quot;,
    y = &quot;Millions of barrels&quot;,
    caption = the_caption,
    title = &quot;Stocks of crude oil&quot;,
    subtitle = &quot;Coloured by country; legend not shown.&quot;
  )</pre></figure>

<p>Here’s that first exploratory plot, a line chart of all the countries. The big one here is the USA; I’ve not shown the legend because it would take up too much space.</p>

<object type="image/svg+xml" data="https://freerangestats.info/img/0334-line-all-countries.svg" width="450"><img src="https://i2.wp.com/freerangestats.info/img/0334-line-all-countries.png?w=450&#038;ssl=1" data-recalc-dims="1" /></object>

<p>Next I want to identify which countries have missing data, and in particular those that are potentially big contributors to world crude oil stocks but are just missing a few observations that perhaps I could impute. I start by making a <code>country_sum</code> summary dataset:</p>

<figure class="highlight"><pre>possible_obs &lt;- length(unique(jodi_wide$time_period))

country_sum &lt;- jodi_wide |&gt;
  group_by(ref_area, country) |&gt;
  summarise(
    crude_total = mean(crude, na.rm = TRUE),
    missing_obs = sum(is.na(crude))
  ) |&gt;
  ungroup() |&gt;
  # particular problem if a country has any missing observations and averages
  # 10 million barrels or more of stock:
  mutate(
    all_there = as.logical(missing_obs == 0),
    some_missing = as.logical(
      missing_obs &gt; 0 & missing_obs &lt; possible_obs &#038; !is.na(crude_total)
    ),
    problem = as.logical(crude_total &gt; 10000 & missing_obs &gt; 0)
  ) |&gt;
  arrange(desc(crude_total))

good_countries &lt;- country_sum |&gt;
  filter(all_there) |&gt;
  pull(country)

good_countries</pre></figure>

<p>That gives me this list of the countries that have a complete set of observations (largest crude oil stocks listed first):</p>
<pre>&gt; good_countries
 [1] &quot;United States&quot;  &quot;Japan&quot;          &quot;Saudi Arabia&quot;  
 [4] &quot;Germany&quot;        &quot;South Korea&quot;    &quot;Canada&quot;        
 [7] &quot;France&quot;         &quot;Turkey&quot;         &quot;Italy&quot;         
[10] &quot;Poland&quot;         &quot;Spain&quot;          &quot;United Kingdom&quot;
[13] &quot;Netherlands&quot;    &quot;Taiwan&quot;         &quot;Thailand&quot;      
[16] &quot;Norway&quot;         &quot;Nigeria&quot;        &quot;Australia&quot;     
[19] &quot;Sweden&quot;         &quot;Finland&quot;        &quot;Czechia&quot;       
[22] &quot;Hungary&quot;        &quot;Portugal&quot;       &quot;Austria&quot;       
[25] &quot;Slovakia&quot;       &quot;Belgium&quot;        &quot;Denmark&quot;       
[28] &quot;Chile&quot;          &quot;New Zealand&quot;    &quot;Azerbaijan&quot;    
[31] &quot;Brunei&quot;         &quot;Ireland&quot;        &quot;Switzerland&quot;   
[34] &quot;Estonia&quot;        &quot;Iceland&quot;        &quot;Luxembourg&quot;    
[37] &quot;Latvia&quot;         &quot;Slovenia&quot;   
</pre>

<p>It also lets me find some countries that have no observations at all:</p>

<figure class="highlight"><pre># 20 countries that never have any observations for Crude:
never_crude &lt;- filter(country_sum, is.na(problem))
print(never_crude$country)</pre></figure>

<pre>&gt; print(never_crude$country)
 [1] &quot;Albania&quot;             &quot;Bangladesh&quot;         
 [3] &quot;Bermuda&quot;             &quot;Belarus&quot;            
 [5] &quot;China&quot;               &quot;Egypt&quot;              
 [7] &quot;Georgia&quot;             &quot;Hong Kong SAR China&quot;
 [9] &quot;Moldova&quot;             &quot;Malta&quot;              
[11] &quot;Malaysia&quot;            &quot;Niger&quot;              
[13] &quot;Nepal&quot;               &quot;Sudan&quot;              
[15] &quot;Singapore&quot;           &quot;Syria&quot;              
[17] &quot;Eswatini&quot;            &quot;Tajikistan&quot;         
[19] &quot;Vietnam&quot;             &quot;Yemen&quot; 
</pre>
<p>Discovering that there were 20 of these, including China and Singapore, is what made me decide there was no point trying to impute the missing values. But I did a bit more exploration of which countries had <em>some</em> observations but not a complete set. Firstly, a scatter plot of all such countries:</p>

<object type="image/svg+xml" data="https://freerangestats.info/img/0334-scatter-missing.svg" width="450"><img src="https://i0.wp.com/freerangestats.info/img/0334-scatter-missing.png?w=450&#038;ssl=1" data-recalc-dims="1" /></object>

<p>…and then a faceted time series of the 9 biggest problematic countries.</p>

<object type="image/svg+xml" data="https://freerangestats.info/img/0334-facet-big-missing.svg" width="450"><img src="https://i1.wp.com/freerangestats.info/img/0334-facet-big-missing.png?w=450&#038;ssl=1" data-recalc-dims="1" /></object>

<p>I judged that maybe I could impute values for India, Mexico , Venezuela and Brazil based on their levels going up or down on average at about the world rate but decided this just wasn’t going to be worth it. And clearly Russia dwarfs them all and is just not worth trying, given the special circumstances there.</p>

<p>Here’s the code for those two plots:</p>

<figure class="highlight"><pre>country_sum |&gt;
  ggplot(aes(
    x = crude_total,
    y = missing_obs,
    label = ref_area,
    colour = problem
  )) +
  geom_text() +
  scale_x_log10(label = comma) +
  labs(
    x = &quot;Average crude stocks (thousands of barrels)&quot;,
    y = &quot;Number of months missing an observation&quot;,
    caption = the_caption,
    title = &quot;Size of crude stocks by number of missing observations&quot;
  ) +
  theme(legend.position = &quot;none&quot;)

# The nine biggest problem countries in terms of partly missing data
jodi_wide |&gt;
  #  filter(ref_area %in% filter(country_sum, some_missing)$ref_area) |&gt;
  filter(ref_area %in% filter(country_sum, problem)$ref_area) |&gt;
  mutate(country = fct_reorder(country, crude)) |&gt;
  ggplot(aes(x = date, y = crude / 1000)) +
  facet_wrap(~country, scales = &quot;free_y&quot;) +
  geom_line(colour = &quot;steelblue&quot;) +
  expand_limits(y = 0) +
  scale_y_continuous(label = comma) +
  labs(
    x = &quot;&quot;,
    y = &quot;Millions of barrels&quot;,
    title = &quot;Crude oil stocks of countries missing at least one data point&quot;,
    caption = the_caption
  )</pre></figure>

<p>One last exploratory plot to try to see the impact of including or dropping those countries with partial data, here’s an area chart:</p>

<object type="image/svg+xml" data="https://freerangestats.info/img/0334-missing-area.svg" width="450"><img src="https://i2.wp.com/freerangestats.info/img/0334-missing-area.png?w=450&#038;ssl=1" data-recalc-dims="1" /></object>

<p>Thinking of the Russia situation in particular, I decided best just to exclude those partially missing countries altogether.</p>

<p>Here’s the code for that chart (and which also makes the <code>crude_stocks</code> data object I will shortly be using for my real plot)</p>

<figure class="highlight"><pre>#----------------Summarise and draw chart------------

# Crude oil stocks (excludes NGL etc because often missing data):
crude_stocks &lt;- jodi_wide |&gt;
  left_join(select(country_sum, ref_area, some_missing), by = &quot;ref_area&quot;) |&gt;
  group_by(date, some_missing) |&gt;
  summarise(
    total_crude_mbbl = sum(crude, na.rm = TRUE) / 1000,
    reporting_countries = length(unique(ref_area))
  ) |&gt;
  ungroup()

crude_stocks |&gt;
  ggplot(aes(x = date, y = total_crude_mbbl, fill = some_missing)) +
  geom_area() +
  labs(
    caption = the_caption,
    y = &quot;Total crude oil (millions of barrels)&quot;,
    x = &quot;&quot;,
    title = &quot;Total crude oil by missingness status of countries&quot;,
    fill = &quot;Countries missing any data:&quot;
  )</pre></figure>

<p>Finally, the code to draw the actual presentation plot. A key magic number here is the world refinery usage of crude oil at 85 million barrels per day; this is the approximate “crude runs” value from <a href="https://iea.blob.core.windows.net/assets/6e8bf347-c9af-4549-a3ac-07e5361813e9/-13AUG2025_OilMarketReport.pdf" rel="nofollow" target="_blank">a recent IEA Oil Market Report</a>. Note that this is less than the commonly cited values of around 105 million barrels per day, which includes NGLs, biofuels, and other liquid fuels. The 85m per day value is the appropriate one for me to use when looking at the CRUDEOIL series in my original JODI data; noting that they also have a TOTCRUDE series that would relate to the 105m per day figure, but which has a lot more missing data and is arguably not really what I’m most interested in anyway.</p>

<figure class="highlight"><pre># note the commonly used figure of 105 is for TOTCRUDE, not just CRUDEOIl (which is more like 85)
world_use_per_day &lt;- 85

# Summary for calculation of how long until only 10 days of cover:
war_summary &lt;- crude_stocks |&gt;
  # restrict it to countries that have observations for all months
  filter(!some_missing) |&gt;
  summarise(
    prewar = total_crude_mbbl[date == &quot;2026-03-01&quot;],
    latest = total_crude_mbbl[date == max(date)],
    days = max(date) - as.Date(&quot;2026-03-01&quot;)
  ) |&gt;
  mutate(
    rate = (prewar - latest) / as.numeric(days),
    ten_days_left = (latest - world_use_per_day * 10) / rate
  )

# draw chart:
crude_stocks |&gt;
  filter(!some_missing) |&gt;
  ggplot(aes(x = date, y = total_crude_mbbl)) +
  geom_line(colour = &quot;blue&quot;) +
  geom_hline(yintercept = world_use_per_day * 10, colour = &quot;darkred&quot;) + #expand_limits(y = world_use_per_day * 10) +
  expand_limits(y = 0) +
  scale_y_continuous(
    label = comma_format(suffix = &quot;m&quot;),
    sec.axis = sec_axis(
      ~ . / world_use_per_day,
      name = glue(
        &quot;Days of crude inventory cover,\nat {world_use_per_day} million barrels per day&quot;
      )
    )
  ) +
  labs(
    x = glue(
      &quot;{length(good_countries)} countries in total have data for all months in this period.&quot;
    ),
    title = &quot;Crude oil stocks of consistently reporting countries worldwide&quot;,
    y = &quot;Millions of barrels&quot;,
    subtitle = glue(
      &quot;Excluding countries with any missing data (eg China, Russia, India, Venezuela).
If the rate of decline since March 2026 continued, crude inventories would fall to 10 days of cover in {round(war_summary$ten_days_left / 7)} weeks.&quot;
    ),
    caption = the_caption
  )</pre></figure>

<p>That gets me this graphic, which is the one I will keep up to date from now on:</p>
<object type="image/svg+xml" data="https://freerangestats.info/img/0334-world-crude-stocks.svg" width="450"><img src="https://i1.wp.com/freerangestats.info/img/0334-world-crude-stocks.png?w=450&#038;ssl=1" data-recalc-dims="1" /></object>

<p>That’s all for today. Take care out there, and buy yourself an electric vehicle and rooftop solar if you can!</p>

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://freerangestats.info/blog/2026/10/06/world-crude-stocks"> free range statistics - R</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/world-crude-oil-stocks-by-ellis2013nz/">World crude oil stocks by @ellis2013nz</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404079</post-id>	</item>
		<item>
		<title>mlreserve: machine-learning loss reserving on &#8216;real-world&#8217; triangles (based on a &#8216;ChainLadder&#8217; fork)</title>
		<link>https://www.r-bloggers.com/2026/10/mlreserve-machine-learning-loss-reserving-on-real-world-triangles-based-on-a-chainladder-fork/</link>
		
		<dc:creator><![CDATA[T. Moudiki]]></dc:creator>
		<pubDate>Sat, 03 Oct 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://thierrymoudiki.github.io//blog/2026/10/03/r/mlreserve</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; "> This post presents a function `mlReserve()` for machine-learning loss reserving on 'real-world' triangles, based on a fork of the 'ChainLadder' package. It includes two experiments, each scored against a known true reserve: (A) Two triangles from an individual-claims simulator (Wang &#038; Wüthrich), embedded below: chain ladder vs mlReserve, in detail; (...</div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/mlreserve-machine-learning-loss-reserving-on-real-world-triangles-based-on-a-chainladder-fork/">mlreserve: machine-learning loss reserving on ‘real-world’ triangles (based on a ‘ChainLadder’ fork)</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://thierrymoudiki.github.io//blog/2026/10/03/r/mlreserve"> T. Moudiki's Webpage - R</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
<p>This post presents a function <code>mlReserve()</code> for machine-learning loss reserving on ‘real-world’ triangles, <strong>based on a fork of R’s ‘ChainLadder’ package</strong> (<a href="https://github.com/thierrymoudiki/ChainLadder/tree/master" rel="nofollow" target="_blank">https://github.com/thierrymoudiki/ChainLadder/tree/master</a>). It includes two experiments, each scored against a known true reserve: (A) Two triangles from an individual-claims simulator (Wang &#038; Wüthrich, <a href="https://github.com/actuarial-data-science/PackageIndividualClaimsSimulator" rel="nofollow" target="_blank">https://github.com/actuarial-data-science/PackageIndividualClaimsSimulator</a>), embedded below: chain ladder vs mlReserve, in detail; (B) A mini-benchmark on freshly simulated triangles from a simpler aggregate generator, with tunable distortions.</p>

<pre># =============================================================================
# mlReserve: machine-learning loss reserving on &quot;real-world&quot; triangles
#
# Colab: Runtime &gt; Change runtime type &gt; R
#
# Two experiments, each scored against a known true reserve:
#   A. Two triangles from an individual-claims simulator (Wang & Wüthrich),
#      embedded below: chain ladder vs mlReserve, in detail.
#   B. A mini-benchmark on freshly simulated triangles from a simpler aggregate
#      generator (defined in step 4), with tunable distortions.
# =============================================================================

# ---- 0. Setup -------------------------------------------------------------------

options(Ncpus = 2, repos = &quot;https://cloud.r-project.org&quot;,
        repr.plot.width = 12, repr.plot.height = 5.5)
for (pkg in c(&quot;remotes&quot;, &quot;ggplot2&quot;, &quot;ranger&quot;, &quot;e1071&quot;))
  if (!requireNamespace(pkg, quietly = TRUE)) install.packages(pkg)
if (!requireNamespace(&quot;ChainLadder&quot;, quietly = TRUE) ||
    !&quot;mlReserve&quot; %in% getNamespaceExports(&quot;ChainLadder&quot;))
  remotes::install_github(&quot;thierrymoudiki/ChainLadder&quot;, upgrade = &quot;never&quot;)

suppressPackageStartupMessages({
  library(ChainLadder)
  library(ggplot2)
})

n      &lt;- 10                                   # triangle size (accident years)
models &lt;- c(&quot;Chain ladder&quot;, &quot;mlReserve: SVM&quot;, &quot;mlReserve: random forest&quot;)
ink    &lt;- &quot;#0b0b0b&quot;
colours &lt;- c(&quot;Chain ladder&quot;             = &quot;#eb6834&quot;,
             &quot;mlReserve: SVM&quot;           = &quot;#2a78d6&quot;,
             &quot;mlReserve: random forest&quot; = &quot;#1baf7a&quot;,
             &quot;Truth&quot;                    = ink)
theme_set(theme_minimal(base_size = 13) +
            theme(legend.position = &quot;top&quot;, legend.title = element_blank(),
                  panel.grid.minor = element_blank(), plot.title.position = &quot;plot&quot;,
                  strip.text = element_text(face = &quot;bold&quot;)))


# ---- 1. Helpers -----------------------------------------------------------------

# A full n x n square of incremental payments -&gt; observed (cumulative) upper
# triangle + true outstanding reserve per accident year (sum of the lower triangle)
make_case &lt;- function(full) {
  dimnames(full) &lt;- list(origin = 1:n, dev = 1:n)
  future &lt;- row(full) + col(full) &gt; n + 1
  upper  &lt;- full; upper[future] &lt;- NA
  list(full      = full,
       upper     = incr2cum(as.triangle(upper)),
       true_ibnr = rowSums(full * future))
}

# Fit the three models; `uncertainty = FALSE` gives point estimates only
fit_models &lt;- function(triangle, uncertainty = TRUE, nsim = 300) {
  mse &lt;- if (uncertainty) &quot;bootstrap&quot; else &quot;none&quot;
  fits &lt;- list(
    MackChainLadder(triangle, est.sigma = &quot;Mack&quot;),
    mlReserve(triangle, e1071::svm, features = &quot;numeric&quot;, transform = &quot;asinh&quot;,
              mse.method = mse, nsim = nsim, seed = 1),
    mlReserve(triangle, ranger::ranger, num.trees = 300, num.threads = 1,
              features = &quot;both&quot;, transform = &quot;asinh&quot;,
              mse.method = mse, nsim = nsim, seed = 1))
  setNames(fits, models)
}

# Reserve (IBNR) and standard error by accident year, whatever the model
reserve_by_origin &lt;- function(fit) {
  if (inherits(fit, &quot;MackChainLadder&quot;)) {
    s &lt;- summary(fit)$ByOrigin
    data.frame(origin = 1:n, IBNR = s$IBNR, SE = s$Mack.S.E)
  } else {
    s &lt;- fit$summary[rownames(fit$summary) != &quot;total&quot;, ]
    data.frame(origin = as.integer(rownames(s)), IBNR = s$IBNR, SE = s$S.E)
  }
}
total_reserve &lt;- function(fit) sum(reserve_by_origin(fit)$IBNR)


# ---- 2. Data for experiment A ---------------------------------------------------

# Incremental paid (thousands), aggregated from the Wang &#038; Wuthrich individual
# claims simulator (github.com/actuarial-data-science/PackageIndividualClaimsSimulator):
#   &quot;Speed-up&quot;     : settlement accelerates for recent accident years (delays -45%)
#   &quot;All combined&quot; : speed-up + calendar inflation shock (2% -&gt; 15% a year from
#                    calendar month 78) + a few large, slowly paid claims
squares &lt;- list(
  &quot;Speed-up&quot; = c(4172.1, 16153.4, 14800.2, 7561.1, 3275, 1856.3, 726.7, 410.7, 35.8, 140, 4983.2, 18064.1, 13574.5, 8350.7, 3954.9, 1499, 510.3, 9.5, 0, 11.3, 4698.1, 19428.8, 14864, 7343, 3710.5, 1378.6, 384.7, 92.2, 6.8, 27.1, 5467.1, 18481, 14847.6, 8089.7, 2794.1, 1379.2, 549.5, 152.2, 6.8, 0, 5985.2, 21516.6, 15980.6, 8353, 2954.2, 1655.5, 636.4, 191.3, 211.3, 28.9, 8274.1, 22014.4, 15981.4, 7425.8, 2362.7, 731, 242.1, 41.6, 52.8, 0, 8148.4, 24712.9, 16329.1, 6587.5, 2804.9, 753.6, 49.5, 0, 0, 0, 9974.2, 27573.4, 14523, 5141.6, 1123.8, 350, 73.3, 0, 0, 0, 10698.8, 29877.8, 14303.3, 3200.3, 978.3, 239.1, 0, 0, 0, 0, 13669.1, 30541.7, 10984, 1837.5, 157.3, 0, 0, 0, 0, 0),
  &quot;All combined&quot; = c(4236.8, 16766.9, 15671.5, 8229, 3582.8, 2143.9, 838.8, 548.6, 57.6, 261.6, 5195.7, 19008.8, 15926.9, 9857.6, 4643.8, 1746.5, 712.4, 26.4, 0, 22.5, 4956.8, 20861.6, 16283.4, 8704.2, 4321.7, 1876.2, 666.5, 161.8, 14.7, 61.4, 5908.2, 20336.4, 16580.1, 9331.1, 3673.8, 2276.2, 1205.8, 332.2, 15.4, 0, 6586.1, 24045.6, 18499, 11218.3, 4576, 3174.8, 1307, 460.6, 665, 96.3, 9292, 26084.3, 21055.7, 11396.7, 4203.8, 1531.8, 585.4, 115, 163.7, 0, 9702.9, 33010, 25202.7, 12133, 6404.4, 1956.8, 385.9, 0, 0, 0, 13752.5, 42686.8, 25857.7, 10698.4, 2706.5, 3213, 4232.5, 0, 0, 0, 17094.3, 53962.4, 30132.7, 8527.6, 3087.9, 746.2, 0, 0, 0, 0, 25375.8, 63473.7, 26195.3, 5737.2, 507, 674.2, 0, 0, 0, 0))
cases &lt;- lapply(squares, function(v) make_case(matrix(v, n, n, byrow = TRUE)))
combo &lt;- cases[[&quot;All combined&quot;]]

cat(&quot;Observed cumulative paid, 'All combined' (thousands):\n&quot;)
print(round(combo$upper))
fits &lt;- suppressWarnings(fit_models(combo$upper))
cat(&quot;\nmlReserve with a random forest; true total reserve =&quot;,
    format(round(sum(combo$true_ibnr)), big.mark = &quot;,&quot;), &quot;\n&quot;)
print(fits[[&quot;mlReserve: random forest&quot;]])


# ---- 3. Experiment A: one triangle in detail ------------------------------------

# 3a. Reserve by accident year vs the truth
by_origin &lt;- do.call(rbind, lapply(models, function(m)
  data.frame(model = m, reserve_by_origin(fits[[m]]))))
by_origin &lt;- subset(by_origin, origin &gt;= 2)             # AY 1 is fully developed
by_origin$model &lt;- factor(by_origin$model, levels = models)
truth_points &lt;- data.frame(origin = 2:n, IBNR = combo$true_ibnr[2:n])

print(
  ggplot(by_origin, aes(origin, IBNR / 1000, colour = model)) +
    geom_linerange(aes(ymin = (IBNR - SE) / 1000, ymax = (IBNR + SE) / 1000),
                   position = position_dodge(0.6), linewidth = 0.9) +
    geom_point(position = position_dodge(0.6), size = 2.6) +
    geom_point(data = truth_points, aes(origin, IBNR / 1000), inherit.aes = FALSE,
               shape = 23, size = 3.4, fill = ink, colour = &quot;white&quot;) +
    scale_colour_manual(values = colours) +
    scale_x_continuous(breaks = 2:n) +
    labs(title = &quot;Reserve by accident year, +/- one standard error (black diamonds: truth)&quot;,
         x = &quot;Accident year&quot;, y = &quot;Reserve (millions)&quot;))

# 3b. Predictive distribution of the total reserve. Each model uses its own
#     resampling scheme: ODP bootstrap (BootChainLadder) for chain ladder,
#     residual bootstrap (mlReserve) for the learners.
set.seed(1)
boot &lt;- BootChainLadder(combo$upper, R = 2000, process.distr = &quot;od.pois&quot;)
draws &lt;- rbind(
  data.frame(model = models[1], total = boot$IBNR.Totals),
  data.frame(model = models[2], total = rowSums(fits[[2]]$sims.reserve.pred)),
  data.frame(model = models[3], total = rowSums(fits[[3]]$sims.reserve.pred)))
draws$model &lt;- factor(draws$model, levels = models)

print(
  ggplot(draws, aes(total / 1000, colour = model)) +
    geom_density(linewidth = 1, adjust = 1.2) +
    geom_vline(xintercept = sum(combo$true_ibnr) / 1000, colour = ink, linewidth = 0.8) +
    annotate(&quot;text&quot;, x = sum(combo$true_ibnr) / 1000, y = Inf, label = &quot; true reserve&quot;,
             hjust = 0, vjust = 1.5, colour = ink) +
    scale_colour_manual(values = colours) +
    labs(title = &quot;Model-specific predictive distributions of the total reserve&quot;,
         x = &quot;Total reserve (millions)&quot;, y = &quot;Density&quot;))

cat(&quot;\nPredictive quantiles of the total reserve (thousands):\n&quot;)
quants &lt;- t(sapply(split(draws$total, draws$model), quantile, c(0.025, 0.5, 0.975)))
print(noquote(formatC(round(quants), format = &quot;d&quot;, big.mark = &quot;,&quot;)))

# 3c. Projected cumulative payments vs the truth, accident years 7-10
cumulative_paths &lt;- function(case, label) {
  cl &lt;- MackChainLadder(case$upper, est.sigma = &quot;Mack&quot;)
  rf &lt;- mlReserve(case$upper, ranger::ranger, num.trees = 300, num.threads = 1,
                  features = &quot;both&quot;, transform = &quot;asinh&quot;, mse.method = &quot;none&quot;, seed = 1)
  projections &lt;- list(&quot;Truth&quot;                    = t(apply(case$full, 1, cumsum)),
                      &quot;Chain ladder&quot;             = unclass(cl$FullTriangle),
                      &quot;mlReserve: random forest&quot; = unclass(rf$FullTriangle))
  out &lt;- expand.grid(dev = 1:n, origin = 7:n, model = names(projections),
                     stringsAsFactors = FALSE)
  out$value &lt;- mapply(function(o, d, m) projections[[m]][o, d],
                      out$origin, out$dev, out$model)
  out$panel &lt;- paste0(label, &quot; · AY &quot;, out$origin)
  subset(out, dev &gt;= n + 1 - origin)      # from the last observed diagonal onwards
}
paths &lt;- do.call(rbind, Map(cumulative_paths, cases, names(cases)))
paths$model &lt;- factor(paths$model, levels = c(&quot;Truth&quot;, models[c(1, 3)]))
paths$panel &lt;- factor(paths$panel, levels = unique(paths$panel))

options(repr.plot.height = 7)
print(
  ggplot(paths, aes(dev, value / 1000, colour = model, linetype = model)) +
    geom_line(linewidth = 1) +
    facet_wrap(~ panel, nrow = 2, scales = &quot;free_y&quot;) +
    scale_colour_manual(values = colours) +
    scale_linetype_manual(values = c(&quot;solid&quot;, &quot;22&quot;, &quot;solid&quot;)) +
    scale_x_continuous(breaks = seq(2, n, 2)) +
    labs(title = &quot;Projected cumulative payments from the last observed diagonal&quot;,
         x = &quot;Development year&quot;, y = &quot;Cumulative paid (millions)&quot;))
options(repr.plot.height = 5.5)


# ---- 4. Experiment B: mini-benchmark on fresh triangles -------------------------

# Aggregate generator: gamma payment pattern with over-dispersed Poisson noise.
#   speedup   : mean payment delay shrinks by up to this share for recent AYs
#   inflation : calendar-year log-growth rate after calendar period 7 (2% before)
simulate_case &lt;- function(seed, speedup = 0.3, inflation = 0.08, phi = 5) {
  set.seed(seed)
  ultimate &lt;- 5e4 * (1 + 0.03 * (0:(n - 1))) * exp(rnorm(n, 0, 0.05))
  delay    &lt;- 3 * (1 - speedup * pmax(0, (1:n - 4) / (n - 4)))
  pattern  &lt;- t(sapply(delay, function(m) diff(pgamma(0:n, shape = 2, scale = m / 2))))
  calendar &lt;- outer(1:n, 1:n, &quot;+&quot;) - 1
  infl     &lt;- exp(0.02 * pmin(calendar, 7) + inflation * pmax(calendar - 7, 0))
  mean_incr &lt;- ultimate * pattern * infl
  make_case(phi * matrix(rpois(n * n, mean_incr / phi), n))
}

n_rep &lt;- 20                                   # triangles per scenario
scenarios &lt;- list(
  # &quot;Baseline (no distortion)&quot; = c(speedup = 0,   inflation = 0.02),  # control
  &quot;Settlement speed-up&quot;      = c(speedup = 0.3, inflation = 0.02),
  &quot;Speed-up + inflation&quot;     = c(speedup = 0.3, inflation = 0.08))

bench &lt;- do.call(rbind, lapply(names(scenarios), function(sc) {
  do.call(rbind, lapply(seq_len(n_rep), function(seed) {
    case &lt;- simulate_case(seed, speedup   = scenarios[[sc]][[&quot;speedup&quot;]],
                                inflation = scenarios[[sc]][[&quot;inflation&quot;]])
    point_fits &lt;- suppressWarnings(fit_models(case$upper, uncertainty = FALSE))
    data.frame(scenario = sc, seed = seed, model = models,
               error = sapply(point_fits, total_reserve) - sum(case$true_ibnr))
  }))
}))

score &lt;- aggregate(error ~ scenario + model, data = bench, FUN = function(e)
  c(bias = mean(e), RMSE = sqrt(mean(e^2)), MAE = mean(abs(e))))
score &lt;- do.call(data.frame, score)
names(score) &lt;- c(&quot;scenario&quot;, &quot;model&quot;, &quot;bias&quot;, &quot;RMSE&quot;, &quot;MAE&quot;)
score &lt;- score[order(score$scenario, score$RMSE), ]

cat(&quot;\nError of the total reserve over&quot;, n_rep, &quot;triangles per scenario (thousands):\n&quot;)
fmt &lt;- function(x) formatC(round(x), format = &quot;d&quot;, big.mark = &quot;,&quot;)
print(transform(score, bias = fmt(bias), RMSE = fmt(RMSE), MAE = fmt(MAE)),
      row.names = FALSE)

long &lt;- rbind(data.frame(score[c(&quot;scenario&quot;, &quot;model&quot;)], metric = &quot;RMSE&quot;, value = score$RMSE),
              data.frame(score[c(&quot;scenario&quot;, &quot;model&quot;)], metric = &quot;MAE&quot;,  value = score$MAE))
long$model &lt;- factor(long$model, levels = rev(models))
print(
  ggplot(long, aes(value / 1000, model, fill = model)) +
    geom_col(width = 0.6) +
    facet_grid(metric ~ scenario, scales = &quot;free_x&quot;) +
    scale_fill_manual(values = colours, guide = &quot;none&quot;) +
    labs(title = &quot;Out-of-sample error of the total reserve (lower is better)&quot;,
         x = &quot;Millions&quot;, y = NULL))

Observed cumulative paid, 'All combined' (thousands):
      dev
origin     1     2     3     4     5     6     7     8     9    10
    1   4237 21004 36675 44904 48487 50631 51470 52018 52076 52338
    2   5196 24204 40131 49989 54633 56379 57092 57118 57118    NA
    3   4957 25818 42102 50806 55128 57004 57670 57832    NA    NA
    4   5908 26245 42825 52156 55830 58106 59312    NA    NA    NA
    5   6586 30632 49131 60349 64925 68100    NA    NA    NA    NA
    6   9292 35376 56432 67829 72032    NA    NA    NA    NA    NA
    7   9703 42713 67916 80049    NA    NA    NA    NA    NA    NA
    8  13752 56439 82297    NA    NA    NA    NA    NA    NA    NA
    9  17094 71057    NA    NA    NA    NA    NA    NA    NA    NA
    10 25376    NA    NA    NA    NA    NA    NA    NA    NA    NA

mlReserve with a random forest; true total reserve = 174,050 
mlReserve (formula interface)

        Latest Dev.To.Date Ultimate   IBNR         S.E        CV
2      57118.1   0.9994925  57147.1     29    39.76128 1.3710786
3      57832.2   0.9982222  57935.2    103    91.80065 0.8912685
4      59311.6   0.9935674  59695.6    384   244.97640 0.6379594
5      68099.8   0.9732881  69968.8   1869   888.80702 0.4755522
6      72032.5   0.9286787  77564.5   5532  1605.55443 0.2902304
7      80048.6   0.8558364  93532.6  13484  2443.15573 0.1811892
8      82297.0   0.7087359 116118.0  33821  7158.48746 0.2116581
9      71056.7   0.5017785 141609.7  70553 13547.27988 0.1920156
10     25375.8   0.1881838 134845.8 109470 23912.17707 0.2184359
total 573172.3   0.7090055 808417.3 235245 37338.60673 0.1587222
</pre>

<p><img src="https://i0.wp.com/thierrymoudiki.github.io/images/2026-10-03/2026-10-03-mlreserve_0_1.png?w=578&#038;ssl=1" alt="image-title-here" class="img-responsive" data-recalc-dims="1" /></p>

<pre>Predictive quantiles of the total reserve (thousands):
                         2.5%    50%     97.5%  
Chain ladder             316,100 356,087 397,519
mlReserve: SVM           178,587 252,035 371,639
mlReserve: random forest 162,579 217,369 301,946
</pre>

<p><img src="https://i1.wp.com/thierrymoudiki.github.io/images/2026-10-03/2026-10-03-mlreserve_0_3.png?w=578&#038;ssl=1" alt="image-title-here" class="img-responsive" data-recalc-dims="1" /></p>

<pre>Error of the total reserve over 20 triangles per scenario (thousands):
             scenario                    model    bias    RMSE     MAE
  Settlement speed-up mlReserve: random forest  67,436  67,519  67,436
  Settlement speed-up             Chain ladder  94,022  94,097  94,022
  Settlement speed-up           mlReserve: SVM  93,888  94,318  93,888
 Speed-up + inflation mlReserve: random forest  58,834  58,975  58,834
 Speed-up + inflation           mlReserve: SVM  79,344  79,800  79,344
 Speed-up + inflation             Chain ladder 100,217 100,311 100,217
</pre>

<p><img src="https://i1.wp.com/thierrymoudiki.github.io/images/2026-10-03/2026-10-03-mlreserve_0_5.png?w=578&#038;ssl=1" alt="image-title-here" class="img-responsive" data-recalc-dims="1" /></p>

<p><img src="https://i2.wp.com/thierrymoudiki.github.io/images/2026-10-03/2026-10-03-mlreserve_0_6.png?w=578&#038;ssl=1" alt="image-title-here" class="img-responsive" data-recalc-dims="1" /></p>

<p>The experiment is not a universal proof that random forests are better. It is a demonstration that <code>mlReserve()</code> can be used to explore the performance of machine-learning models on loss reserving problems, and that it can outperform the classical chain ladder method in some scenarios.</p>

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://thierrymoudiki.github.io//blog/2026/10/03/r/mlreserve"> T. Moudiki's Webpage - R</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/mlreserve-machine-learning-loss-reserving-on-real-world-triangles-based-on-a-chainladder-fork/">mlreserve: machine-learning loss reserving on ‘real-world’ triangles (based on a ‘ChainLadder’ fork)</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404051</post-id>	</item>
		<item>
		<title>pipeflow: v0.4.0 on CRAN</title>
		<link>https://www.r-bloggers.com/2026/10/pipeflow-v0-4-0-on-cran/</link>
		
		<dc:creator><![CDATA[R some blog]]></dc:creator>
		<pubDate>Sat, 03 Oct 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://rpahl.github.io/r-some-blog/posts/2026-10-03-pipeflow-v0.4.0-on-cran</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; "> Pipeline setup<br />
{pipeflow} builds a pipeline by adding one R function per step with<br />
pip_add (see also the Get<br />
started article). In this post, we’ll work with the following toy<br />
pipeline:</p>
<p>step load produces a sequence<br />
step clean doubles it<br />
step fit sums it, and<br />
step report formats ...</p></div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/pipeflow-v0-4-0-on-cran/">pipeflow: v0.4.0 on CRAN</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://rpahl.github.io/r-some-blog/posts/2026-10-03-pipeflow-v0.4.0-on-cran"> R some blog</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>



<h3 id="pipeline-setup">Pipeline setup</h3>
<p>{pipeflow} builds a pipeline by adding one R function per step with
<em>pip_add</em> (see also the <a href="https://rpahl.github.io/pipeflow/articles/v01-get-started.html" rel="nofollow" target="_blank">Get
started</a> article). In this post, we’ll work with the following toy
pipeline:</p>
<ul>
<li>step <code>load</code> produces a sequence</li>
<li>step <code>clean</code> doubles it</li>
<li>step <code>fit</code> sums it, and</li>
<li>step <code>report</code> formats the result</li>
</ul>
<pre>pip &lt;- pip_new(&quot;my-pip&quot;) |&gt;
    pip_add(
        step = &quot;load&quot;,
        fun = \(n = 5) seq_len(n),
        tags = c(&quot;io&quot;, &quot;daily&quot;)
    ) |&gt;
    pip_add(
        step = &quot;clean&quot;,
        fun = \(x = ~load) x * 2,
        tags = c(&quot;io&quot;, &quot;core&quot;)
    ) |&gt;
    pip_add(
        step = &quot;fit&quot;,
        fun = \(x = ~clean) sum(x),
        tags = c(&quot;model&quot;, &quot;core&quot;)
    ) |&gt;
    pip_add(
        step = &quot;report&quot;,
        fun = \(x = ~fit) paste(&quot;result:&quot;, x),
        tags = &quot;report&quot;
    )</pre>
<aside>
<img src="https://i1.wp.com/rpahl.github.io/r-some-blog//posts/2026-10-03-pipeflow-v0.4.0-on-cran/logo.png?w=450&#038;ssl=1" alt="" data-recalc-dims="1" />
</aside>
<p>Each step must have a unique name, and function parameters can refer
to other steps — for example, <code>x = ~load</code> takes the output of
the <em>load</em> step. We also assign <code>tags</code> to each step,
which we will use to filter steps later.</p>
<p>Before we run it, let’s have a look:</p>
<pre>pip
&lt;pipeflow&gt; my-pip (4 steps)
---------------------------
     step params depends state       tags
1:   load      n           new   io,daily
2:  clean      x    load   new    io,core
3:    fit      x   clean   new model,core
4: report      x     fit   new     report
---------------------------
&lt;ready&gt; last run: never</pre>
<p>We see one row per step, showing its parameters
(<code>params</code>), the steps it depends on (<code>depends</code>),
its current <code>state</code> (all <code>new</code> before the first
run), and our <code>tags</code>. Once we run the pipeline, …</p>
<pre>pip_run(pip)
info [2026-10-03 09:36:44.793 UTC]: Starting run of pipeflow &#39;my-pip&#39;
info [2026-10-03 09:36:44.794 UTC]: Step 1/4 load
info [2026-10-03 09:36:44.795 UTC]: Step 2/4 clean
info [2026-10-03 09:36:44.797 UTC]: Step 3/4 fit
info [2026-10-03 09:36:44.799 UTC]: Step 4/4 report
info [2026-10-03 09:36:44.803 UTC]: Finished run of pipeflow &#39;my-pip&#39;
pip
&lt;pipeflow&gt; my-pip (4 steps)
---------------------------
     step params depends state            out       tags
1:   load      n          done      1,2,3,4,5   io,daily
2:  clean      x    load  done  2, 4, 6, 8,10    io,core
3:    fit      x   clean  done             30 model,core
4: report      x     fit  done     result: 30     report
---------------------------
&lt;ready&gt; last run: 2026-10-03 11:36:44</pre>
<p>… there is a new <code>out</code> column with the results, which can
also be accessed directly:</p>
<pre>pip[[&quot;report&quot;, &quot;out&quot;]]
[1] &quot;result: 30&quot;</pre>
<h3 id="pipeline-views-in-action">Pipeline views in action</h3>
<p>Real pipelines get long, and often you only care about a subset of
steps: a topic, a stage, or the steps that produce the desired outputs.
Views (new in v0.4.0) let you work on such a subset without copying
anything. They reference the underlying pipeline, so every operation
applied to it — running it, updating parameters, collecting output, … —
writes through to the original pipeline, restricted to the steps the
view covers.</p>
<h4 id="creating-views">Creating views</h4>
<p><code>pip_view()</code> returns a view containing only the steps that
match the given filters:</p>
<pre>pip_view(pip, tags = &quot;core&quot;)
&lt;pipeflow_view&gt; my-pip view (2 of 4 steps)
------------------------------------------
    step params depends state            out       tags
1: clean      x    load  done  2, 4, 6, 8,10    io,core
2:   fit      x   clean  done             30 model,core
------------------------------------------
&lt;ready&gt; last run: 2026-10-03 11:36:44
pip_view(pip, tags = &quot;core&quot;, state = &quot;done&quot;)
&lt;pipeflow_view&gt; my-pip view (2 of 4 steps)
------------------------------------------
    step params depends state            out       tags
1: clean      x    load  done  2, 4, 6, 8,10    io,core
2:   fit      x   clean  done             30 model,core
------------------------------------------
&lt;ready&gt; last run: 2026-10-03 11:36:44
pip_view(pip, step = c(&quot;clean&quot;, &quot;fit&quot;))
&lt;pipeflow_view&gt; my-pip view (2 of 4 steps)
------------------------------------------
    step params depends state            out       tags
1: clean      x    load  done  2, 4, 6, 8,10    io,core
2:   fit      x   clean  done             30 model,core
------------------------------------------
&lt;ready&gt; last run: 2026-10-03 11:36:44</pre>
<p>By default steps must match <em>all</em> filters (logical AND), while
the values within one filter are alternatives (OR).
<code>join = &quot;union&quot;</code> keeps steps that match any filter, and
<code>fixed = FALSE</code> treats filter values as regular
expressions:</p>
<pre>pip_view(pip, tags = &quot;report&quot;, step = &quot;clean&quot;, join = &quot;union&quot;)
&lt;pipeflow_view&gt; my-pip view (2 of 4 steps)
------------------------------------------
     step params depends state            out    tags
1:  clean      x    load  done  2, 4, 6, 8,10 io,core
2: report      x     fit  done     result: 30  report
------------------------------------------
&lt;ready&gt; last run: 2026-10-03 11:36:44
pip_view(pip, step = &quot;^f&quot;, fixed = FALSE)
&lt;pipeflow_view&gt; my-pip view (1 of 4 steps)
------------------------------------------
   step params depends state out       tags
1:  fit      x   clean  done  30 model,core
------------------------------------------
&lt;ready&gt; last run: 2026-10-03 11:36:44</pre>
<h4 id="selecting-steps-with">Selecting steps with <code>[</code></h4>
<p>The extract operator (also new in v0.4.0) provides a data.table-like
way of selecting steps and returns a view by default:</p>
<pre>pip[c(&quot;load&quot;, &quot;fit&quot;)]
&lt;pipeflow_view&gt; my-pip view (2 of 4 steps)
------------------------------------------
   step params depends state       out       tags
1: load      n          done 1,2,3,4,5   io,daily
2:  fit      x   clean  done        30 model,core
------------------------------------------
&lt;ready&gt; last run: 2026-10-03 11:36:44</pre>
<p>Boolean filters are evaluated in the context of the step table.</p>
<pre>pip[tags %like% &quot;core&quot;]
&lt;pipeflow_view&gt; my-pip view (2 of 4 steps)
------------------------------------------
    step params depends state            out       tags
1: clean      x    load  done  2, 4, 6, 8,10    io,core
2:   fit      x   clean  done             30 model,core
------------------------------------------
&lt;ready&gt; last run: 2026-10-03 11:36:44
pip[step %in% c(&quot;clean&quot;, &quot;fit&quot;) & state == &quot;done&quot;]
&lt;pipeflow_view&gt; my-pip view (2 of 4 steps)
------------------------------------------
    step params depends state            out       tags
1: clean      x    load  done  2, 4, 6, 8,10    io,core
2:   fit      x   clean  done             30 model,core
------------------------------------------
&lt;ready&gt; last run: 2026-10-03 11:36:44</pre>
<h4 id="running-views">Running views</h4>
<p>Running a view executes the steps it covers together with any
upstream dependencies that are not up to date. The run log marks the
view’s own steps as <code>[view]</code> and the dependencies pulled in
as <code>[upstream]</code>:</p>
<pre>pip_reset(pip)  # reset the pipeline to &quot;new&quot; state for demonstration
pip_run(pip_view(pip, step = &quot;report&quot;))
info [2026-10-03 09:36:44.883 UTC]: Starting run of pipeflow &#39;my-pip view&#39;
info [2026-10-03 09:36:44.883 UTC]: Step 1/4 [upstream] load
info [2026-10-03 09:36:44.883 UTC]: Step 2/4 [upstream] clean
info [2026-10-03 09:36:44.884 UTC]: Step 3/4 [upstream] fit
info [2026-10-03 09:36:44.885 UTC]: Step 4/4 [view] report
info [2026-10-03 09:36:44.886 UTC]: Finished run of pipeflow &#39;my-pip view&#39;</pre>
<p>Afterwards, the original pipeline is up to date for the covered
steps. Let’s collect all results related to the “core” steps:</p>
<pre>pip[tags %like% &quot;core&quot;] |&gt; pip_collect()
$clean
[1]  2  4  6  8 10

$fit
[1] 30</pre>
<p>For a complete guide on composing views, see the <a href="https://rpahl.github.io/pipeflow/articles/v03b-pipeline-views.html" rel="nofollow" target="_blank">Pipeline
views</a> vignette.</p>
<h3 id="more-new-and-updated-features">More new and updated
features</h3>
<p>Views are only one of the additions. Each of the following has its
own article on the <a href="https://rpahl.github.io/pipeflow/" rel="nofollow" target="_blank">documentation site</a>:</p>
<ul>
<li><strong>Functional and method-style API</strong> — the pipeable
<code>pip_*()</code> functions, with every function also available as a
method (<code>p$run()</code>, <code>p$view()</code>, …): <a href="https://rpahl.github.io/pipeflow/articles/v01-get-started.html" rel="nofollow" target="_blank">Get
started</a>.</li>
<li><strong>Extract and replace operators</strong> — data.table-style
<code>[</code> and <code>[[</code> for reading and editing steps: <a href="https://rpahl.github.io/pipeflow/reference/Extract.pipeflow.html" rel="nofollow" target="_blank">reference</a>.</li>
<li><strong>Collect and group output</strong> —
<code>pip_collect()</code> with grouping by tags or dependencies: <a href="https://rpahl.github.io/pipeflow/articles/v04-collect-output.html" rel="nofollow" target="_blank">Collect
and group output</a>.</li>
<li><strong>Combining and modifying pipelines</strong> — combine several
pipelines with <code>rbind()</code>, or replace, rename, remove, and
reset steps: <a href="https://rpahl.github.io/pipeflow/articles/v03a-combine-pipelines.html" rel="nofollow" target="_blank">Combine
pipelines</a> · <a href="https://rpahl.github.io/pipeflow/articles/v02-modify-pipeline.html" rel="nofollow" target="_blank">Modify
existing pipelines</a>.</li>
<li><strong>Split, map, and reduce</strong> — native
<code>exec = &quot;split&quot;</code>, <code>&quot;auto&quot;</code>, and
<code>&quot;reduce&quot;</code> modes: <a href="https://rpahl.github.io/pipeflow/articles/v05a-split-map-reduce.html" rel="nofollow" target="_blank">Split,
map, and reduce</a>.</li>
<li><strong>Nested pipelines</strong> — use a pipeline as a reusable
building block inside a step: <a href="https://rpahl.github.io/pipeflow/articles/v05b-nested-pipeline.html" rel="nofollow" target="_blank">Nested
pipelines</a>.</li>
<li><strong>Self-modifying pipelines</strong> — <code>.self</code>,
run-time restructuring, and <code>restart()</code>: <a href="https://rpahl.github.io/pipeflow/articles/v06-self-modify-pipeline.html" rel="nofollow" target="_blank">Self-modifying
pipelines</a>.</li>
</ul>
<p>Under the hood, significant performance gains have been made by
implementing the dependency graph in C++ (since v0.3.0) and by improving
the step and parameter bookkeeping in the underlying {data.table}.
Finally, by dropping <code>lgr</code> and <code>jsonlite</code>,
external package dependencies have been reduced to {data.table} and
Rcpp.</p>
<h3 id="pipeflow-vs-targets">{pipeflow} vs {targets}</h3>
<p>{targets} remains R’s de-facto standard for heavy-duty, reproducible
pipelines: it persists results to disk, records provenance, and scales
to distributed infrastructure via <code>crew</code>. {pipeflow} does not
try to compete on that home turf but targets a different niche — the
interactive session, often as the backend of a Shiny app — where a
pipeline is built and modified on the fly and has to respond while a
user waits. Since responsiveness is key in that use case, {pipeflow} has
been optimized for low latency.</p>
<p>The <a href="https://rpahl.github.io/pipeflow/articles/v07-vs-targets.html" rel="nofollow" target="_blank">pipeflow
vs targets</a> article compares the in-session latency of the two
packages head to head. The figure below gives a flavour of the
results:</p>
<p><img src="https://i2.wp.com/rpahl.github.io/r-some-blog//posts/2026-10-03-pipeflow-v0.4.0-on-cran/benchmark-work.png?w=450&#038;ssl=1" alt="Warm incremental update with 5 ms of work per step for pipeflow and targets" data-recalc-dims="1" /></p>
<p>It shows the scenario where one parameter has changed<a href="https://rpahl.github.io/r-some-blog/posts/2026-10-03-pipeflow-v0.4.0-on-cran#fn1" class="footnote-ref" id="fnref1" rel="nofollow" target="_blank"><sup>1</sup></a> and the pipeline is
rerun, with each step doing 5 ms of work. The x-axis represents the
number of steps in the pipeline (its size), and the y-axis shows the
time required to re-run it.<a href="https://rpahl.github.io/r-some-blog/posts/2026-10-03-pipeflow-v0.4.0-on-cran#fn2" class="footnote-ref" id="fnref2" rel="nofollow" target="_blank"><sup>2</sup></a></p>
<p>Across the tested sizes, {pipeflow} is consistently the faster of the
two — see the article for the full benchmark and methodology.</p>
<p>Overall, the two are best seen as complementary: reach for {targets}
for large batch jobs that must be reproducible end to end, and for
{pipeflow} when the pipeline is part of an interactive application.</p>
<h3 id="wrapping-up">Wrapping up</h3>
<p>{pipeflow} 0.4.0 is on CRAN:</p>
<pre>install.packages(&quot;pipeflow&quot;)</pre>
<p>The <a href="https://rpahl.github.io/pipeflow/news/index.html" rel="nofollow" target="_blank">full
changelog</a> lists every change, and the <a href="https://rpahl.github.io/pipeflow/" rel="nofollow" target="_blank">documentation</a> includes the
articles linked above. If you build Shiny apps or spend a lot of time
exploring parameters interactively, give it a try — feedback and issues
are welcome on <a href="https://github.com/rpahl/pipeflow/issues" rel="nofollow" target="_blank">GitHub</a>.</p>

<div class="footnotes footnotes-end-of-document">
<hr />
<ol>
<li id="fn1"><p>Imagine a user changing the plot axis in a Shiny app.<a href="https://rpahl.github.io/r-some-blog/posts/2026-10-03-pipeflow-v0.4.0-on-cran#fnref1" class="footnote-back" rel="nofollow" target="_blank"><img src="https://s.w.org/images/core/emoji/13.0.0/72x72/21a9.png" alt="↩" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎</a></p></li>
<li id="fn2"><p> That means checking which steps are affected by the
parameter change and then executing those steps.<a href="https://rpahl.github.io/r-some-blog/posts/2026-10-03-pipeflow-v0.4.0-on-cran#fnref2" class="footnote-back" rel="nofollow" target="_blank"><img src="https://s.w.org/images/core/emoji/13.0.0/72x72/21a9.png" alt="↩" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎</a></p></li>
</ol>
</div>
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://rpahl.github.io/r-some-blog/posts/2026-10-03-pipeflow-v0.4.0-on-cran"> R some blog</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/pipeflow-v0-4-0-on-cran/">pipeflow: v0.4.0 on CRAN</a>]]></content:encoded>
					
		
		<enclosure url="https://rpahl.github.io/r-some-blog/posts/2026-10-03-pipeflow-v0.4.0-on-cran/logo.png" length="0" type="image/png" />

		<post-id xmlns="com-wordpress:feed-additions:1">404047</post-id>	</item>
		<item>
		<title>Major update to VS Code R extension</title>
		<link>https://www.r-bloggers.com/2026/10/major-update-to-vs-code-r-extension/</link>
		
		<dc:creator><![CDATA[Seascapemodels]]></dc:creator>
		<pubDate>Thu, 01 Oct 2026 14:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://www.seascapemodels.org/posts/2026-10-02-major-update-to-vs-code-r-extension/</guid>

					<description><![CDATA[<p>I was doing some analysis work with R today (don’t get to do that very often anymore) and R wasn’t playing with VS Code nicely.<br />
Turns out there has recently been a major update to the VS Code R editor extension.<br />
So if you are having issues, chec...</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/major-update-to-vs-code-r-extension/">Major update to VS Code R extension</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://www.seascapemodels.org/posts/2026-10-02-major-update-to-vs-code-r-extension/"> Seascapemodels</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
 





<p>I was doing some analysis work with R today (don’t get to do that very often anymore) and R wasn’t playing with VS Code nicely.</p>
<p>Turns out there has recently been a major update to the VS Code R editor extension.</p>
<p>So if you are having issues, check out the <a href="https://github.com/REditorSupport/vscode-R" rel="nofollow" target="_blank">Extensions page for setup instructions</a>.</p>
<section id="key-changes" class="level2">
<h2 class="anchored" data-anchor-id="key-changes">Key changes:</h2>
<ul>
<li><p>New <code>jgd</code> package to improve the plotting experience. This replaces the <code>httpgd</code> package that was previously used to improve the plotting experience.</p></li>
<li><p>The <code>r.term.path</code> setting is now called <code>r.consolePath</code> (that’s the setting where you point VS Code to the R program)</p></li>
<li><p>Option to install <code>afr</code> to use as the console for R (instead of the base console or Radian).</p></li>
<li><p>It was easy to fix once I realized what was wrong. See the extension page for instructions.</p></li>
</ul>
<p>I’m not yet sure if its better yet, <code>afr</code> does seem a bit smoother and faster than <code>radian</code> was.</p>
<p>I have also been trying out Positron lately, Posit’s VS Code clone. In general that seems much easier for R based work than VS Code always has been. It plays nicely with R out of the box. Unlike VS Code, which requires lots of additional settings and extension installs.</p>
<p>I switched from RStudio to VS Code as my primary IDE a few years ago. I also teach my AI data analysis workshops in VS Code. Though there is a fair bit of time spent in each workshop just helping people connect VS Code to R. At the time Posit just weren’t keeping up with the AI coding tools at that time. But they seem to be catching up again with Positron.</p>
<p>I’ve switched my teaching to recommend Positron. So if this extension update is giving you headaches, give Positron a go.</p>


</section>

 
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://www.seascapemodels.org/posts/2026-10-02-major-update-to-vs-code-r-extension/"> Seascapemodels</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/major-update-to-vs-code-r-extension/">Major update to VS Code R extension</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404031</post-id>	</item>
		<item>
		<title>BioC2026 conference recap</title>
		<link>https://www.r-bloggers.com/2026/09/bioc2026-conference-recap/</link>
		
		<dc:creator><![CDATA[Laurah Ondari]]></dc:creator>
		<pubDate>Wed, 30 Sep 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://blog.bioconductor.org/posts/2026-09-30-BioC2026-recap/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>The North American Bioconductor Conference 2026 (([BioC2026])(https://bioc2026.bioconductor.org/)) took place from August 10-12, 2026, at the Fred Hutchinson Cancer Center in Seattle, Washington. The conference brought together researchers, dev...</p></div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/09/bioc2026-conference-recap/">BioC2026 conference recap</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://blog.bioconductor.org/posts/2026-09-30-BioC2026-recap/"> Bioconductor community blog</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
 





<p><a href="https://i0.wp.com/blog.bioconductor.org/posts/2026-09-30-BioC2026-recap/media/bioc2026_groupshot.jpg?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-1" rel="nofollow" target="_blank"><img src="https://i0.wp.com/blog.bioconductor.org/posts/2026-09-30-BioC2026-recap/media/bioc2026_groupshot.jpg?w=578&#038;ssl=1" class="zoomable img-fluid" style="width:100.0%" data-recalc-dims="1"></a></p>
<p>The North American Bioconductor Conference 2026 (([BioC2026])(https://bioc2026.bioconductor.org/)) took place from August 10-12, 2026, at the Fred Hutchinson Cancer Center in Seattle, Washington. The conference brought together researchers, developers, educators, and community members to explore current developments within and beyond the Bioconductor project. BioC2026 welcomed 108 attendees, with 70 participating in person and 38 joining virtually. Across the three days, the programme featured three keynote speakers, 20 contributed speakers, and 17 sessions. The programme highlighted the breadth of biological research supported by Bioconductor, as well as the tools, infrastructure, and communities that enable open and reproducible computational biology. The conference was then followed by two community events on August 13-14: a Carpentries workshop on orchestrating large-scale single-cell analysis with Bioconductor and the Bioconductor North America 2026 Hackathon.</p>
<p><a href="https://i1.wp.com/blog.bioconductor.org/posts/2026-09-30-BioC2026-recap/media/Closing%20Remarks%202026.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-2" rel="nofollow" target="_blank"><img src="https://i1.wp.com/blog.bioconductor.org/posts/2026-09-30-BioC2026-recap/media/Closing%20Remarks%202026.png?w=578&#038;ssl=1" class="zoomable img-fluid" style="width:100.0%" data-recalc-dims="1"></a></p>
<section id="participants-by-country" class="level2">
<h2 class="anchored" data-anchor-id="participants-by-country">Participants by country</h2>
<p>Participants travelled to Seattle from across North America and beyond, reflecting the international reach of the Bioconductor community. Attendees joined from the United States, Canada, Germany, Ireland, the United Kingdom, Australia, and Nigeria. The audience included researchers at different stages of their careers, with staff representing 42% of attendees, followed by students and postdoctoral researchers at 28%, faculty at 13.8%, industry at 13.8%, and government at 2.4%.</p>
<iframe src="https://blog.bioconductor.org/posts/2026-09-30-BioC2026-recap/media/eurobioc2026-participants-map.html" width="450" height="600" frameborder="0">
</iframe>
</section>
<section id="programme-overview" class="level2">
<h2 class="anchored" data-anchor-id="programme-overview">Programme overview</h2>
<section id="keynotes" class="level3">
<h3 class="anchored" data-anchor-id="keynotes">Keynotes</h3>
<p>The conference featured three keynote speakers: &#8211; <a href="https://www.youtube.com/watch?v=FuFSePQXVhM&#038;t=187s" rel="nofollow" target="_blank">Jeff Leek speaking on “Building Open Infrastructure for Translational AI in a Cancer Center”</a> &#8211; <a href="https://www.youtube.com/watch?v=ZG0weEXGvlA&#038;t=21s" rel="nofollow" target="_blank">Ting Ye speaking on “From Association to Causation: Genetic-Anchored Causal Inference in Human Biology”</a> &#8211; <a href="https://www.youtube.com/watch?v=_-sitxM0pNM" rel="nofollow" target="_blank">Michael Lawrence speaking on “S7 for Bioconductor”</a></p>
<div style="display:flex; gap:3px;">
<p><img src="https://i0.wp.com/blog.bioconductor.org/posts/2026-09-30-BioC2026-recap/media/leek_keynote.jpeg?w=578&#038;ssl=1" style="width:33.33%; height:200px; object-fit:cover;" data-recalc-dims="1"></p>
<p><img src="https://i2.wp.com/blog.bioconductor.org/posts/2026-09-30-BioC2026-recap/media/lawrence_keynote.jpeg?w=578&#038;ssl=1" style="width:33.33%; height:200px; object-fit:cover;" data-recalc-dims="1"></p>
<p><img src="https://i2.wp.com/blog.bioconductor.org/posts/2026-09-30-BioC2026-recap/media/ting_keynote.jpg?w=578&#038;ssl=1" style="width:33.33%; height:200px; object-fit:cover;" data-recalc-dims="1"></p>
</div>
<div style="text-align:center; font-style:italic; font-size:0.9em; margin-top:5px;">
<p>(Left to Right: Jeff Leek, Michael Lawrence and Ting Ye giving their keynotes at BioC2026)</p>
</div>
</section>
<section id="short-talks" class="level3">
<h3 class="anchored" data-anchor-id="short-talks">Short talks</h3>
<p>Twenty contributed speakers shared their work across the conference programme. The short talks at BioC2026 reflected the breadth of research and development across the Bioconductor community. Topics ranged from community training and developer engagement to multi-omics exploration, reproducible workflows, and AI infrastructure. The programme also covered emerging areas of biological data analysis, including spatial transcriptomics, antibody repertoire analysis, rare variant testing, DNA methylation, structural variation, and Perturb-seq.</p>
</section>
<section id="poster-sessions-and-package-demo-sessions" class="level3">
<h3 class="anchored" data-anchor-id="poster-sessions-and-package-demo-sessions">Poster sessions and package demo sessions</h3>
<p>The poster session showcased a diverse range of research applications and software development projects from across the Bioconductor community. Topics included single-cell and spatial transcriptomics, multi-omics integration, microbiome analysis, proteomics, antibody repertoire analysis, functional enrichment, ancestry inference, genomic data processing, and AI-guided drug discovery. The posters provided an opportunity for attendees to discuss new methods and tools in a more informal setting, while connecting researchers, package developers, and community members working across different areas of computational biology. The sessions also complemented the package demonstrations, creating space for participants to explore new software and engage directly with the people developing and applying Bioconductor tools.</p>
<p>Feedback from the conference showed that these interactions were particularly valuable, with attendees highlighting conversations around posters and package demonstrations as some of the most useful networking opportunities.</p>
<img src="https://i2.wp.com/blog.bioconductor.org/posts/2026-09-30-BioC2026-recap/media/bioc2026_poster_session.jpg?w=578&#038;ssl=1" class="zoomable img-fluid" style="width:100.0%" data-recalc-dims="1">
<center>
<p><em>Participants during the poster session.</em></p>
</center>
</section>
<section id="celebrating-25-years-of-bioconductor" class="level3">
<h3 class="anchored" data-anchor-id="celebrating-25-years-of-bioconductor">Celebrating 25 years of Bioconductor</h3>
<p>The celebrations marking Bioconductor’s 25th anniversary continued at BioC2026 following the celebrations that began at EuroBioC2026 earlier in the year. The milestone was marked with a specially branded anniversary cake, thanking participants and community members for being part of the Bioconductor community and contributing to its growth over the past 25 years.</p>
<div style="display:flex; gap:3px;">
<p><img src="https://i0.wp.com/blog.bioconductor.org/posts/2026-09-30-BioC2026-recap/media/bioc_25_cake.jpeg?w=578&#038;ssl=1" style="width:calc(50% - 1.5px); height:250px; object-fit:cover;" data-recalc-dims="1"></p>
<p><img src="https://i2.wp.com/blog.bioconductor.org/posts/2026-09-30-BioC2026-recap/media/Bioc2026_25years.png?w=578&#038;ssl=1" style="width:calc(50% - 1.5px); height:250px; object-fit:cover;" data-recalc-dims="1"></p>
</div>
<center>
<p><em>Bioconductor at 25 celebrations.</em></p>
</center>
</section>
</section>
<section id="community-and-networking" class="level2">
<h2 class="anchored" data-anchor-id="community-and-networking">Community and networking</h2>
<p>One of the strongest themes to emerge from the BioC2026 survey was the value of the Bioconductor community itself. Participants described the conference as welcoming, collaborative, and interactive, and appreciated the opportunity to reconnect with colleagues and meet new people. All respondents who answered the question said they would recommend BioC2026 to their colleagues. The smaller size of the conference was also seen as a strength. Several respondents noted that the more intimate format made it easier to engage in conversations and build connections across different roles and career stages. Networking was consistently identified as one of the most valuable aspects of the event. Participants appreciated both the formal scientific programme and the informal conversations that happened around talks, posters, and demonstrations. Looking ahead, many attendees expressed interest in even more dedicated time for networking and informal interaction.</p>
</section>
<section id="infrastructure-and-tools" class="level2">
<h2 class="anchored" data-anchor-id="infrastructure-and-tools">Infrastructure and tools</h2>
<section id="zulip" class="level3">
<h3 class="anchored" data-anchor-id="zulip">Zulip</h3>
<p>During BioC2026, Zulip provided a dedicated space for participants to connect, interact and stay engaged throughout the conference. The conference space included topic-based discussions covering: &#8211; <strong>Workshops</strong>: Dedicated topics for individual workshops, including Single Cell, Bioconductor in Workflows, microbiome, WebR, and other training sessions. &#8211; <strong>Training &#038; education</strong>: A space for broader conversations around training activities and educational sessions. &#8211; <strong>Social</strong>: Topics for informal conversations, social activities and connecting with other participants. &#8211; <strong>Introductions</strong>: Participants could introduce themselves and connect with others attending the conference. &#8211; <strong>Conference logistics channels</strong>: For travel, rooms, schedules, Day 1 information, and general conference support. &#8211; <strong>Community engagement</strong>: Dedicated spaces for the developer survey, student and early-career researcher activities, and other community initiatives. &#8211; <strong>Help and support</strong>: Dedicated topics such as the helpdesk, lost and found, and issues accessing virtual sessions.</p>
</section>
<section id="sticker-contest-winner" class="level3">
<h3 class="anchored" data-anchor-id="sticker-contest-winner">Sticker contest winner</h3>
<p><img src="https://i0.wp.com/blog.bioconductor.org/posts/2026-09-30-BioC2026-recap/media/BioC2026_sticker.png?w=578&#038;ssl=1" style="float:right; width:40%; margin-left:20px; margin-bottom:10px;" data-recalc-dims="1"></p>
<p>
Kayva Vaddadi was BioC2026’s sticker design contest winner. Kayva’s design celebrated Seattle as the host city of BioC2026, featuring well-known landmarks such as the Space Needle, the Seattle Great Wheel, Mount Rainier, and the surrounding evergreen forests. Puget Sound and a Washington State ferry reflect Seattle’s maritime landscape and the culture of connection across the region. The BioC2026 hot air balloon on the sticker was inspired by the scenic balloon views popular in the Seattle area and represents the celebratory and exploratory spirit of the Bioconductor community.
</p>
<p>
See the announcement here: <a href="https://lnkd.in/p/emAWMGX" rel="nofollow" target="_blank">https://lnkd.in/p/emAWMGX</a>
</p>
<div style="clear:both;">

</div>
</section>
</section>
<section id="conference-materials" class="level2">
<h2 class="anchored" data-anchor-id="conference-materials">Conference materials</h2>
<p>All conference recordings are available on the <a href="https://www.youtube.com/watch?v=OzpgIa2W414&#038;list=PLMYePXTcNLQo" rel="nofollow" target="_blank">Bioconductor YouTube channel</a>. BioC2026 workshops will also be available in the “Archived” section of <a href="http://workshop.bioconductor.org/" rel="nofollow" target="_blank">workshop.bioconductor.org</a>, alongside workshops from previous BioC, EuroBioC, and BiocAsia events. If you’d like to add additional materials or use the platform for your own workshop or course, contact us at <a href="http://support.bioconductor.org/" rel="nofollow" target="_blank">support.bioconductor.org</a>.</p>
</section>
<section id="post-conference-events" class="level2">
<h2 class="anchored" data-anchor-id="post-conference-events">Post-conference events</h2>
<section id="orchestrating-large-scale-single-cell-analysis-with-bioconductor" class="level3">
<h3 class="anchored" data-anchor-id="orchestrating-large-scale-single-cell-analysis-with-bioconductor">Orchestrating Large-Scale Single-Cell Analysis with Bioconductor</h3>
<p>On August 13–14, participants took part in a Carpentries workshop focused on orchestrating large-scale single-cell analysis with Bioconductor led by Ted Laderas and Wes Wilson. The workshop was attended by 11 participants and provided a hands-on environment for participants to work through approaches for analysing large-scale single-cell datasets using Bioconductor tools and workflows. Feedback highlighted the value of the live coding, small-group format and complementary teaching approaches.</p>
<div style="display:flex; gap:3px;">
<p><img src="https://i1.wp.com/blog.bioconductor.org/posts/2026-09-30-BioC2026-recap/media/carpentry-workshop1.jpeg?w=578&#038;ssl=1" style="width:calc(50% - 1.5px); height:250px; object-fit:cover;" data-recalc-dims="1"></p>
<p><img src="https://i0.wp.com/blog.bioconductor.org/posts/2026-09-30-BioC2026-recap/media/scrna_bioc2026_workshop_1.jpeg?w=578&#038;ssl=1" style="width:calc(50% - 1.5px); height:250px; object-fit:cover;" data-recalc-dims="1"></p>
</div>
<center>
<p><em>Participants during the scRNA-seq workshop.</em></p>
</center>
</section>
<section id="bioconductor-north-america-2026-hackathon" class="level3">
<h3 class="anchored" data-anchor-id="bioconductor-north-america-2026-hackathon">Bioconductor North America 2026 Hackathon</h3>
<p>The Bioconductor North America 2026 Hackathon took place on August 13–14, bringing community members together to collaborate on software and infrastructure projects that strengthen interoperability, software development, and the wider Bioconductor ecosystem. These projects included: AI harness tools for standardizing how users within organizations would interact with agents on shared projects and on shared institutional resources. Link: <a href="https://github.com/eisenefaust/harness_agent" rel="nofollow" target="_blank">https://github.com/eisenefaust/harness_agent</a></p>
<p>Bioconductor tooling for including CLI executable components in Bioconductor packages. Link: <a href="https://blog.bioconductor.org/posts/2026-08-21-biocjobs/" rel="nofollow" target="_blank">https://blog.bioconductor.org/posts/2026-08-21-biocjobs/</a></p>
<p>An exploration on benchmarking LLM generated workflows, building out infrastructure and paradigms for how we can evaluate what LLMs return as work product to scientific questions. Link: <a href="https://github.com/Amisor/LLM_BioBenchmarking/tree/main" rel="nofollow" target="_blank">https://github.com/Amisor/LLM_BioBenchmarking/tree/main</a></p>
<img src="https://i1.wp.com/blog.bioconductor.org/posts/2026-09-30-BioC2026-recap/media/bioc2026_hackathon.jpeg?w=578&#038;ssl=1" class="zoomable img-fluid" style="width:100.0%" data-recalc-dims="1">
<center>
<p><em>Participants during the Hackathon.</em></p>
</center>
</section>
</section>
<section id="up-next" class="level2">
<h2 class="anchored" data-anchor-id="up-next">Up next…</h2>
<p>Later this year, the Bioconductor community will head to Melbourne, Australia, for BioCAsia2026, taking place on November 19-20, 2026. <a href="https://biocasia2026.bioconductor.org/" rel="nofollow" target="_blank">BioCAsia2026</a> brings together researchers, developers, and data scientists from across the Asia-Pacific region and beyond to share advances in bioinformatics, computational biology, and open-source software. The meeting will highlight practical training, community building, and the latest developments in R and Bioconductor. BioC2027 will then take place in early summer in Boston, Massachusetts, continuing the North American Bioconductor conference series and bringing the community together once again for scientific exchange, collaboration, and community building. The community will thereafter gather in Basel, Switzerland, for EuroBioC2027, taking place from September 8–10, 2027. The European Bioconductor Conference will continue to bring together researchers, developers, and educators from across Europe and beyond to share new software, methods, and applications in computational biology.</p>
</section>
<section id="acknowledgements" class="level2">
<h2 class="anchored" data-anchor-id="acknowledgements">Acknowledgements</h2>
<p>BioC2026 gratefully acknowledges the support of all sponsors and partners whose contributions and support made the conference possible.</p>
<section id="sponsors" class="level3">
<h3 class="anchored" data-anchor-id="sponsors">Sponsors</h3>
<p>BioC2026 was proudly supported by Cortex, R consortium and Posit</p>
<p><a href="https://i0.wp.com/blog.bioconductor.org/posts/2026-09-30-BioC2026-recap/media/Bioc2026_sponsors.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-3" rel="nofollow" target="_blank"><img src="https://i0.wp.com/blog.bioconductor.org/posts/2026-09-30-BioC2026-recap/media/Bioc2026_sponsors.png?w=578&#038;ssl=1" class="zoomable img-fluid" style="width:100.0%" data-recalc-dims="1"></a></p>
</section>
<section id="organising-committee" class="level3">
<h3 class="anchored" data-anchor-id="organising-committee">Organising committee</h3>
<p>We thank the local organisers, programme committee, workshop instructors, keynote speakers, volunteers, sponsors, and all participants whose contributions made BioC2026 a success.</p>
<ul>
<li>Marc Carlson, Seattle Children’s Hospital</li>
<li>Nicholas Cooley, University of Limerick</li>
<li>Sean Davis, University of Colorado Cancer Center</li>
<li>Maria Doyle, University of Limerick</li>
<li>Mikhail Dozmorov, Virginia Commonwealth University</li>
<li>Jenny Drnevich, University of Illinois at Urbana-Champaign</li>
<li>Russell Eberts, Fred Hutchinson Cancer Center</li>
<li>Erica Feick, Dana-Farber Cancer Institute</li>
<li>Sana Hirata, Fred Hutchinson Cancer Center</li>
<li>Ted Laderas, Fred Hutchinson Cancer Center</li>
<li>Alexandru Mahmoud, University of Limerick</li>
<li>Matthew McCall, University of Rochester Medical Center</li>
<li>Lori (Shepherd) Kern, Roswell Park Comprehensive Cancer Center</li>
<li>Laurah Nyasita Ondari, International Institute of Tropical Agriculture (IITA)</li>
<li>Charlotte Soneson, Friedrich Miescher Institute for Biomedical Research</li>
<li>Tim Triche, Van Andel Institute</li>
<li>Levi Waldron, CUNY Graduate School of Public Health and Health Policy</li>
<li>Wes Wilson, Princess Margaret Cancer Centre</li>
</ul>


</section>
</section>

<p>
© 2026 Bioconductor. Content is published under <a href="https://creativecommons.org/licenses/by/4.0/" rel="nofollow" target="_blank">Creative Commons CC-BY-4.0 License</a> for the text and <a href="https://opensource.org/licenses/BSD-3-Clause" rel="nofollow" target="_blank">BSD 3-Clause License</a> for any code. | <a href="https://www.r-bloggers.com/" rel="nofollow" target="_blank">R-Bloggers</a>
</p> 
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://blog.bioconductor.org/posts/2026-09-30-BioC2026-recap/"> Bioconductor community blog</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/09/bioc2026-conference-recap/">BioC2026 conference recap</a>]]></content:encoded>
					
		
		<enclosure url="https://blog.bioconductor.org/posts/2026-09-30-BioC2026-recap/media/bioc2026_groupshot.jpg" length="0" type="image/jpeg" />

		<post-id xmlns="com-wordpress:feed-additions:1">404015</post-id>	</item>
		<item>
		<title>My first attempt at porting a Shiny app failed, and not for a technical reason</title>
		<link>https://www.r-bloggers.com/2026/09/my-first-attempt-at-porting-a-shiny-app-failed-and-not-for-a-technical-reason/</link>
		
		<dc:creator><![CDATA[Arthur Bréant]]></dc:creator>
		<pubDate>Tue, 29 Sep 2026 16:14:32 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://rtask.thinkr.fr/?p=30097</guid>

					<description><![CDATA[<p>You can read the original post in its original format on Rtask website by ThinkR here: My first attempt at porting a Shiny app failed, and not for a technical reason<br />
Diary of a rewrite, episode 2 of 4. Translating your code line by line is the surest way to get it ...</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/09/my-first-attempt-at-porting-a-shiny-app-failed-and-not-for-a-technical-reason/">My first attempt at porting a Shiny app failed, and not for a technical reason</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://rtask.thinkr.fr/my-first-attempt-at-porting-a-shiny-app-failed-and-not-for-a-technical-reason/"> Rtask</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
<p>You can read the original post in its original format on <a rel="nofollow" href="https://rtask.thinkr.fr/" target="_blank">Rtask</a> website by ThinkR here: <a rel="nofollow" href="https://rtask.thinkr.fr/my-first-attempt-at-porting-a-shiny-app-failed-and-not-for-a-technical-reason/" target="_blank">My first attempt at porting a Shiny app failed, and not for a technical reason</a></p>
<p><em>Diary of a rewrite, episode 2 of 4.</em></p>
<p>Translating your code line by line is the surest way to get it all wrong…</p>
<p>In <a href="https://rtask.thinkr.fr/we-chose-react-to-learn-it-the-ai-wrote-it-for-us/" rel="nofollow" target="_blank">the previous episode</a> we told the story of why we cancelled our own stack decision halfway through, and why we went back to R. If you have not read it, no worries: this one stands on its own.</p>
<p>Today we are talking about the port itself. And above all about the way I got it wrong the first time.</p>
<p>A quick bit of context. StaffPuzzle is our internal staffing tool: it says who works on what, half-day by half-day. Its Shiny version is 1,200 lines, reads the team’s Google calendars, and has been running for years. The goal was to rewrite it in <a href="https://github.com/hyperverse-r/htmxr" rel="nofollow" target="_blank">htmxr</a>, meaning HTML rendered server-side, hydrated by htmx, with plumber2 behind it.</p>
<p>Here is how I went about it.</p>
<p>I opened <code>app_server.R</code>, I listed every <code>observeEvent</code>, and I started turning them into routes. One route per observer. It is methodical, it is exhaustive… and it produces an application full of endpoints that serve no purpose.</p>
<p>A quick reminder: what is a route? In a classic web application, the browser asks for an address and the server sends back a response. A route is that pair: an address, and the function that builds what we send back. Where Shiny holds a continuous conversation with the browser for the whole visit, a route-based application answers requests that are independent of one another, with no memory of the previous one.</p>
<p>Concretely, here is the kind of translation I was doing. On the Shiny side, the observer that rebuilds the main plot whenever a filter moves (I have shortened and renamed it for readability, the original is bushier):</p>
<pre>observeEvent(c(input$refresh, rv$slots, rv$data_ready), {
  req(rv$slots, rv$consultants, rv$start, rv$end, rv$sort_by)
  rv$filtered_slots &lt;- rv$slots |&gt;
    filter(member %in% rv$consultants) |&gt;
    mark_focus(types = rv$slot_types) |&gt;
    add_weekdays()
  rv$plot &lt;- rv$filtered_slots |&gt;
    team_availability_plot(
      start   = rv$start,
      end     = rv$end,
      sort_by = rv$sort_by,
      order   = rv$consultants
    )
})</pre>
<p>And, a hundred lines further down in the same file, the output that displays it:</p>
<pre>output$plot &lt;- renderPlotly({
  rv$plot |&gt; ggplotly(tooltip = &quot;text&quot;)
})</pre>
<p>On the routes side, it becomes an address and a function:</p>
<pre>#* @get /plot
function(consultants, start, end, sort_by, slot_types) {
  slots |&gt;
    filter(member %in% consultants) |&gt;
    mark_focus(types = slot_types) |&gt;
    add_weekdays() |&gt;
    team_availability_plot(
      start   = start,
      end     = end,
      sort_by = sort_by,
      order   = consultants
    )
}</pre>
<p>The browser will ask for <code>/plot?consultants=arthur,vincent&start=2026-10-05&end=2026-11-30&sort_by=alphabetical</code>, and the server will answer with the corresponding piece of page.</p>
<p>Notice in passing what has melted away on the Shiny side: I need an observer, a slot in <code>reactiveValues</code> to store the result, and a <code>render*</code> elsewhere in the file to display it. On the route side, all that is left is a function that takes arguments and returns something. That one translates well: it produces something visible, and that something can state its address.</p>
<p>The problem is all the others.</p>
<p>My mistake did not come from not knowing the destination tool. I was translating code instead of changing paradigm. And it took me several days to notice!</p>
<p>It is a bit like moving house. You do not pack by photographing each room so you can rebuild it identically in the new flat. You look at what you actually use, and you leave the rest at the car boot sale.</p>
<p>So here is what I wish someone had told me before I started.</p>
<h2>Why inventorying the observers is the wrong entry point</h2>
<p>The reason is structural. Shiny is driven by a reactive graph, and a good share of the observers have no visible output at all. They copy a value from one place to another, they disable a button, they keep two inputs in sync. In short, they maintain the consistency of an in-memory state.</p>
<p>But that state does not exist in htmxr. Neither does the work of those observers. They do not translate: they disappear.</p>
<p>I remember the sentence I blurted out in a meeting, on 30 June, right in the middle of the port:</p>
<blockquote><p>
I feel like saying “I’ve got this or that observeEvent, so I’ve got this or that endpoint” isn’t ok. Whereas I thought starting from the data to serve is a good entry point.
</p></blockquote>
<p>It has been my working rule ever since.</p>
<h2>Start from the data being served</h2>
<p>An htmxr application is a set of addressable blocks of data. Every block on screen has a URL that renders it. The controls that drive it (filters, dates, sorting) are the parameters of that URL. We are in a hypermedia frame, no longer in a dependency graph.</p>
<p>The method comes down to 5 moves, and the order matters:</p>
<ol style="list-style-type: decimal">
<li><strong>Inventory the screens, screenshot by screenshot.</strong> Not what the code does: what the user sees.</li>
<li><strong>Cut each screen into blocks of data</strong>: header, filters, grid, detail panel.</li>
<li><strong>Give every block a URL.</strong> A block that cannot state its address is a badly cut block.</li>
<li><strong>Turn the controls into parameters.</strong> <code>?start=2026-10-01&n_weeks=8</code>, not observers.</li>
<li><strong>For each interaction, ask yourself a single question</strong>: is this a mutation of persistent business state?</li>
</ol>
<p>The fifth one is the one I kept missing. Clicking a filter, changing a sort, picking a period: those are <code>GET</code>s, even if “something changes” on screen. Nothing is modified server-side, we are simply asking for another view of the same data.</p>
<p>Tip: if your inventory produces a lot of <code>POST</code>s, chances are some <code>GET</code>s have slipped in. Go back over them.</p>
<h2>60 lines with nothing left to do</h2>
<p>Here is the example that tipped my understanding.</p>
<p>My Shiny version devoted 60 lines to keeping the URL and the inputs in sync, in both directions. Good code, by the way: that application had shareable URLs, which is not that common in Shiny.</p>
<p>On the Shiny side, 60 lines. Here are the two ends of it, abridged and renamed:</p>
<pre>## Direction 1: the URL arrives, we put the inputs in the right state
observeEvent(session$clientData$url_search, {
  params &lt;- parseQueryString(session$clientData$url_search)
  url_params$consultants &lt;- params$consultants
  url_params$slot_types  &lt;- params$types
  url_params$services    &lt;- params$services
  url_params$period      &lt;- params$period
  if (!is.null(url_params$consultants)) {
    updateSelectInput(
      session  = session,
      inputId  = &quot;consultants&quot;,
      selected = str_to_title(
        str_split_1(str_remove_all(url_params$consultants, &quot; &quot;), pattern = &quot;,&quot;)
      )
    )
  }
  ## … and the same treatment for period, slot_types and services
})
## Direction 2: an input moves, we rewrite the URL
observeEvent(
  c(input$consultants, input$slot_types, input$services,
    input$period, input$sort_by),
  ignoreInit = TRUE, {
    consultants &lt;- str_to_lower(glue_collapse(input$consultants, sep = &quot;,&quot;))
    slot_types  &lt;- str_to_lower(glue_collapse(input$slot_types, sep = &quot;,&quot;))
    period      &lt;- glue_collapse(input$period, sep = &quot;,&quot;)
    url &lt;- glue(&quot;?consultants={consultants}&#038;types={slot_types}&#038;period={period}&quot;)
    updateQueryString(session = session, url, mode = &quot;replace&quot;)
  })</pre>
<p>On the htmxr side, one argument:</p>
<pre>hx_set(
  form,
  get      = &quot;.&quot;,
  target   = &quot;#trainings-view&quot;,
  trigger  = &quot;change&quot;,
  push_url = &quot;true&quot;
)</pre>
<p>Careful, this is not a matter of concision. Those 60 lines have not been shortened: they have nothing left to do. They were maintaining a mirror between two representations of the same state, the URL on one side, the inputs on the other. When the URL <em>is</em> the state, there is simply no mirror left to hold.</p>
<p>Back to our house move: these are not furniture you carry up to the new flat. There is no room for them.</p>
<p>Want a simple test to see where you stand? If your port produces code that synchronises two things, you have kept the original paradigm. A good port deletes whole categories of code, it does not rewrite them shorter.</p>
<h2>What happens to each building block</h2>
<p>Once the frame is set, the port becomes mechanical. Let us look at what becomes of the blocks you have in front of you in your <code>server.R</code>.</p>
<p><strong>A module becomes a route and a rendering function.</strong> My biggest module was 267 lines: 4 <code>selectInput</code>, one <code>dateRangeInput</code>, 6 observers, and the URL synchronisation. All that is left is an HTML form and some parameters. The <code>ns()</code> disappears entirely, since it only existed to avoid identifier collisions between instances. With no input binding, there is nothing left to name.</p>
<p><strong><code>input$x</code> becomes an argument of your function.</strong> There is no <code>input</code> object any more. The value arrives in the request: as a URL parameter for a <code>GET</code>, in the body for a <code>POST</code>. It is the most disorienting change in the first few days, and the most liberating one afterwards. A view function receives its inputs as arguments, so it can be tested!</p>
<p><strong><code>output$x &lt;- render*()</code> becomes a function that returns HTML.</strong> The output/render pair disappears. All that is left is a function and its return value, called by a route. A <code>renderPlot</code> becomes a URL that serves the SVG, a <code>renderUI</code> a URL that serves a fragment.</p>
<p><strong><code>updateSelectInput()</code> becomes: nothing.</strong> We no longer update a control. The server renders the <code>&lt;select&gt;</code> in the state it should be in, and htmx swaps the fragment that contains it. The whole <code>update*Input()</code> family evaporates with it.</p>
<p><strong><code>invalidateLater()</code> becomes an attribute.</strong> A periodic refresh is written <code>trigger = &quot;every 15s&quot;</code>. The browser asks for the fragment again, the server recomputes it. Nothing to invalidate, since there is no state.</p>
<p><strong><code>showNotification()</code> and <code>withProgress()</code> become a fragment, or CSS.</strong> A message is a piece of HTML returned with the response. A waiting indicator is an element that htmx shows during the request and hides afterwards. A CSS class, not R code.</p>
<p><strong><code>reactiveValues()</code> and <code>session$userData</code> become the URL, or the database.</strong> In the URL if it describes what we are looking at, in the database if it has to survive closing the tab. And if nothing is left after that sorting? Then it was plumbing state, and it goes with the rest <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f609.png" alt="😉" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<h3>Seeing it run</h3>
<p>All of this stays abstract until you have it in front of you. So let us take Shiny’s canonical example, the one you get with <code>shiny::runExample(&quot;01_hello&quot;)</code>: the Old Faithful geyser histogram, with a slider for the number of bins.</p>
<ul>
<li><a href="https://hello.demo.hyperverse.world/" rel="nofollow" target="_blank">The application, written in htmxr</a>. One slider, one plot, and that is all.</li>
<li><a href="https://hello.demo.hyperverse.world/plot?bins=30" rel="nofollow" target="_blank">The URL that serves the plot</a>. Open it directly in your browser: you land on the SVG itself, the drawing spelled out. That is exactly what “give every block a URL” means.</li>
</ul>
<p>Have fun changing <code>bins=30</code> to <code>bins=5</code>, then to <code>bins=50</code>. The server recomputes and sends you back the drawing. No session, no state, no open conversation.</p>
<h3>And the code, on both sides</h3>
<p>You already know the Shiny version: it is the one from the tutorial. Here it is, stripped to the essentials.</p>
<pre>ui &lt;- page_sidebar(
  title = &quot;Hello Shiny!&quot;,
  sidebar = sidebar(
    sliderInput(&quot;bins&quot;, &quot;Number of bins:&quot;, min = 1, max = 50, value = 30)
  ),
  plotOutput(&quot;distPlot&quot;)
)
server &lt;- function(input, output) {
  output$distPlot &lt;- renderPlot({
    x    &lt;- faithful$waiting
    bins &lt;- seq(min(x), max(x), length.out = input$bins + 1)
    hist(x, breaks = bins, col = &quot;darkgray&quot;, border = &quot;white&quot;)
  })
}</pre>
<p>And the htmxr version, the one running behind the link above. I have removed the Bootstrap classes, they teach you nothing.</p>
<pre>#* @get /
#* @serializer html
function() {
  hx_page(
    hx_head(title = &quot;Old Faithful Geyser Data&quot;),
    hx_slider_input(
      id      = &quot;bins&quot;,
      label   = &quot;Number of bins:&quot;,
      value   = 30, min = 1, max = 50,
      get     = &quot;plot&quot;,
      trigger = &quot;input changed delay:300ms&quot;,
      target  = &quot;#plot&quot;
    ),
    tags$div(id = &quot;plot&quot;) |&gt;
      hx_set(get = &quot;plot&quot;, trigger = &quot;load&quot;, target = &quot;#plot&quot;)
  )
}
#* @get /plot
#* @serializer none
function(query) {
  xmlSVG({
    x    &lt;- faithful$waiting
    bins &lt;- seq(min(x), max(x), length.out = as.numeric(query$bins %||% 30) + 1)
    hist(x, breaks = bins, col = &quot;darkgray&quot;, border = &quot;white&quot;)
  }, width = 7, height = 5)
}</pre>
<p>Three things to look at, and they sum up the whole article.</p>
<p><strong>The slider is the same object.</strong> <code>sliderInput(id, label, min, max, value)</code> becomes <code>hx_slider_input(id, label, min, max, value)</code>. There is nothing to relearn.</p>
<p><strong>The plot computation has not moved by a single line.</strong> The four lines of <code>hist()</code> are identical on both sides. That is what I had not understood when I started: it is not your business code you are porting, it is the plumbing around it.</p>
<p><strong>What changes is the wiring, and it becomes visible.</strong> In Shiny, nothing says that moving the slider redraws the plot: the reactive graph works it out by itself from the presence of <code>input$bins</code> inside <code>renderPlot</code>. In htmxr, it is written on the slider itself: <code>get = &quot;plot&quot;</code> (go fetch this address), <code>target = &quot;#plot&quot;</code> (put the result there). You read the wiring instead of guessing it.</p>
<p>And <code>server &lt;- function(input, output)</code> is gone. All that is left is two functions that take arguments and return something.</p>
<h3>Try it yourself</h3>
<p>No need to copy anything: the example ships with the package.</p>
<pre># install.packages(&quot;pak&quot;)
pak::pak(&quot;hyperverse-r/htmxr&quot;)
htmxr::hx_run_example(&quot;hello&quot;)</pre>
<p>Your browser is waiting at <code>http://127.0.0.1:8080</code>. Move the slider: every move fires a request to <code>/plot</code>, and the server returns a new SVG. Open your browser’s network tab to watch them go by, it beats any explanation.</p>
<p>And if you call <code>hx_run_example()</code> with no argument, it lists the others: <code>select-input</code>, <code>multi-select</code>, <code>delete-row</code>, <code>toast-notification</code>, <code>infinity-scroll</code>, <code>page-or-fragment</code>, <code>json-endpoint</code> and <code>reactive-values</code>. That last one is worth a look if you are wondering what becomes of a <code>reactiveValues()</code> once the reactive graph is gone.</p>
<p>And since the plot is a URL, anyone can consume it. <a href="https://helloreact-mu.vercel.app/" rel="nofollow" target="_blank">Here is the same application written in React</a>, calling exactly the same R endpoint. The backend does not know who is asking, and it does not care.</p>
<p>This is the most underrated property of cutting things into addressable blocks: it does not lock you in. The day you want another frontend, or a mobile app, or simply for a colleague to pull your plot into their own tool, your R logic is already an API. It became one without you doing anything extra.</p>
<h2>What costs, and what does not</h2>
<p>Everything above is mechanical. On my port, only 3 patterns called for a real decision, and they are the ones that make an estimate vary.</p>
<table>
<colgroup>
<col width="33%" />
<col width="33%" />
<col width="33%" />
</colgroup>
<thead>
<tr class="header">
<th>Shiny pattern</th>
<th>Cost</th>
<th>Equivalent</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>selectInput</code>, <code>actionButton</code>, <code>sliderInput</code></td>
<td>low</td>
<td><code>hx_select_input()</code>, <code>hx_button()</code>, <code>hx_slider_input()</code></td>
</tr>
<tr class="even">
<td><code>dateRangeInput</code></td>
<td>low</td>
<td>no dedicated constructor: two <code>&lt;input type=&quot;date&quot;&gt;</code></td>
</tr>
<tr class="odd">
<td>URL <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/2194.png" alt="↔" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎ inputs sync</td>
<td>low</td>
<td>the <code>push_url</code> argument</td>
</tr>
<tr class="even">
<td>static <code>ggplot</code></td>
<td>low</td>
<td><code>svglite</code> + htmx swap</td>
</tr>
<tr class="odd">
<td><code>DT</code> (display)</td>
<td>low</td>
<td><code>hx_table()</code>, <code>hx_table_rows()</code></td>
</tr>
<tr class="even">
<td>long task + notifications</td>
<td>medium</td>
<td>external job + periodic polling</td>
</tr>
<tr class="odd">
<td>interactive <code>plotly</code></td>
<td><strong>high</strong></td>
<td>plotly.js, or drop the interactivity</td>
</tr>
<tr class="even">
<td><code>rhandsontable</code></td>
<td><strong>high</strong></td>
<td>JS library, or one form per cell</td>
</tr>
</tbody>
</table>
<p><strong>The editable table has no native equivalent.</strong> Either you embed a JavaScript library, or you rebuild it with HTML fields and one save per cell. The criterion is dimensional: under twenty rows or so HTML wins, at a thousand rows with multi-cell copy-paste the library becomes the right choice again.</p>
<p><strong>The interactive plot deserves a prior question</strong>, and it is precisely the one I forgot to ask myself: is it really a plot? When it is in fact a data grid, a schedule, a calendar or a heatmap, porting it as a plot is a mistake. Mine was one because Shiny made tables hard, not because the table was the right object. A real plot, on the other hand, stays an SVG served by a URL, and that is an easy case.</p>
<p><strong>An opaque internal dependency is not worked around, it is isolated.</strong> You definitely have one: the homegrown package that fetches the data from somewhere, that nobody has really read in years. The temptation is to start with it, to “understand before porting”. Do not. You will spend three days on it and you will have ported nothing.</p>
<p>Develop everything else against a demo dataset, hard-coded, and only plug the real package in at the very end, in one go. By then you will know exactly what you want from it: a function, some arguments, a table back. And if it is still incomprehensible, call it as it is without trying to rewrite it. That is not cowardice, it is what stops it from blocking the whole port.</p>
<h2>The most useful side effect</h2>
<p>Shiny encourages mixing business logic and reactive orchestration: they live in the same file, often in the same function. In my v1, the rule that decides whether a slot is highlighted was computed twice, with different rules, 15 lines apart. That was not carelessness on my part, it is what a file where business and plumbing cohabit produces.</p>
<p>The port forces the separation. A function called by a route can no longer lean on an ambient reactive context. What is left is ordinary R, which takes data and returns data or HTML. So it can be tested.</p>
<p>The tests-to-code ratio went from 0.26 to 0.77: 544 tests, no browser, a few milliseconds. With the interface having become a pure function from data to HTML, it falls into the same test harness as the rest. The Shiny equivalent needs <code>shinytest2</code>, a headless Chrome and snapshots. It is slow, it is fragile, and it breaks when a margin moves.</p>
<p>Two pieces of honesty are in order here. My new version does more than the old one (writing to calendars, draft mode, historised indicators), and part of the difference in volume comes from that, not from the paradigm. And nothing stops you from testing the business logic of a Shiny application: v1 did it over 300 lines. What changes is that the interface stops being the area you never test.</p>
<h2>What you do not get back</h2>
<p>Let us be clear: the reactive graph does render a real service when everything depends on everything. Think of a dashboard where 6 linked plots recompute together, with cross dependencies and expensive derived values. Rebuilding that with fragments and URL parameters is possible, but you are hand-writing what Shiny gives you for free.</p>
<p>Hence the only decision criterion that is worth anything, to my mind: does my application’s state fit in a URL?</p>
<p>For mine, yes: a period, some filters, a sort. The port was a good bet. For an exploration tool where the user builds a complex working session, the answer is no, and Shiny remains the right tool. That is not a polite concession, it is the same reasoning applied to a different problem.</p>
<h2>Conclusion</h2>
<p>I still write Shiny, for our clients and for ourselves. This port did not prove that one technology beats another.</p>
<p>What it did show me is that for years I had been solving a page-rendering problem with a tool designed to hold state in memory. And that the cost of that gap stayed invisible until I had paid it once.</p>
<p>So before you set off, ask yourself the URL question. It will save you the few days I lost!</p>
<p><strong>Do you have a Shiny application in this situation?</strong> We read it and give you back a note that says whether the port is worth it, including when the answer is no. <a href="https://rtask.thinkr.fr/contact/" rel="nofollow" target="_blank">Write to us</a> in two lines: the size of your application, and how long it has been running.</p>
<p>Next week, episode 3: what leaving the managed platform cost us, bill in hand.</p>
<hr />
<p><strong>Diary of a rewrite</strong>, four episodes:</p>
<ol style="list-style-type: decimal">
<li><strong>We chose React to learn it. The AI wrote it for us.</strong></li>
<li><strong>My first attempt at porting a Shiny app failed, and not for a technical reason</strong> <em>(you are here)</em></li>
<li><strong>We left the managed platform. Here is the real bill.</strong> <em>(6 October)</em></li>
<li><strong>Five signs your Shiny app has outgrown Shiny</strong> <em>(13 October)</em></li>
</ol>
<p><em>The application cited is StaffPuzzle, ThinkR’s internal staffing tool, written with htmxr, alpiner and plumber2. The figures are measured on the repository’s two branches, not estimated. Disclosure of interest: I am the author of htmxr and alpiner.</em></p>
<p>This post is better presented on its original ThinkR website here: <a rel="nofollow" href="https://rtask.thinkr.fr/my-first-attempt-at-porting-a-shiny-app-failed-and-not-for-a-technical-reason/" target="_blank">My first attempt at porting a Shiny app failed, and not for a technical reason</a></p>

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://rtask.thinkr.fr/my-first-attempt-at-porting-a-shiny-app-failed-and-not-for-a-technical-reason/"> Rtask</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/09/my-first-attempt-at-porting-a-shiny-app-failed-and-not-for-a-technical-reason/">My first attempt at porting a Shiny app failed, and not for a technical reason</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">403991</post-id>	</item>
		<item>
		<title>August 2026 Top 40 New CRAN Packages</title>
		<link>https://www.r-bloggers.com/2026/09/august-2026-top-40-new-cran-packages/</link>
		
		<dc:creator><![CDATA[Joseph Rickert]]></dc:creator>
		<pubDate>Tue, 29 Sep 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://rworks.dev/posts/august-2026-top-40-new-cran-packages/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>Three hundred twenty-nine new packages were submitted to CRAN in July. Here are my Top 40 picks in nineteen categories: Agriculture, Artificial Intelligence, Climate Studies, Computational Methods, Ecology, Epidemiology, Genomics, Machine Learni...</p></div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/09/august-2026-top-40-new-cran-packages/">August 2026 Top 40 New CRAN Packages</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://rworks.dev/posts/august-2026-top-40-new-cran-packages/"> R Works</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
 





<p>Three hundred twenty-nine new packages were submitted to CRAN in July. Here are my Top 40 picks in nineteen categories: Agriculture, Artificial Intelligence, Climate Studies, Computational Methods, Ecology, Epidemiology, Genomics, Machine Learning, Medical Statistics, Meta-Analysis, Networks, Process control, Programming, Public Transit, Statistics, Surveys, Time Series, Utilities, and Visualization.</p>
<div class="columns">
<div class="column" style="width:45%;">
<section id="agriculture" class="level3">
<h3 class="anchored" data-anchor-id="agriculture">Agriculture</h3>
<p><a href="https://cran.r-project.org/package=agridatasets" rel="nofollow" target="_blank">agridatasets</a> v0.1.1: Offers a rich and diverse collection of datasets focused on agriculture, agronomy, animal science, and related fields. The package includes experimental, observational, and field-trial data on crops such as rice, wheat, corn, soybean, cotton, coffee, avocado, and orange, as well as forestry species including bamboo, eucalyptus, and timber. Datasets cover plant breeding and genetics, factorial and randomized block experiments, herbicide and insecticide efficacy trials, pest and disease infestation, soil characteristics and land suitability, plant growth regulators, seed germination, and crop yield modeling. See the <a href="https://cran.r-project.org/web/packages/agridatasets/vignettes/agridatasets.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i1.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/agridatasets.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-1" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/agridatasets.png?w=578&#038;ssl=1" class="img-fluid" alt="Rice vs Wheat Time Series" data-recalc-dims="1"></a></p>
</section>
<section id="artificial-intelligence" class="level2">
<h2 class="anchored" data-anchor-id="artificial-intelligence">Artificial Intelligence</h2>
<p><a href="https://cran.r-project.org/package=commons" rel="nofollow" target="_blank">commons</a> v0.1.0: Implements trustworthy large language model agents. Connect raw data sources, provides a pool of trusted calculations, and a searchable context layer that demonstrates how to interpret them. Then, deploy data agents that answer questions, log interactions, and can be evaluated and improved over time. See the vignettes <a href="https://cran.r-project.org/web/packages/commons/vignettes/commons.html" rel="nofollow" target="_blank">Introduction</a> and <a href="https://cran.r-project.org/web/packages/commons/vignettes/governance.html" rel="nofollow" target="_blank">Security and governance</a>.</p>
<p><a href="https://cran.r-project.org/package=diffuseR" rel="nofollow" target="_blank">diffuseR</a> v0.2.2: A native <code>R</code> implementation of diffusion models providing a functional interface to state-of-the-art generative AI. Inspired by the <code>Python</code> library <code>diffusers</code> from <a href="https://huggingface.co/" rel="nofollow" target="_blank">Hugging Face</a>, functions generate and manipulate images from text prompts using models such as <code>Stable Diffusion</code>, with no <code>Python</code> dependency. Supports multiple diffusion schedulers and device acceleration. See the <a href="https://cran.r-project.org/web/packages/diffuseR/vignettes/performance-levers.html" rel="nofollow" target="_blank">vignette</a> and <a href="https://cran.r-project.org/web/packages/diffuseR/readme/README.html" rel="nofollow" target="_blank">README</a> for more information.</p>
<p><a href="https://i1.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/diffuseR.jpg?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-2" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/diffuseR.jpg?w=578&#038;ssl=1" class="img-fluid" alt="Sample Output" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=Rhobots" rel="nofollow" target="_blank">Rhobots</a> v0.1.10: Implements the <code>BERTopic</code> topic modeling pipeline directly in <code>R</code>: Provides transformer-based sentence embedding, uniform manifold approximation and projection dimensionality reduction, hierarchical density-based spatial clustering of applications with noise clustering, and class-based term frequency-inverse document frequency topic extraction without any dependency on <code>Python</code>, <code>conda</code>, or <code>reticulate</code>. Every stage runs in <code>R</code> through <code>torch</code>, <code>safetensors</code>, <code>tok</code>, <code>uwot</code>, and <code>dbscan</code>. The package mirrors the accessor API of the original <code>Python</code> package, adds integrated quality metrics and hyperparameter search tools, and introduces part-of-speech filtered and C-value-ranked representation models. See <a href="https://cran.r-project.org/web/packages/Rhobots/readme/README.html" rel="nofollow" target="_blank">README</a> for more information.</p>
<section id="climate-studies" class="level3">
<h3 class="anchored" data-anchor-id="climate-studies">Climate Studies</h3>
<p><a href="https://cran.r-project.org/package=topocast" rel="nofollow" target="_blank">topocast</a> v0.0.5: Downscales coarse-resolution raster data to a finer grid by fitting local linear regressions of a response, such as a climate variable, on one or more fine-resolution predictors, such as elevation and other terrain indices, within a moving window. Multiplicative and additive anomaly application downscale time series relative to a baseline climatology. Follows the regression-on-elevation approach used for high-resolution climate surfaces described in <a href="doi:10.1038/sdata.2017.122" rel="nofollow" target="_blank">Karger et al. (2017)</a>. See the vignettes <a href="https://cran.r-project.org/web/packages/topocast/vignettes/getting-started.html" rel="nofollow" target="_blank">Getting started</a> and <a href="https://cran.r-project.org/web/packages/topocast/vignettes/how-it-works.html" rel="nofollow" target="_blank">How moving-window downscaling works</a>.</p>
<p><a href="https://rworks.dev/posts/august-2026-top-40-new-cran-packages/topocast.svg" class="lightbox" data-gallery="quarto-lightbox-gallery-3" rel="nofollow" target="_blank"><img src="https://rworks.dev/posts/august-2026-top-40-new-cran-packages/topocast.svg" class="img-fluid" alt="Heat map showing the result of downscaling"></a></p>
</section>
<section id="computational-methods" class="level3">
<h3 class="anchored" data-anchor-id="computational-methods">Computational Methods</h3>
<p><a href="https://cran.r-project.org/package=openfhe.R" rel="nofollow" target="_blank">openfhe.R</a> v1.5.1.1: Provides an <code>R</code> interface to <code>penFHE</code>, the open-source <code>C++</code> library for fully homomorphic encryption <a href="https://eprint.iacr.org/2022/915" rel="nofollow" target="_blank">Badawi et al. (2022)</a>, which allows computation directly on encrypted data without access to the secret key. Supports the <a href="https://eprint.iacr.org/2012/144" rel="nofollow" target="_blank">Brakerski-Fan-Vercauteren (2012)</a>, <a href="doi:10.1145/2633600" rel="nofollow" target="_blank">Brakerski-Gentry-Vaikuntanathan (2014)</a>, and <a href="https://eprint.iacr.org/2016/421" rel="nofollow" target="_blank">Cheon-Kim-Kim-Song (2017)</a> schemes for arithmetic on encrypted numbers. There are three vignettes including <a href="https://cran.r-project.org/web/packages/openfhe.R/vignettes/introduction.html" rel="nofollow" target="_blank">Introduction</a> and <a href="https://cran.r-project.org/web/packages/openfhe.R/vignettes/ckks-bootstrapping.html" rel="nofollow" target="_blank">CKKS Bootstrapping</a>.</p>
</section>
<section id="ecology" class="level3">
<h3 class="anchored" data-anchor-id="ecology">Ecology</h3>
<p><a href="https://cran.r-project.org/package=FINN" rel="nofollow" target="_blank">FINN</a> v0.1.0: Implements a hybrid dynamic forest model that can be configured as a fully mechanistic, process-based model, like classic forest gap models, or with its demographic processes (growth, mortality, regeneration) replaced by deep neural networks, or any combination of the two. Provides functions to define a model and its mechanistic or empirical components, calibrate it to forest inventory data, and interpret the calibrated processes. FINN is implemented with the <code>torch</code> package but no knowledge of <code>torch</code> is required. The hybrid modeling approach is described in <a href="doi:10.1111/2041-210x.70347" rel="nofollow" target="_blank">Pichler and Käber (2026)</a>. There are five vignettes including <a href="https://cran.r-project.org/web/packages/FINN/vignettes/A-Introduction_to_FINN.html" rel="nofollow" target="_blank">Introduction</a> and <a href="https://cran.r-project.org/web/packages/FINN/vignettes/E-Mortality.html" rel="nofollow" target="_blank">Mortality: a binomial response and a neural-network process</a>.</p>
<p><a href="https://i1.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/FINN.jpeg?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-4" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/FINN.jpeg?w=578&#038;ssl=1" class="img-fluid" alt="Illustration of how FINN works" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=gbif.range" rel="nofollow" target="_blank">gbif.range</a> v1.9.2: Implements an end-to-end workflow to generate ecologically informed species range maps from sparse observations using environmental clustering and convex hulls. Serves as a standalone framework or complementary approach to species distribution models. By constraining estimated ranges within authoritative or custom ecoregion boundaries, the approach prevents spurious range over-prediction common in geometric hull methods. There are five vignettes including <a href="https://cran.r-project.org/web/packages/gbif.range/vignettes/getting-started.html" rel="nofollow" target="_blank">Getting Started</a> and <a href="https://cran.r-project.org/web/packages/gbif.range/vignettes/ecoregion-constrained-range-inference.html" rel="nofollow" target="_blank">Part 2: Ecoregion-Based Range Inference</a>.</p>
<p><a href="https://i2.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/gbif.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-5" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/gbif.png?w=578&#038;ssl=1" class="img-fluid" alt="Map showing 10 species classes over the European Alps, using two CHELSA bioclimatic layers" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=PhysMove" rel="nofollow" target="_blank">PhysMove</a> v1.2.5: Provides tools to analyse animal movement and space-use patterns from telemetry data using methods derived from statistical physics. Methods span displacement-based approaches, distribution fitting, space-use metrics, including the influence of correlations on space-use, network-based community detection, and measures of entropy and predictability. The package enables characterization of these patterns across spatial and temporal scales. For applications of these methods in ecological studies see <a href="doi:10.1038/s41598-017-00165-0" rel="nofollow" target="_blank">Rodríguez et al. (2017)</a> and <a href="doi:10.1073/pnas.1716137115" rel="nofollow" target="_blank">Sequeira et al. (2018)</a>. There are four vignettes including <a href="https://cran.r-project.org/web/packages/PhysMove/vignettes/pt1_introduction.html" rel="nofollow" target="_blank">Introduction</a> and <a href="https://cran.r-project.org/web/packages/PhysMove/vignettes/pt2_movement_patterns.html" rel="nofollow" target="_blank">Movement Patterns</a>.</p>
<p><a href="https://i2.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/PhysMove.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-6" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/PhysMove.png?w=578&#038;ssl=1" class="img-fluid" alt="Map showing simulated movement patterns" data-recalc-dims="1"></a></p>
</section>
<section id="epidemiology" class="level3">
<h3 class="anchored" data-anchor-id="epidemiology">Epidemiology</h3>
<p><a href="https://cran.r-project.org/package=RtForecastR" rel="nofollow" target="_blank">RtForecastR</a> v0.1.1: Provides functions for filtered (real-time/causal) and smoothed (retrospective) estimation of the time-varying effective reproduction number from case-count time series, using the EpiFilter algorithm of <a href="doi:10.1371/journal.pcbi.1009347" rel="nofollow" target="_blank">Parag (2021)</a>, together with a one-step-ahead in-sample prediction check, a genuine out-of-sample one-step forecast with predictive intervals, elimination probability <img src="https://latex.codecogs.com/png.latex?P(R_t%20%3C%201)">, and forecast calibration metrics. See the <a href="https://cran.r-project.org/web/packages/RtForecastR/vignettes/rtforecastr-walkthrough.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i1.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/RtForecastR.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-7" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/RtForecastR.png?w=578&#038;ssl=1" class="img-fluid" alt="Filtered realtime vs smoothed R_t" data-recalc-dims="1"></a></p>
</section>
<section id="genomics" class="level3">
<h3 class="anchored" data-anchor-id="genomics">Genomics</h3>
<p><a href="https://cran.r-project.org/package=diffwrap" rel="nofollow" target="_blank">diffwrap</a> v0.6-3: Provides functions for differential expression analysis of read counts from messenger RNA (mRNA) sequencing (RNA-Seq) data or micro RNA (miRNA) expression values generated by the Comprehensive Analysis Pipeline for microRNA Sequencing <code>expression_reports.sh</code> script. The workflow follows the Bioconductor <code>edgeR</code>&#8211;<code>limma</code> expression data analysis pipeline providing options for different approaches, such as pure <code>edgeR</code>, <code>voom</code> or paired samples. Methods are described in <a href="doi:10.1093/bioinformatics/btp616" rel="nofollow" target="_blank">Robinson, McCarthy and Smyth (2010)</a>, <a href="doi:10.1093/nar/gkv007" rel="nofollow" target="_blank">Ritchie et al. (2015)</a>, <a href="doi:10.1186/gb-2014-15-2-r29" rel="nofollow" target="_blank">Law et al. (2014</a> and <a href="doi:10.1186/1471-2164-15-423" rel="nofollow" target="_blank">Sun et al. (2014)</a>. See the <a href="https://rworks.dev/posts/august-2026-top-40-new-cran-packages/" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i2.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/diffwrap.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-8" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/diffwrap.png?w=578&#038;ssl=1" class="img-fluid" alt="Heat map of genes over FDR-filtered samples" data-recalc-dims="1"></a></p>
</section>
<section id="machine-learning" class="level3">
<h3 class="anchored" data-anchor-id="machine-learning">Machine Learning</h3>
<p><a href="https://cran.r-project.org/package=figsr" rel="nofollow" target="_blank">figsr</a> v0.1.1: Implements a flexible, interpretable machine learning algorithm for additive tree sums. Fits a sum of shallow classification and regression trees (CART) by greedily minimizing residual impurity, growing a new tree or deepening an existing one at each step, whichever reduces the residuals most. Supports regression and two-class classification, variable importance, bootstrap ensembling and seamless integration with <code>parsnip</code> and <code>tidymodels</code> workflows. The method is described in <a href="doi:10.1073/pnas.2310151122" rel="nofollow" target="_blank">Tan et al. (2023)</a>. See the <a href="https://cran.r-project.org/web/packages/figsr/vignettes/figsr-intro.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i2.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/figsr.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-9" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/figsr.png?w=578&#038;ssl=1" class="img-fluid" alt="Visualization of the tree sum" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=neuralsbi" rel="nofollow" target="_blank">neuralsbi</a> v0.3.2: Provides a native <code>R</code> implementation of a neural simulation-based estimator that runs on the <code>torch</code> back end and is focused on Neural Posterior Estimation. Given a prior over parameters and a simulator, functions train a conditional neural density estimator to approximate the Bayesian posterior, enabling amortized, likelihood-free inference. It targets applied researchers who want an approachable interface with sensible defaults and built-in posterior diagnostics. There are four vignettes including <a href="https://cran.r-project.org/web/packages/neuralsbi/vignettes/neuralsbi.html" rel="nofollow" target="_blank">Getting started</a> and <a href="https://cran.r-project.org/web/packages/neuralsbi/vignettes/sir-epidemic.html" rel="nofollow" target="_blank">Case study: inferring epidemic parameters (SIR)</a>.</p>
<p><a href="https://i1.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/neuralsbi.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-10" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/neuralsbi.png?w=578&#038;ssl=1" class="img-fluid" alt="Plots showing fit of multi-modal estimator" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=NBvarsel" rel="nofollow" target="_blank">NBvarsel</a> v0.1.1: Performs exhaustive or groupwise (backward elimination) variable selection for binary outcome prediction models using cross-validated net benefit as the optimization criterion. It supports predictor costs, restricted cubic splines, interaction terms, permutation importance, and parallel computation. It includes visualizations for model comparison and variable importance. References include <a href="doi:10.1177/0272989X06295361" rel="nofollow" target="_blank">Vickers &#038; Elkin (2006)</a> and <a href="doi:10.1016/j.eururo.2018.08.038" rel="nofollow" target="_blank">Van Calster et al. (2018)</a>. See the <a href="https://cran.r-project.org/web/packages/NBvarsel/vignettes/nb-varsel-tutorial.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/NBvarsel.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-11" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/NBvarsel.png?w=578&#038;ssl=1" class="img-fluid" alt="Adjusted net benefit for predictors" data-recalc-dims="1"></a></p>
</section>
<section id="medical-statistics" class="level3">
<h3 class="anchored" data-anchor-id="medical-statistics">Medical Statistics</h3>
<p><a href="https://cran.r-project.org/package=CausalState" rel="nofollow" target="_blank">CausalState</a> v0.10.2: Implements sequential doubly robust and infinite-dimensional targeted maximum likelihood estimators for longitudinal modified treatment policies in settings with transitioning states, such as ICU, ward, or emergency department care episodes. Treatment is permitted in active states and becomes structurally inapplicable after a state transition (e.g. discharge or death). Methods based on <a href="doi:10.1080/01621459.2021.1955691" rel="nofollow" target="_blank">Diaz et al. (2021)</a> and <a href="doi:10.48550/arXiv.1705.02459" rel="nofollow" target="_blank">Luedtke et al. (2017)</a>. There are three vignettes including <a href="https://cran.r-project.org/web/packages/CausalState/vignettes/getting-started.html" rel="nofollow" target="_blank">Getting started</a> and <a href="https://cran.r-project.org/web/packages/CausalState/vignettes/wb-metalearner.html" rel="nofollow" target="_blank">Wu-Benkeser density-ratio metalearner</a>.</p>
<p><a href="https://cran.r-project.org/package=orthoMTL" rel="nofollow" target="_blank">orthoMTL</a> v0.1.0: Fits regularized multi-task learning models where relationships between tasks are controlled via orthogonality or disjoint-support constraints. Supports regression, binary classification, and censored survival data. In survival mode, time-to-event outcomes are converted into binary labels at user-defined thresholds, enabling the discovery of features with time-varying effects that standard proportional-hazards models cannot detect. Implements the penalty described in <a href="https://hal.science/hal-00985654" rel="nofollow" target="_blank">Vervier et al. (2014)</a>. See the <a href="https://cran.r-project.org/package=orthoMTL" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i2.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/orthoMTL.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-12" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/orthoMTL.png?w=578&#038;ssl=1" class="img-fluid" alt="Heatmap showing coefficient weights across time thresholds" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=expoquimR" rel="nofollow" target="_blank">expoquimR</a> v0.1.0: Provides a unified toolkit for occupational chemical exposure risk assessment, implementing three internationally recognized methods: the qualitative control-banding methods COSHH Essentials (UK Health and Safety Executive) and the method of the French National Research and Safety Institute (INRS), together with the quantitative statistical procedure of the UNE-EN 689 standard for comparing measured exposure levels against occupational exposure limits. Every step of each method is implemented as a small, independently callable, and unit-tested function, so assessments are reproducible and auditable. Optional <code>shiny</code> applications provide a guided, interactive workflow. References: <a href="https://www.hse.gov.uk/coshh/essentials/index.htm" rel="nofollow" target="_blank">UK Health and Safety Executive (2003)</a> and Mallet, Pilorget and Berne (2013), ISBN:978-2-7389-2166-2. There are three vignettes including <a href="https://cran.r-project.org/web/packages/expoquimR/vignettes/coshh-essentials.html" rel="nofollow" target="_blank">COSHH Essentials: Qualitative Chemical Risk Assessment</a> and <a href="https://cran.r-project.org/web/packages/expoquimR/vignettes/inrs-method.html" rel="nofollow" target="_blank">INRS Method: Qualitative Inhalation Risk Assessment</a></p>
<p><a href="https://cran.r-project.org/package=rdborrow" rel="nofollow" target="_blank">rdborrow</a> v0.0.4.0: Implements causal inference methods for incorporating external control data into randomized controlled trials with longitudinal outcomes. Provides an analysis module supporting weighting-based methods such as inverse probability weighting and augmented inverse probability weighting, difference-in-differences. Methods are based on <a href="doi:10.1093/biostatistics/kxae012" rel="nofollow" target="_blank">Zhou et al. (2024)</a> and <a href="doi:10.1080/01621459.2024.2395586" rel="nofollow" target="_blank">Zhou et al. (2024)</a>. There are five vignettes including <a href="https://cran.r-project.org/web/packages/rdborrow/index.html" rel="nofollow" target="_blank">Introduction</a> and <a href="https://cran.r-project.org/web/packages/rdborrow/vignettes/primary_analysis_workflow.html" rel="nofollow" target="_blank">Primary Analysis Workflow</a>.</p>
</section>
<section id="meta-analysis" class="level3">
<h3 class="anchored" data-anchor-id="meta-analysis">Meta Analysis</h3>
<p><a href="https://cran.r-project.org/package=metaselection" rel="nofollow" target="_blank">metaselection</a> v0.3.0: Fits a flexible class of p-value selection models for meta-analysis and meta-regression models, providing standard errors and confidence intervals based on either cluster-robust variance estimators (i.e., sandwich estimators) or cluster-level bootstrapping to handle dependent effect size estimates, as described in <a href="doi:10.31222/osf.io/qg5x6_v1" rel="nofollow" target="_blank">Pustejovsky, Citkowicz, and Joshi (2025)</a> and <a href="doi:10.31222/osf.io/wjpxk_v1" rel="nofollow" target="_blank">Citkowicz, Pustejovsky, and Joshi (2026)</a>. Supported models include generalizations of the step-function selection model as proposed by <a href="doi:10.1007/BF02294384" rel="nofollow" target="_blank">Vevea and Hedges (1995)</a> and the beta-function selection model as proposed by <a href="doi:10.1037/met0000119" rel="nofollow" target="_blank">Citkowicz and Vevea (2017)</a>. See the <a href="https://cran.r-project.org/web/packages/metaselection/vignettes/selection-models.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i1.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/metaselection.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-13" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/metaselection.png?w=578&#038;ssl=1" class="img-fluid" alt="Distribution of one sided p-value" data-recalc-dims="1"></a></p>
</section>
</section>
</div><div class="column" style="width:10%;">

</div><div class="column" style="width:45%;">
<section id="networks" class="level3">
<h3 class="anchored" data-anchor-id="networks">Networks</h3>
<p><a href="https://cran.r-project.org/package=idiographic" rel="nofollow" target="_blank">idiographic</a> v0.3.4: Provides functions to make person-specific and within-person network estimation from intensive longitudinal and panel data. Estimators include ordinary vector autoregression (VAR), graphical vector autoregression, multilevel vector autoregression, rolling ordinary and graphical VAR, native Bayesian VAR, multilevel Bayesian VAR, unified Structural Equation Modeling, and Group Iterative Multiple Model Estimation. Methods are described in <a href="doi:10.1007/978-3-031-95365-1_20" rel="nofollow" target="_blank">Saqr et al. 2025</a> and <a href="doi:10.1080/00273171.2018.1454823" rel="nofollow" target="_blank">Epskamp et al. (2018)</a>. There are seven vignettes including an <a href="https://cran.r-project.org/web/packages/idiographic/vignettes/idiographic.html" rel="nofollow" target="_blank">introduction</a> and <a href="https://cran.r-project.org/web/packages/idiographic/vignettes/mlvar.html" rel="nofollow" target="_blank">Multilevel VAR</a>.</p>
<p><a href="https://i1.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/idiographic.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-14" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/idiographic.png?w=578&#038;ssl=1" class="img-fluid" alt="Example of within person network" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=lame" rel="nofollow" target="_blank">lame</a> v1.3.4: Implements additive and multiplicative effects models for both cross-sectional and longitudinal network analysis and supports square and rectangular network structures. Key features include: (1) Cross-sectional network analysis with support for binary, continuous, ordinal, and count data; (2) Longitudinal network analysis with additive sender/receiver and multiplicative latent-factor effects that can evolve over time through AR(1) processes’ (<a href="doi:10.1080/01621459.2014.988214" rel="nofollow" target="_blank">Sewell and Chen (2015)</a> and <a href="doi:10.1093/biomet/asu040" rel="nofollow" target="_blank">Durante and Dunson (2014)</a>); (3) Handling of changing actor compositions across time periods in longitudinal models; and (4) Performance improvements. There are seven vignettes including <a href="https://cran.r-project.org/web/packages/lame/vignettes/lame-overview.html" rel="nofollow" target="_blank">Overview</a> and <a href="https://cran.r-project.org/web/packages/lame/vignettes/lame.html" rel="nofollow" target="_blank">Getting Started</a>.</p>
<p><a href="https://i1.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/lame.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-15" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/lame.png?w=578&#038;ssl=1" class="img-fluid" alt="Circular plot showing multiplicative effects" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=tabulergm" rel="nofollow" target="_blank">tabulergm</a> v0.1.0: Creates publication-ready tables documenting exponential-family random graph models (ERGMs), a class of statistical models for social networks (<a href="doi:10.1016/j.socnet.2006.08.002" rel="nofollow" target="_blank">Robins et al. (2007)</a>). Tables describe model terms through their definitions, mathematical representations, and graphical representations, and can be generated from ERGM formulas or from models fitted with the <code>ergm</code> package (<a href="doi:10.18637/jss.v024.i03" rel="nofollow" target="_blank">Hunter et al. (2008)</a>. Resulting tables can be integrated into <code>quarto</code> and <code>rmarkdown</code> documents.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/tabulergm.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-16" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/tabulergm.png?w=578&#038;ssl=1" class="img-fluid" alt="Table with glyphs and equations" data-recalc-dims="1"></a></p>
</section>
<section id="process-control" class="level3">
<h3 class="anchored" data-anchor-id="process-control">Process Control</h3>
<p><a href="https://cran.r-project.org/package=gpci" rel="nofollow" target="_blank">gpci</a> v0.1.0: Implements a comprehensive, generalized framework for computing, estimating, and validating generalized process capability indices. Supports user-supplied probability density functions, cumulative distribution functions, survival functions, and quantile functions with uncensored data parameter estimation via Maximum Likelihood Estimation. Provides several classical and non-normal capability indices, including Cpy (<a href="doi:10.1080/16843703.2010.11673233" rel="nofollow" target="_blank">Maiti, Saha and Nanda, (2010)</a>, Spmk (<a href="doi:10.1007/s41872-019-00081-4" rel="nofollow" target="_blank">Dey and Saha (2019)</a>, CpTk (<a href="doi:10.1007/s13198-019-00789-7" rel="nofollow" target="_blank">Saha, Dey and Maiti (2019)</a> and others. Functions also compute parametric and non-parametric bootstrap confidence intervals, confidence levels using percentile, highest posterior density intervals and Heidelberger-Welch convergence diagnostics. See <a href="doi:10.1080/16843703.2010.11673233" rel="nofollow" target="_blank">Maiti, Saha and Nanda (2010)</a> and <a href="doi:10.1080/21681015.2018.1437793" rel="nofollow" target="_blank">Saha, Dey and Maiti (2018)</a> for background. See the vignettes <a href="https://cran.r-project.org/web/packages/gpci/vignettes/getting-started.html" rel="nofollow" target="_blank">Getting Started</a> and <a href="https://cran.r-project.org/web/packages/gpci/vignettes/custom-distributions.html" rel="nofollow" target="_blank">Using Custom Distributions</a>.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/gpci.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-17" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/gpci.png?w=578&#038;ssl=1" class="img-fluid" alt="Process run chart" data-recalc-dims="1"></a></p>
</section>
<section id="programming" class="level3">
<h3 class="anchored" data-anchor-id="programming">Programming</h3>
<p><a href="https://cran.r-project.org/package=polyglotSQL" rel="nofollow" target="_blank">polyglotSQL</a> v0.1.0: Provides functions to parse, tokenize, validate, format, analyze and translate SQL between more than 30 dialects (<code>PostgreSQL</code>, <code>MySQL</code>, <code>BigQuery</code>, <code>Snowflake</code>, <code>DuckDB</code>, <code>T-SQL</code>, and others) using <code>polyglot-sql</code> <a href="https://github.com/tobilg/polyglot" rel="nofollow" target="_blank">Rust crate</a>. All processing happens locally in the <code>R</code> session. Includes column-level lineage, structural query analysis, query optimization, <code>AST</code> diffing and <code>OpenLineage</code> facet generation. There are four vignettes including <a href="https://cran.r-project.org/web/packages/polyglotSQL/vignettes/getting-started.html" rel="nofollow" target="_blank">Getting Started</a> and <a href="https://cran.r-project.org/web/packages/polyglotSQL/vignettes/parsing-validation-lineage.html" rel="nofollow" target="_blank">Parsing, validation and lineage</a>.</p>
</section>
<section id="public-transit" class="level3">
<h3 class="anchored" data-anchor-id="public-transit">Public Transit</h3>
<p><a href="https://cran.r-project.org/package=transittraj" rel="nofollow" target="_blank">transittraj</a> v1.1.0: Today’s public transit vehicles produce a large amount of automatic vehicle location (AVL) data which is very useful for planning and performance studies, but can be noisy, error-prone, and sparse. This package provides tools for cleaning AVL point data and turning it into continuous, differentiable, monotonic, and invertible vehicle trajectory functions, based on the work of <a href="doi:10.48550/arXiv.2509.00119" rel="nofollow" target="_blank">Robbennolt et al. (2025)</a> and <a href="doi:10.1109/ITSC57777.2023.10422524" rel="nofollow" target="_blank">Huang et al. (2023)</a>. See the vignettes <a href="https://cran.r-project.org/web/packages/transittraj/vignettes/intro-trajectories.html" rel="nofollow" target="_blank">Introduction</a> and <a href="https://cran.r-project.org/web/packages/transittraj/vignettes/intro-trajectories.html" rel="nofollow" target="_blank">The AVL Cleaning Workflow</a>.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/transittraj.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-18" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/transittraj.png?w=578&#038;ssl=1" class="img-fluid" alt="Plot of LA E line vehicle trajectories" data-recalc-dims="1"></a></p>
</section>
<section id="statistics" class="level3">
<h3 class="anchored" data-anchor-id="statistics">Statistics</h3>
<p><a href="https://cran.r-project.org/package=chaidr" rel="nofollow" target="_blank">chaidr</a> v0.1.0: Implements the CHAID (Chi-squared Automatic Interaction Detection) decision tree algorithm of <a href="doi:10.2307/2986296" rel="nofollow" target="_blank">Kass (1980)</a> and the Exhaustive CHAID variant of <a href="doi:10.1080/02664769100000005" rel="nofollow" target="_blank">Biggs, de Ville, and Suen (1991)</a>, as specified in the <code>IBM SPSS</code> Statistics Algorithms documentation. Supports nominal, ordinal with floating missing category, and continuous predictors, and nominal, ordinal, and continuous response variables using Pearson chi-squared, Goodman row-effects, and one-way ANOVA F tests respectively. Includes prediction, rule extraction, gains and lift analysis, validation on holdout data, and visualization via base graphics, <code>Graphviz</code> DOT, <code>plotly</code>, and conversion to <code>partykit</code> objects. See the <a href="https://cran.r-project.org/web/packages/chaidr/vignettes/chaidr.html" rel="nofollow" target="_blank">Introduction</a> and the <a href="https://cran.r-project.org/web/packages/chaidr/vignettes/chaidr-ja.html" rel="nofollow" target="_blank">Japanese tutorial</a>.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/chaidr.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-19" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/chaidr.png?w=578&#038;ssl=1" class="img-fluid" alt="CHAID decision tree" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=citcdf" rel="nofollow" target="_blank">citcdf</a> v1.1.0: Enables complex hypothesis testing through conditional cumulative distribution function estimation. Method is detailed in: <a href="doi:10.1101/2021.05.21.445165" rel="nofollow" target="_blank">Gauthier et al. (2021)</a>. See the <a href="https://cran.r-project.org/web/packages/citcdf/vignettes/citcdf-userguide.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i2.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/citcdf.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-20" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/citcdf.png?w=578&#038;ssl=1" class="img-fluid" alt="Plot of p-values sorted against BH threshold" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=falsifyr" rel="nofollow" target="_blank">falsifyr</a> v1.0.0: Attacks fitted <code>R</code> model claims by searching for small plausible perturbations that make a target result disappear. The package focuses on claim-level fragility, smallest-kill reporting, and reproducible caveated robustness checks for ordinary fitted model objects. The methods draw on the fragility-index concept of <a href="doi:10.1016/j.jclinepi.2013.10.019" rel="nofollow" target="_blank">Walsh et al. (2014)</a>, multiverse analysis of <a href="doi:10.1177/1745691616658637" rel="nofollow" target="_blank">Steegen et al. (2016)</a>, specification-curve analysis of <a href="doi:10.1038/s41562-020-0912-z" rel="nofollow" target="_blank">Simonsohn et al. (2020)</a>, and robust covariance estimation of <a href="doi:10.18637/jss.v011.i10" rel="nofollow" target="_blank">Zeileis (2004)</a>. See the vignettes <a href="https://cran.r-project.org/web/packages/falsifyr/vignettes/attacking-a-regression-claim.html" rel="nofollow" target="_blank">Attacking a Regression Claim</a> and <a href="https://cran.r-project.org/web/packages/falsifyr/vignettes/interpreting-survival-scores.html" rel="nofollow" target="_blank">Interpreting Survival Scores</a>.</p>
<p><a href="https://cran.r-project.org/package=FPScausal" rel="nofollow" target="_blank">FPScausal</a> v0.1.1: Implements functional propensity score weighting for causal inference with functional treatments. The method estimates weights that balance observed confounders by removing their dependence on the functional treatment and uses a dual formulation of the weighting problem for efficient unconstrained optimization. The framework supports scalar, binary, and functional outcomes, as well as functional covariates, and can be used to estimate marginal causal effects in settings with time-varying exposures. The methodology follows <a href="doi:10.48550/arXiv.2608.03200" rel="nofollow" target="_blank">Ciardulli et al. (2026)</a>. See the <a href="https://cran.r-project.org/web/packages/FPScausal/vignettes/FPScausal.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/FPScausal.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-21" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/FPScausal.png?w=578&#038;ssl=1" class="img-fluid" alt="Plot weighted vs unweighted causal effects" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=LRErdd" rel="nofollow" target="_blank">LRErdd</a> v0.1.0: Provides functions for the design and analysis of Regression Discontinuity Designs as local randomized experiments within the potential outcome approach as formalized in <a href="doi:10.1214/15-AOAS809" rel="nofollow" target="_blank">Li, Mattei and Mealli (2015)</a> including functions to implement the design phase of the study, where the focus is on the selection of suitable subpopulations for which valid causal inference can be drawn. These functions provide summary statistics of pre- and post-treatment variables by treatment status and select suitable subpopulations around the threshold where pre-treatment variables are well balanced between treatment groups. See the <a href="https://cran.r-project.org/web/packages/LRErdd/vignettes/LRErdd.html" rel="nofollow" target="_blank">vignette</a>. There is also a <code>Shiny</code> application.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/LRErdd.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-22" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/LRErdd.png?w=578&#038;ssl=1" class="img-fluid" alt="Distribution plots for Fisher test." data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=mvboxcox" rel="nofollow" target="_blank">mvboxcox</a> v0.1.4: Fits bivariate logistic Box-Cox regression models for binary outcomes and positive continuous predictors. Transformation parameters are selected by cross-validated grid search with adaptive refinement and thin-plate spline smoothing. The package also provides prediction, empirical and sampling-weighted median effects, simulation tools, and sampling-weighted model fitting. The methodology extends the logistic Box-Cox approach of <a href="doi:10.1002/cjs.11587" rel="nofollow" target="_blank">Xing et al. (2021)</a>. See the <a href="https://cran.r-project.org/web/packages/mvboxcox/vignettes/introduction.html" rel="nofollow" target="_blank">vignette</a>.</p>
</section>
<section id="surveys" class="level3">
<h3 class="anchored" data-anchor-id="surveys">Surveys</h3>
<p><a href="https://cran.r-project.org/package=sondage" rel="nofollow" target="_blank">sondage</a> v0.9.1: Implements survey sampling algorithms for single-stage probability sampling from finite populations, written in <code>C</code>. Provides equal probability methods (simple random sampling, systematic, Bernoulli), unequal probability methods (conditional Poisson / maximum entropy, Sampford, Brewer, systematic PPS, Pareto, sequential Poisson, Poisson, Chromy’s minimum replacement, multinomial), balanced sampling via the cube method, and spatially balanced sampling via the local pivotal method and spatially correlated Poisson sampling. Functions compute joint inclusion probabilities, pairwise expectations, and sampling covariances for variance estimation. See <a href="doi:10.1007/0-387-34240-0" rel="nofollow" target="_blank">Tillé (2006)</a> for background. There are two vignettes <a href="https://cran.r-project.org/web/packages/sondage/vignettes/sondage.html" rel="nofollow" target="_blank">Getting Started</a> and <a href="https://cran.r-project.org/web/packages/sondage/vignettes/custom-methods.html" rel="nofollow" target="_blank">Extending sondage with Custom Methods</a>.</p>
</section>
<section id="time-series" class="level3">
<h3 class="anchored" data-anchor-id="time-series">Time Series</h3>
<p><a href="https://cran.r-project.org/package=scanr" rel="nofollow" target="_blank">scanr</a> v0.1.1: Detects change points in long univariate time series using the <a href="https://arxiv.org/html/2608.28110v1" rel="nofollow" target="_blank">SCAN framework</a>. The implementation uses a native <code>Rust</code> backend exposed to <code>R</code> via <code>extendr</code>. See the <a href="https://cran.r-project.org/web/packages/scanr/vignettes/scanr-introduction.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i1.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/scanr.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-23" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/scanr.png?w=578&#038;ssl=1" class="img-fluid" alt="Plot of time series with detected change points" data-recalc-dims="1"></a></p>
</section>
<section id="utilities" class="level3">
<h3 class="anchored" data-anchor-id="utilities">Utilities</h3>
<p><a href="https://cran.r-project.org/package=rmoriebricklayer" rel="nofollow" target="_blank">rmoriebricklayer</a> v0.5.1: Tools for building brick-proof, reproducible, self-contained data capsules. Resolves open-data sources through the <a href="https://ckan.org/" rel="nofollow" target="_blank">Comprehensive Knowledge Archive Network</a> <code>package_show</code>and <code>package_search</code> endpoints, records and verifies provenance with Secure Hash Algorithm 256 (SHA-256) digests and Internet Archive <a href="https://web.archive.org/" rel="nofollow" target="_blank">Wayback Machine</a> snapshots, validates downloaded data against a pinned schema. Run records are captured in a manifest plus a plain-language summary so any result can be traced back to its inputs. Tests distributional drift between a pinned capsule and a fresh fetch because a re-released extract can be statistically identical yet differ byte-for-byte. Manifests can be authenticated with keyed digests (HMAC-SHA-256, RFC 2104) or post-quantum hash-based signatures. There are eight vignettes including <a href="https://cran.r-project.org/web/packages/rmoriebricklayer/vignettes/capsules.html" rel="nofollow" target="_blank">Building reproducible data capsules</a> and <a href="https://cran.r-project.org/web/packages/rmoriebricklayer/vignettes/provenance.html" rel="nofollow" target="_blank">Provenance you can verify</a>.</p>
<p><a href="https://cran.r-project.org/package=scimesh" rel="nofollow" target="_blank">scimesh</a> v0.4.0: Implements a fast, GPU-free 3D software renderer written in modern <code>C++17</code> with native <code>R</code> bindings. Renders triangle meshes to publication-quality images entirely on the CPU, requiring no display server or graphics hardware. Features multi-light Blinn-Phong shading, screen-space ambient occlusion, anti-aliasing, depth fog, transparency, wireframe rendering, texture mapping, screen-space lines and text labels, and procedural geometry generation. Supports standard mesh file formats with PNG and PPM output. Works on high-performance computing clusters, headless servers, containers, and continuous integration pipelines, making it suitable for scientific visualization across neuro-imaging, molecular structures, and general 3D graphics. See the <a href="https://cran.r-project.org/web/packages/scimesh/vignettes/scimesh.html" rel="nofollow" target="_blank">vignette</a>.</p>
</section>
<section id="visualization" class="level3">
<h3 class="anchored" data-anchor-id="visualization">Visualization</h3>
<p><a href="https://cran.r-project.org/web/packages/dgraphs/vignettes/data-derived-graph-workflow.html" rel="nofollow" target="_blank">dgraphs</a> v0.2.0: Constructs data-derived graphs from numerical observations using mutual, shared-neighbor, intersection, geodesic, radius, adaptive-radius, and minimum-spanning-tree completion methods. Provides graph conversion, pruning, diagnostics, spectral embedding, endpoint detection, and path utilities. The implemented graph constructions include methods described by <a href="doi:10.1109/T-C.1973.223640" rel="nofollow" target="_blank">Jarvis and Patrick (1973)</a>, <a href="doi:10.1016/S0167-7152(96)00213-1" rel="nofollow" target="_blank">Brito et al. (1997)</a>, <a href="doi:10.3934/fods.2019001" rel="nofollow" target="_blank">Berry and Sauer (2019)</a>, and <a href="doi:10.2307/2346439" rel="nofollow" target="_blank">Gower and Ross (1969)</a>. See the <a href="https://cran.r-project.org/web/packages/dgraphs/vignettes/data-derived-graph-workflow.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i2.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/dgraphs.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-24" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/dgraphs.png?w=578&#038;ssl=1" class="img-fluid" alt="Continuous-kNN graph on the variable-density circular point cloud" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=gghotelling" rel="nofollow" target="_blank">gghotelling</a> v0.2.1: Calculate Hotelling’s T² ellipses and detect multivariate outliers both for base <code>R</code> plots and <code>ggplot2</code> plots. Optionally, uses robust covariance estimation to reduce the influence of outliers on the ellipses. Also included: bagplots, kernel density plots and outlier diagnostic plots. See the <a href="https://cran.r-project.org/web/packages/gghotelling/vignettes/gghotelling.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i1.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/gghotelling.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-25" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/gghotelling.png?w=578&#038;ssl=1" class="img-fluid" alt="Plot of Hotelling ellipses with minimum covariance determinant estimator" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=ggmultiglyph" rel="nofollow" target="_blank">ggmultiglyph</a> v0.1.0: Provides <code>ggplot2</code> geoms for visualizing multivariate data using glyphs. The package implements several established glyph designs described in the information visualization literature, including the review by <a href="doi:10.2312/conf/EG2013/stars/039-063" rel="nofollow" target="_blank">Borgo et al. (2013)</a>.</p>
<p><a href="https://i2.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/ggmultiglyph.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-26" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/ggmultiglyph.png?w=578&#038;ssl=1" class="img-fluid" alt="Glyphs" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=grip" rel="nofollow" target="_blank">grip</a> v0.2.0: Implements GRIP multiscale graph layout with a unified choice between hop-count and geometry-aware edge-length graph metrics in 2D and 3D. Provides layout scoring, candidate comparison, multiscale trace diagnostics, synthetic graph families, and advanced experimental geodesic-KK utilities for weighted-layout evaluation and polish. Based on <a href="doi:10.7155/jgaa.00052" rel="nofollow" target="_blank">Gajer and Kobourov (2002)</a> and <a href="doi:10.1016/j.comgeo.2004.03.014" rel="nofollow" target="_blank">Gajer, Goodrich and Kobourov (2004)</a>. There are four vignettes including <a href="https://cran.r-project.org/web/packages/grip/vignettes/grip-examples.html" rel="nofollow" target="_blank">Getting Started</a> and <a href="https://cran.r-project.org/web/packages/grip/vignettes/weighted-grip-intro.html" rel="nofollow" target="_blank">Weighted Graph Layouts</a>.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/grip.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-27" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/grip.png?w=578&#038;ssl=1" class="img-fluid" alt="Plots of large graph weighted patterns" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=orbis" rel="nofollow" target="_blank">orbis</a> v0.1.0: Implements a layered grammar of graphics that compiles plots to a resolution-independent scene description and renders it through two back-ends: a self-contained SVG writer with embedded <code>JavaScript</code> for interactive figures (tooltips, hover highlighting, zoom, pan and legend toggling) and <code>R</code>’s own graphics devices for publication-quality output at any resolution. Geographic layers are first class. The layered grammar follows <a href="doi:10.1198/jcgs.2009.07098" rel="nofollow" target="_blank">Wickham (2010)</a>; projections follow <a href="doi:10.3133/pp1395" rel="nofollow" target="_blank">Snyder (1987)</a> and, for Equal Earth, <a href="doi:10.1080/13658816.2018.1504949" rel="nofollow" target="_blank">Savric, Patterson and Jenny (2019)</a>; line simplification uses <a href="doi:10.3138/FM57-6770-U75U-7727" rel="nofollow" target="_blank">Douglas and Peucker (1973)</a>; the default colour scales follow the guidance on perceptually uniform palettes of <a href="doi:10.1038/s41467-020-19160-7" rel="nofollow" target="_blank">Crameri, Shephard and Heron (2020)</a>. See the <a href="https://cran.r-project.org/web/packages/orbis/vignettes/orbis.html" rel="nofollow" target="_blank">vignette</a></p>
<p><a href="https://i1.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/orbis.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-28" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/august-2026-top-40-new-cran-packages/orbis.png?w=578&#038;ssl=1" class="img-fluid" alt="World choropleth: Robinson projection" data-recalc-dims="1"></a></p>
</section>
</div>
</div>



 
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://rworks.dev/posts/august-2026-top-40-new-cran-packages/"> R Works</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/09/august-2026-top-40-new-cran-packages/">August 2026 Top 40 New CRAN Packages</a>]]></content:encoded>
					
		
		<enclosure url="https://rworks.dev/posts/august-2026-top-40-new-cran-packages/gbif.png" length="0" type="image/png" />

		<post-id xmlns="com-wordpress:feed-additions:1">403988</post-id>	</item>
		<item>
		<title>Celebrating the rOpenSci Community</title>
		<link>https://www.r-bloggers.com/2026/09/celebrating-the-ropensci-community/</link>
		
		<dc:creator><![CDATA[rOpenSci]]></dc:creator>
		<pubDate>Tue, 29 Sep 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://ropensci.org/blog/2026/09/29/celebrating-ropensci/</guid>

					<description><![CDATA[<p>Read it in: Español. rOpenSci turns 15 and I’ve been a part of this community for a good chunk of that time. As I reflected on those years, a lot of great memories came to mind, and I realized some of them taught me some lessons that I would ...</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/09/celebrating-the-ropensci-community/">Celebrating the rOpenSci Community</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://ropensci.org/blog/2026/09/29/celebrating-ropensci/"> rOpenSci - open tools for open science</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>

<p><a href='https://ropensci.org/es/blog/2026/09/29/celebrando-ropensci/' rel="nofollow" target="_blank">Read it in: Español</a>.</p> <p>rOpenSci turns 15 and I’ve been a part of this community for a good chunk of that time. As I reflected on those years, a lot of great memories came to mind, and I realized some of them taught me some lessons that I would love to share.</p>
<h2>
Stretch your comfort zone
</h2><p>Early in 2018 I came across <a href="https://ropensci.org/blog/2018/02/08/unconf2018/" rel="nofollow" target="_blank">this post</a>:</p>
<blockquote>
<p><strong>Apply to attend rOpenSci unconf 2018!</strong><br>
We’re organizing unconf18 to bring together scientists, developers, and open data enthusiasts from academia, industry, government, and non-profits to get together for a couple of days to hack on various projects and generally enrich our community.</p>
</blockquote>
<p>In hindsight I was a good fit. I had recently transitioned from academia to my first job as a research software engineer. And yet, did I think I was a solid candidate? Oh no, I had lots of doubts.</p>
<p>But I know the greatest things often happen a little stretch outside your comfort zone. So I applied anyway, got invited, and spent two days collaborating with <a href="https://unconf18.ropensci.org/#participants" rel="nofollow" target="_blank">an incredible bunch of people</a>. Some of them were already rockstars, many others eventually became influential in their own niche, and all of them had this great attitude that at the time I lacked words to describe. I now do: they were respectful and kind.</p>
<h2>
Be respectful and kind
</h2><p>In 2022 <a href="https://ropensci.org/blog/2022/09/22/launch-champions-program/" rel="nofollow" target="_blank">rOpenSci launched the first cohort of the rOpenSci Champions Program</a>. By then I already had a few years of experience as an associate editor of <a href="https://ropensci.org/software-review/" rel="nofollow" target="_blank">rOpenSci software peer-review</a>, and <a href="https://ropensci.org/author/yanina-bellini-saibene/" rel="nofollow" target="_blank">Yani</a> invited me to talk to our champions about the process.</p>
<p>As I prepared <a href="https://ropensci-training.github.io/software-review/en/" rel="nofollow" target="_blank">that talk</a> and discussed some ideas with Yani, she made me realize how much the rOpenSci community cares about the way we communicate with one another<sup id="fnref:1"><a href="https://ropensci.org/blog/2026/09/29/celebrating-ropensci/#fn:1" class="footnote-ref" role="doc-noteref" rel="nofollow" target="_blank">1</a></sup>. So much so that our guides for <a href="https://devguide.ropensci.org/softwarereview_reviewer.html" rel="nofollow" target="_blank">reviewers</a> and <a href="https://devguide.ropensci.org/softwarereview_editor.html" rel="nofollow" target="_blank">editors</a> open with this message:</p>
<blockquote>
<p>rOpenSci’s community is our best asset. We aim for reviews to be open, non-adversarial, and focused on improving software quality. Be respectful and kind! See our reviewers’ guide and <a href="https://ropensci.org/code-of-conduct/" rel="nofollow" target="_blank">code of conduct</a> for more.</p>
</blockquote>
<p>Coming from such a deeply technical guide, that opening might surprise you. But it makes sense to me now; broken code is way easier to fix than broken human relationships.</p>
<h2>
Contribute in your own way
</h2><p>At rOpenSci you can contribute in so many ways. We have a <a href="https://contributing.ropensci.org/" rel="nofollow" target="_blank">community contributing guide</a> but here’s a list of some of my own contributions:</p>
<ul>
<li>Attend <a href="https://ropensci.org/community/" rel="nofollow" target="_blank">events</a>.</li>
<li>Help to welcome and onboard new members.</li>
<li>Ask or answer questions on Slack, or share or discuss ideas or jobs.</li>
<li>Improve our documentation, e.g. fix a typo.</li>
<li>Write a <a href="https://ropensci.org/blog/" rel="nofollow" target="_blank">blog</a> post.</li>
<li><a href="https://ropensci.org/multilingual-publishing/" rel="nofollow" target="_blank">Translate</a> or review some work in your native language.</li>
<li>Pilot a new process.</li>
<li>Author, review, or edit an R package.</li>
<li>Lead a workshop.</li>
<li>Build a tool to enhance some process.</li>
<li>Give and get support, e.g. mentor, review grant applications or talks, advice or recommendations for jobs, help unavailable or overwhelmed people.</li>
</ul>
<p>As you can see, most of my contributions did not involve any code, and only a few required an invitation. Some of them are the kind of thing you could add to your CV, and the most important ones only belong in your heart.</p>
<h2>
Attract great people
</h2><p>Great communities are made of great people, and you can play an active role in growing it in the direction you want. If you know someone that would fit in and enjoy the rOpenSci community, you can try to <a href="https://contributing.ropensci.org/" rel="nofollow" target="_blank">find a way to bring them in</a>. For example, I’ve encouraged and helped people to submit and review packages. Also as an editor I have the occasional privilege to nominate other editors. Using this super-power I’ve attracted two <a href="https://ropensci.org/software-review/" rel="nofollow" target="_blank">editors</a> and I now get to enjoy interacting with them quite regularly.</p>
<h2>
Keep the human in your human connections
</h2><p>AI is changing the way we contribute to open source software. But we don’t know much about its effect on the open communities around that software. Earlier this year rOpenSci published a <a href="https://ropensci.org/blog/2026/02/26/ropensci-ai-policy/" rel="nofollow" target="_blank">preliminary set of policies</a>, and you may want to watch rOpenSci’s <a href="https://ropensci.org/news/" rel="nofollow" target="_blank">newsletter</a> and this <a href="https://openscapes.org/events/2026-10-01-community-call-open-communities-ai/" rel="nofollow" target="_blank">Openscapes community call</a>:</p>
<blockquote>
<p><strong>Open Communities in the Age of AI</strong><br>
{Open communities} help us connect on a human level around science and data and the things that we are passionate about. They change careers, they change lives. (…) But now, people are not necessarily finding their communities and getting the benefit of those communities in the way that they used to, because they’re more easily able to get help and answers by using AI. (…) These connections and communities are more important than ever, as we need to find ways to support each other and ourselves as AI rapidly changes the landscape right under our feet.</p>
</blockquote>
<p>Personally, over the past year I’ve used AI heavily and learned several lessons. The most relevant here is that I don’t want AI to impersonate me or the other person in a <a href="https://contributing.ropensci.org/motivations.html#connect" rel="nofollow" target="_blank">human-to-human connection</a>. The <a href="https://contributing.ropensci.org/intro.html#humans" rel="nofollow" target="_blank">humans of rOpenSci</a> are real and wonderful people. Here are some that I’ve recently met in person:</p>
<div class="row">
<div class="col-md-6">
<figure><img src="https://i0.wp.com/ropensci.org/blog/2026/09/29/celebrating-ropensci/2024_boston_zci_yani-noam-mauro.JPG?w=578&#038;ssl=1"
alt="Noam Ross, Yanina Bellini Saibene, and Mauro Lepore at the 2024 CZI meeting in Boston, USA" data-recalc-dims="1"><figcaption>
<p><a href='https://www.linkedin.com/in/noamross/' rel="nofollow" target="_blank">Noam Ross</a>, <a href='https://www.linkedin.com/in/yabellini/' rel="nofollow" target="_blank">Yanina Bellini Saibene</a> and <a href='https://www.linkedin.com/in/mauro-lepore/' rel="nofollow" target="_blank">me</a> at the 2024 <a href='https://chanzuckerberg.com/' rel="nofollow" target="_blank">CZI</a> meeting in Boston, USA.</p>
</figcaption>
</figure>
</div>
<div class="col-md-6">
<figure><img src="https://i0.wp.com/ropensci.org/blog/2026/09/29/celebrating-ropensci/2024_seattle_posit-conf_monica-stefanie-sean-julia-mauro-kelly.png?w=578&#038;ssl=1"
alt="Monica Gerber, Stefanie Butland, Sean Kross, Julia Stewart Lowndes, Kelly O&#39;Briant, and Mauro Lepore at the 2024 Posit conference in Seattle, USA" data-recalc-dims="1"><figcaption>
<p><a href='https://www.linkedin.com/in/monica-gerber/' rel="nofollow" target="_blank">Monica Gerber</a>, <a href='https://www.linkedin.com/in/stefaniebutland/' rel="nofollow" target="_blank">Stefanie Butland</a>, <a href='https://www.linkedin.com/in/seankross/' rel="nofollow" target="_blank">Sean Kross</a>, <a href='https://www.linkedin.com/in/julia-stewart-lowndes/' rel="nofollow" target="_blank">Julia Stewart Lowndes</a>, <a href='https://www.linkedin.com/in/kellyobriant/' rel="nofollow" target="_blank">Kelly O’Briant</a> and me at the 2024 Posit conference in Seattle, USA.</p>
</figcaption>
</figure>
</div>
</div>
<div class="row">
<div class="col-md-6">
<figure><img src="https://i0.wp.com/ropensci.org/blog/2026/09/29/celebrating-ropensci/2025_atlanta_posit-conf_luis-diana-mauro.jpg?w=578&#038;ssl=1"
alt="Luis D. Verde Arregoitia, Diana Garcia Cortes, and Mauro Lepore at the Georgia Aquarium after the 2025 Posit conference in Atlanta, USA" data-recalc-dims="1"><figcaption>
<p><a href='https://www.linkedin.com/in/luis-d-verde-arregoitia-a20339209/' rel="nofollow" target="_blank">Luis D. Verde Arregoitia</a>, <a href='https://www.linkedin.com/in/ddiannae/' rel="nofollow" target="_blank">Diana Garcia Cortes</a> and me at the Georgia Aquarium after the 2025 Posit conference in Atlanta, USA.</p>
</figcaption>
</figure>
</div>
<div class="col-md-6">
<figure><img src="https://i1.wp.com/ropensci.org/blog/2026/09/29/celebrating-ropensci/2025_san-jose_costa-rica_ronny.jpg?w=578&#038;ssl=1"
alt="Ronny A. Hernández Mora and Mauro Lepore at Ronny&#39;s family gathering near San José, Costa Rica in 2025" data-recalc-dims="1"><figcaption>
<p><a href='https://www.linkedin.com/in/ronny-hernandez-mora/' rel="nofollow" target="_blank">Ronny A. Hernández Mora</a> and me at Ronny’s family gathering near San José, Costa Rica in 2025.</p>
</figcaption>
</figure>
</div>
</div>
<div class="row">
<div class="col-md-6">
<figure><img src="https://i1.wp.com/ropensci.org/blog/2026/09/29/celebrating-ropensci/2025_sao-pablo_brasil_bea-mauro.jpg?w=578&#038;ssl=1"
alt="Beatriz Milz and Mauro Lepore after coffee and pastries in São Paulo, Brazil in 2025" data-recalc-dims="1"><figcaption>
<p><a href='https://www.linkedin.com/in/beatrizmilz/' rel="nofollow" target="_blank">Beatriz Milz</a> and me after coffee and pastries in São Paulo, Brazil in 2025.</p>
</figcaption>
</figure>
</div>
</div>
<p>Thanks for joining me in celebrating rOpenSci. And if, like me, you <a href="https://contributing.ropensci.org/intro.html#community" rel="nofollow" target="_blank">self-identify with our community</a>, then happy birthday to you!</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Two books I like are <a href="https://openlibrary.org/books/OL31981288M/How_to_Win_Friends_and_Influence_People" rel="nofollow" target="_blank">How to Win Friends and Influence People</a> and <a href="https://openlibrary.org/works/OL282391W/Crucial_Conversations" rel="nofollow" target="_blank">Crucial Conversations (Third Edition): Tools for Talking When Stakes Are High</a>. <a href="https://ropensci.org/blog/2026/09/29/celebrating-ropensci/#fnref:1" class="footnote-backref" role="doc-backlink" rel="nofollow" target="_blank"><img src="https://s.w.org/images/core/emoji/13.0.0/72x72/21a9.png" alt="↩" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎</a></p>
</li>
</ol>
</div>
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://ropensci.org/blog/2026/09/29/celebrating-ropensci/"> rOpenSci - open tools for open science</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/09/celebrating-the-ropensci-community/">Celebrating the rOpenSci Community</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">403978</post-id>	</item>
		<item>
		<title>X Reasons for policy professionals to get into programming in 202X</title>
		<link>https://www.r-bloggers.com/2026/09/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/</link>
		
		<dc:creator><![CDATA[Giles]]></dc:creator>
		<pubDate>Mon, 28 Sep 2026 10:06:06 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://www.gilesd-j.com/?p=4701</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; "> In 2019 I wrote a post with seven reasons policy professionals should learn to code. Now that AI can write reasonable code, I’ve had a number of people ask me if it’s still worth learning to program. Based on the evidence I’ve seen, AI appears to be a ...</div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/09/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/">X Reasons for policy professionals to get into programming in 202X</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/"> Data Analytics and AI Archives - Giles</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>

<p class="wp-block-paragraph"><strong>TLDR:</strong> In 2019 I wrote a post with seven reasons policy professionals should learn to code. Now that AI can write <em>reasonable</em> code<em>,</em> I’ve had a number of people ask me if it’s still worth learning to program. Based on the evidence I’ve seen, AI appears to be a an amplifier, rather than a substitute for human intelligence in the policy space: meaning the people that get the most out of AI are likely to be those that know how to instruct AI to tackle a problem and tell good output from bad. So my answer is yes, learning to code still makes sense. Not necessarily so you can write the code yourself, but so you can make AI a force multiplier for your work.</p>



<p class="wp-block-paragraph"><strong>Note: </strong>I’m intending to make updates and refinements to this post over time: both to correct the typos that I haven’t picked up and to incorporate feedback from other coders in the policy community. Feel free to <a href="https://www.gilesd-j.com/contact/" rel="nofollow" target="_blank">reach out if you have suggestions</a>.  </p>



<p class="wp-block-paragraph"><strong>How AI was used</strong>: I wrote the first draft of this post. AI was used (minimally) to refine how some ideas were communicated. </p>



<h3 class="wp-block-heading">Background</h3>



<p class="wp-block-paragraph">In 2019 I wrote <a href="https://www.gilesd-j.com/2019/01/07/7-reasons-for-policy-professionals-to-get-pumped-about-r-programming-in-2019/" rel="nofollow" target="_blank">a listicle outlining why people working on public policy should learn how to program</a>.</p>



<p class="wp-block-paragraph">Putting aside that <s>R</s> <em>python</em> was probably the answer to which language to learn, the point of the post was to share some of the practical benefits I’d seen as an applied economist and policy advisor from learning to code.</p>



<p class="wp-block-paragraph">Despite the listicle being one of my lower effort posts, it seemed to strike a chord: my inbox was quickly flooded with messages thanking me for articulating the benefits others in the public policy community had seen from leveraging code in their work. To my surprise, the article was also used as a reference for several university programming courses, resulting in my inbox still receiving messages from people starting their coding journey.</p>



<p class="wp-block-paragraph">But a lot has changed since I first wrote the article: the UK voted to leave the European Union; the World Health Organization declared a global pandemic; and 25 movies in the Marvel Cinematic universe were released.</p>



<p class="wp-block-paragraph">Oh! And a quaint lil’ startup called OpenAI launched something called ChatGPT.</p>



<p class="wp-block-paragraph">And while there’s certainly a lot that could be said about the unfolding Marvel Universe’s implication for learning to code, this will have to be a subject of a future post. As today, I’d like to attempt to answer a question I’ve been increasingly asked since Artificial Intelligence (AI) tools and/or Large Language Models (LLMs) have made producing code trivial.</p>



<h3 class="wp-block-heading">It’s sunk costs all the way down</h3>



<p class="wp-block-paragraph">At the outset, I’m clearly not an impartial observer. I was motivated enough to teach myself R, to write the original post and create a dedicated MOOC on using R for policy analysis. I also <em>enjoy</em> coding, so I have a vested interest in answering <em>yes</em> to the question. But, in my defence, these reasons also mean that I’ve thought about it a lot. I’m also a frequent user of AI in my work and find it has greatly expanded the scope and scale of what I can achieve.</p>



<p class="wp-block-paragraph">This post is therefore my attempt to provide an answer to the question <em>Is it still worth learning to code in 202X?</em> My answer is, yes, but for different reasons to the 2019 listicle.</p>



<figure class="wp-block-image aligncenter size-full"><img loading="lazy" decoding="async" loading="lazy" src="https://i1.wp.com/www.gilesd-j.com/wp-content/uploads/2026/09/image-11.png?w=450&#038;ssl=1" alt="" class="wp-image-4704" srcset_temp="https://i1.wp.com/www.gilesd-j.com/wp-content/uploads/2026/09/image-11.png?w=450&#038;ssl=1 634w, https://www.gilesd-j.com/wp-content/uploads/2026/09/image-11-300x256.png 300w" sizes="auto, (max-width: 634px) 100vw, 634px" data-recalc-dims="1" /><figcaption class="wp-element-caption"><strong>Source: </strong>rogierK @ Twitter (post no longer available)</figcaption></figure>



<h3 class="wp-block-heading">Doing smart stuff quickly</h3>



<p class="wp-block-paragraph">What got me excited about learning to program when I first started, was its ability to expand the scope and scale of analysis I could do and questions I could answer. In the world of public policy, there’s an almost unlimited number of interesting questions that could be asked, but rarely the time and data to answer them. Sometimes this is simply because the data didn’t exist, which makes learning to code not particularly helpful. However, often there are cases where data <em>does</em> exist, just not in a format that can be readily analysed with the commercial tools made available to you:</p>



<ul class="wp-block-list">
<li class="">Want to import thousands of individual excel files and merge them into a single dataframe?</li>



<li class="">Have a repetitive data cleaning task that would take hours to do using a spreadsheet?</li>



<li class="">Want to apply a statistical technique that hasn’t been added to your stats software?</li>
</ul>



<p class="wp-block-paragraph">Programming languages makes solving these problems trivial, but with the right prompt, so can AI.</p>



<h3 class="wp-block-heading">Probability-based slop machines</h3>



<p class="wp-block-paragraph">But let’s be clear: at their core, LLMs are probabilistic slop machines. I don’t mean <em>slop</em> in the <a href="https://en.wikipedia.org/wiki/AI_slop" rel="nofollow" target="_blank">pejorative sense</a>, but as a palpable description of what LLMs do: slap together <em>intelligent looking</em> responses based on <a href="https://ai.stackexchange.com/questions/50233/is-next-token-prediction-sufficient-to-explain-emergent-capabilities-like-comple" rel="nofollow" target="_blank">next-token prediction</a>.<a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftn1" id="_ftnref1" rel="nofollow" target="_blank">[1]</a> I say <em>intelligent looking<a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftn2" id="_ftnref2" rel="nofollow" target="_blank"><strong>[2]</strong></a></em> as LLMs have been trained to produce outputs that look intelligent, rather than to exhibit intelligence in a human sense:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><em>“…AI systems have demonstrated their capability to solve cognitive ability test problems, primarily through guided training (e.g., Zhuo &#038; Kankanhalli, 2020) or programmed approaches to transform problems into algorithmically solvable formats (e.g., Schmidhuber, 2004). While remarkable, it is debatable whether these accomplishments signify intelligence, given that the capabilities of most current AI systems are limited to specific programming and/or training data, without the necessary demonstration of novel problem-solving ability characteristic of human intelligence (Davidson &#038; Downing, 2000; Raaheim &#038; Brun, 1985). Consequently, many AI systems might be more aptly recognised as having the capacity to exhibit artificial achievement or artificial expertise…”</em></p>
<cite><em>Gignac, G.E. and Szodorai, E.T., 2024. Defining intelligence: Bridging the gap between human and artificial perspectives. Intelligence, 104, p.101832 (</em><a href="https://www.sciencedirect.com/science/article/pii/S0160289624000266#article" rel="nofollow" target="_blank"><em>link</em></a><em>).</em></cite></blockquote>



<p class="wp-block-paragraph">But sometimes a little bit of AI slop can be helpful. When producing code, I’ll often get AI to produce the first draft before making tweaks to the approach and style.<a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftn3" id="_ftnref3" rel="nofollow" target="_blank">[3]</a> I use AI to help with writing and research. For this post I asked AI for good search terms to find research exploring the cognitive and economic benefits of learning to code, so I could check my assumptions and think clearly about what has changed since I first wrote the post. I then asked AI for ideas to better communicate key ideas presented in this post.  </p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><em>“The majority of human production has always been slop. Mediocrity is not a bug of technology; it is the baseline of culture. The canon of art we revere today – those few thousands works in museums and textbooks – is the surviving tip of an immense iceberg of forgotten, derivative, or simply boring creations…”</em></p>
<cite>D’Isa, Francesco (1 December 2025). “The Idea of ‘AI Slop’ Is Slop”. The Philosophical Salon (<a href="https://thephilosophicalsalon.com/the-idea-of-ai-slop-is-slop/" rel="nofollow" target="_blank">link</a>)</cite></blockquote>



<p class="wp-block-paragraph">If you’re wondering why I’m waxing lyrical about intelligence and the mechanics of LLMs, it’s because I think they help decide two things: which problems AI is likely to be good at solving and whether (or when) it’s worth learning to code. After all, if LLMs can already code better and faster than you, why bother learning how to do it yourself? Particularly if the type of <em>intelligence</em> AI outputs can mimic have sufficient crossover with our own to make it practically indistinguishable in the realm of applied economics and public policy.</p>



<h3 class="wp-block-heading">Widening the scope, scale and quality(?) of analysis</h3>



<p class="wp-block-paragraph">Looking back at <a href="https://www.gilesd-j.com/2019/01/07/7-reasons-for-policy-professionals-to-get-pumped-about-r-programming-in-2019/" rel="nofollow" target="_blank">my original post</a>, almost all the cited benefits come from programming languages changing the scope, scale and quality of what you can produce as an applied policy analyst. And if I had to put a number on it: of the seven reasons cited in the listicle, six can be achieved without having to write the code yourself.<a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftn4" id="_ftnref4" rel="nofollow" target="_blank">[4]</a> Have some messy data that needs to be cleaned and reshaped before analysis? Share a sample with AI and let it write the code to take care of the data cleaning for you. Have research question you want answered? LLMs can suggest alternative statistical approaches you might not have thought of and write the code to implement them. Need to produce a set of publication ready plots? AI can write the code to do this too in a format that tells the story of your choice.</p>



<p class="wp-block-paragraph">It would seem to be game over for learning to code and my 2019 post.</p>



<p class="wp-block-paragraph">Of course, you already know that I don’t believe this but let me tell you why…</p>



<h3 class="wp-block-heading">The jagged frontier of AI-assisted public policy analysis</h3>



<p class="wp-block-paragraph">Firstly, whether AI is <em>intelligent</em> or merely produces outputs that make it appear to be, early evidence suggests its ability to substitute for <em>human</em> intelligence varies across tasks. Researchers that tested the productivity benefits of AI termed this as a <em>jagged frontier,</em> that the users had to carefully navigate to benefit from using AI in their work.<a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftn5" id="_ftnref5" rel="nofollow" target="_blank">[5]</a> It’s also not clear whether improving model performance is likely to expand the frontier equally across tasks, with many of the benchmark scores not necessarily generalizing outside test questions and being fragile to irrelevant and/or incorrect context.<a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftn6" id="_ftnref6" rel="nofollow" target="_blank">[6]</a> Finally, at the time of writing this post, the types of tasks that frontier AI models had the hardest time completing successfully, looked strikingly similar to the problems encountered in public policy: messy, interdependent and heavily reliant on context.<a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftn7" id="_ftnref7" rel="nofollow" target="_blank">[7]</a></p>



<figure class="wp-block-image aligncenter size-full"><img loading="lazy" decoding="async" loading="lazy" src="https://i2.wp.com/www.gilesd-j.com/wp-content/uploads/2026/09/image-13.png?w=450&#038;ssl=1" alt="" class="wp-image-4709" srcset_temp="https://i2.wp.com/www.gilesd-j.com/wp-content/uploads/2026/09/image-13.png?w=450&#038;ssl=1 501w, https://www.gilesd-j.com/wp-content/uploads/2026/09/image-13-295x300.png 295w" sizes="auto, (max-width: 501px) 100vw, 501px" data-recalc-dims="1" /><figcaption class="wp-element-caption"><strong>Source: </strong>Ethan Mollick, Centaurs and Cyborgs on the Jagged Frontier, One Useful Thing, <a href="https://www.oneusefulthing.org/p/centaurs-and-cyborgs-on-the-jagged" rel="nofollow" target="_blank">link</a></figcaption></figure>



<p class="wp-block-paragraph">Secondly, while there’s solid evidence suggesting that AI <em>can</em> lead to productivity gains for knowledge workers, this isn’t automatic and appears to lean on the taste, judgement and experience of the user. <a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftn8" id="_ftnref8" rel="nofollow" target="_blank">[8]</a> This was demonstrated by a field experiment in Kenya,<a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftn9" id="_ftnref9" rel="nofollow" target="_blank">[9]</a> where a little over 600 entrepreneurs were randomly provided access to an AI-powered business assistant. The performance of those with the assistant was then tracked over time and compared to those without the AI assistant. The result: top performers gained around 15 percent from having access to AI, while weaker ones lost around 10. Entrepreneurs that benefited the most from AI appeared to be better at identifying and acting on contextually appropriate advice, while poor performers tended to adopt generic strategies proposed by the tool.</p>



<p class="wp-block-paragraph">A much smaller experimental study closer to the world of coding pointed to a similar idea,  with both the 2025 (n=16) and 2026 (n=57) replication suggesting the productivity returns to using AI are ambiguous <em>and</em> vary based on the experience of the user.<a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftn10" id="_ftnref10" rel="nofollow" target="_blank">[10]</a> Both versions of the study took the same basic form: experienced developers were randomly assigned to work with AI or not on their own repositories and the performance benefits were compared between groups. In the 2025 study, the authors found the performance benefits were <em>on average</em> negative: experienced developers took longer to complete the same tasks and overestimated the time saved.  In the 2026 replication,<a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftn11" id="_ftnref11" rel="nofollow" target="_blank">[11]</a> the authors found performance increased on average, but not for all developers.<a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftn12" id="_ftnref12" rel="nofollow" target="_blank">[12]</a> Although the authors note their estimates are likely biased downwards due to difficulties in recruiting developers willing not to use AI, they paint the same basic picture: the benefits of using AI are not unambiguously positive and depend on the skills and experience of the user.<a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftn13" id="_ftnref13" rel="nofollow" target="_blank">[13]</a>  </p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">“…The right analogy for AI is not humans, but an alien intelligence with a distinct set of capabilities and limitations. Just because it exceeds human ability at one task doesn’t mean it can do all related work at human level. Although AIs and humans can perform some similar tasks, the underlying “cognitive” processes are fundamentally different…”</p>
<cite>Mollick, E. (2024, May 12). Superhuman? What does it mean for AI to be better than a human? And how can we tell? One Useful Thing (<a href="https://www.oneusefulthing.org/p/superhuman" rel="nofollow" target="_blank">link</a>).</cite></blockquote>



<h3 class="wp-block-heading">There’s no accounting for taste</h3>



<p class="wp-block-paragraph">To be clear, my point here isn’t to suggest AI tools aren’t helpful, just that the size of the benefit depends on the judgement, expertise and taste of the user. AI certainly can suggest a variety of statistical techniques for answering your research question, but are they the right ones?<a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftn14" id="_ftnref14" rel="nofollow" target="_blank">[14]</a> Sure, AI can quickly produce code for twenty reasonable looking plots, but do you understand the precise changes needed to accurately present your results and apply your corporate style guidelines and colours? Certainly, AI might simplify data cleaning, but does the approach make the right assumptions given the context, your research question and how the data was collected? And, while it might be true that research built on AI-generated code is more reproducible than a spreadsheet with a tangled collection of formulas, it may just be encoding hidden errors, the wrong methodology and creating a new source of technical debt you and your team will have to pay in the future.</p>



<p class="wp-block-paragraph">In my original post I was excited at programming expanding the scale, scope and quality of analysis I could do. AI can help with the first two, but its influence on the latter seems to depend on the user. AI may be able to slap together a solid draft that gets you 90 percent of the way, which for some tasks is all you need, but deciding what good looks like <em>and</em> instructing AI how to help relies on building sufficient judgement, taste and expertise. </p>



<h3 class="wp-block-heading">Vibing fast and slow</h3>



<p class="wp-block-paragraph">The need to develop judgement, expertise and professional taste is also not unique to coding. But like musicians with sheet music, a mathematician with an equation and an economist with a set of national account statistics, anyone working with code is likely to perform better with some understanding of how it works, even with the help of AI. In <a href="https://www.linkedin.com/posts/andrewyng_some-people-today-are-discouraging-others-share-7305984835037118464-RPIX/" rel="nofollow" target="_blank">the words of Andrew Ng</a>:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><em>“…people who understand the language of software through their knowledge of coding can tell an LLM or an AI-enabled IDE what they want much more precisely, and get much better results.”</em></p>
</blockquote>



<p class="wp-block-paragraph">And once again, there’s research that backs this up. For instance, pre-AI a meta-analysis of the effects of learning to code points to benefits to creativity and metacognition i.e. <em>the processes underlying the monitoring, adaptation, evaluation, and planning of thinking and behaviour,</em><a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftn15" id="_ftnref15" rel="nofollow" target="_blank">[15]</a> both which are plausibly linked to the ability to properly prompt and manage AI to perform tasks. A small observational study<a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftn16" id="_ftnref16" rel="nofollow" target="_blank">[16]</a>  (n=100) seems to support this idea, with their analysis finding a positive association between vibe-coding efficacy and computer science (CS) domain expertise. Although the exact questions used to evaluate CS expertise aren’t explicitly outlined, their cited source points to them covering the same thinking and skills that learning to program helps to build,<a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftn17" id="_ftnref17" rel="nofollow" target="_blank">[17]</a> with the authors concluding that effectively <em>vibe-coding likely draws on problem decomposition and algorithmic thinking skills</em>.</p>



<p class="wp-block-paragraph">The book <em>Messy Jobs</em> also makes this point by noting that <em>by cheapening the cost of cognition, AI simultaneously raises the value of judgement and taste.<a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftn18" id="_ftnref18" rel="nofollow" target="_blank"><strong>[18]</strong></a></em> The consequence of this for vibe-coding is that both instructing AI how to solve a problem and being able to judge the quality of outputs relies on knowing how code works, otherwise you might just be doing stupid things faster, in the words of Hadley Wickham:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">“…LLMs and AI are, I think, a massive amplifier. You can certainly use them to amplify the worst aspects of yourself, but you can also use them to amplify the best aspects…”</p>
<cite>Wickham, H. (2026, July 25). y code when ai? A talk on AI and coding. Tidy Design. <a href="https://tidydesign.substack.com/p/y-code-when-ai" rel="nofollow" target="_blank">https://tidydesign.substack.com/p/y-code-when-ai</a></cite></blockquote>



<h3 class="wp-block-heading">Summing up</h3>



<p class="wp-block-paragraph">In my 2019 listicle, I argued that learning to code was worth it so you can answer a wider variety of questions more quickly and at a higher standard to traditional point-and-click software. Seven years later and AI tools can produce code for a wide variety of tasks more quickly than an experienced developer. But that doesn’t make what it produces <em>good</em>. </p>



<p class="wp-block-paragraph">None of this means that I think you should avoid using AI before having mastered how to code, just that to be a force-multiplier it helps to know what good code looks like. Treat it as a brainy intern: motivated, well read, and in need of supervision. Give it clear instructions and it might come back with something useful. Don’t and its code might set the office microwave on fire. Learning to code can help you tell the difference between the two. </p>



<p class="wp-block-paragraph">That matters because you’re accountable for the result as the human given the the instructions. Use AI to decide how to prioritize which grants to cancel and you might cause unnecessary harm,<a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftn19" id="_ftnref19" rel="nofollow" target="_blank">[19]</a> trust its recommendations without validating them and you might just erroneously reinforce cultural stereotypes<a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftn20" id="_ftnref20" rel="nofollow" target="_blank">[20]</a>, and have it author a report for you, and it might just cite hallucinated sources.<a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftn21" id="_ftnref21" rel="nofollow" target="_blank">[21]</a> If used well, AI can help you reach your destination quicker and expand your professional frontier. If used poorly, it might just having you producing more errors at scale.</p>



<p class="wp-block-paragraph">I don’t know how long this holds. The studies here are small and not focused directly on the tasks we do in public policy. My best guess is that the jagged frontier will continue to expand, but unevenly, which will continue to place a premium on professional expertise and taste for tasks AI continues to struggle with. </p>



<p class="wp-block-paragraph">So: should you still learn to code in 202X? Yes, but not for the reason I gave in 2019. Learn to code so you can tell good code from bad and steer the tool in the right direction. </p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph"><a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftnref1" id="_ftn1" rel="nofollow" target="_blank">[1]</a>The <a href="https://gmcgoldr.github.io/2026/09/04/llm-next-token-predictors.html" rel="nofollow" target="_blank">push-back I’ve seen to this description</a> of how LLMs work doesn’t change the core point. LLMs are trained to probabilistically predict the next token to produce intelligent-looking outputs at scale.</p>



<p class="wp-block-paragraph"><a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftnref2" id="_ftn2" rel="nofollow" target="_blank">[2]</a> LLMs are trained on the finished product of human intelligence, such as published research, books and social media posts, but not the thinking that went into producing them. Newer models that ‘think’ before responding to a prompt are meant as a partial workaround to this, but don’t change the basic point that LLMs are trained to mimic <em>articulated</em> <em>intelligence</em>, rather than the process of producing. </p>



<p class="wp-block-paragraph"><a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftnref3" id="_ftn3" rel="nofollow" target="_blank">[3]</a> A practice that might have the opposite result if you’re not careful, see: Goldsmith-Pinkham, P. (2026, September 10). How do we know if writing is AI? Some initial analyses of AI’s impact on economics writing. A Causal Affair (<a href="https://paulgp.substack.com/p/how-do-we-know-if-writing-is-ai" rel="nofollow" target="_blank">link</a>) </p>



<p class="wp-block-paragraph"><a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftnref4" id="_ftn4" rel="nofollow" target="_blank">[4]</a> The one that may no longer be true is the trend towards open-source statistics. Certainly, I’ve been experimenting with ways to use AI to better extract government data (<a href="https://policyanalysislab.com/shared/general/250312_mvp/media/260625_MVP_Demo.mp4" rel="nofollow" target="_blank">see example</a>), but it’s entirely possible LLMs will make the internet a more difficult resource to navigate as text can be generated at larger scales.</p>



<p class="wp-block-paragraph"><a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftnref5" id="_ftn5" rel="nofollow" target="_blank">[5]</a> Dell’Acqua, F., McFowland III, E., Mollick, E., Lifshitz, H., Kellogg, K.C., Rajendran, S., Krayer, L., Candelon, F. and Lakhani, K.R., 2026. Navigating the jagged technological frontier: Field experimental evidence of the effects of artificial intelligence on knowledge worker productivity and quality. Organization Science, 37(2), pp.403-423.</p>



<p class="wp-block-paragraph"><a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftnref6" id="_ftn6" rel="nofollow" target="_blank">[6]</a> Akimitsu, P., 2026. Wrong and More Confident: A Field Experiment on Large Language Models Taking a Graduate Economics Exam. arXiv preprint arXiv:2607.23424.</p>



<p class="wp-block-paragraph"><a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftnref7" id="_ftn7" rel="nofollow" target="_blank">[7]</a> METR. (2026, May). Task-completion time horizons of frontier AI models. <a href="https://metr.org/time-horizons/" rel="nofollow" target="_blank">https://metr.org/time-horizons/</a></p>



<p class="wp-block-paragraph"><a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftnref8" id="_ftn8" rel="nofollow" target="_blank">[8]</a> Ethan Mollick uses the term <em>Centaurs</em> and <em>Cyborgs</em> todescribe the two most common approaches used by  workers that were able to navigate this frontier. In both cases, using AI effectively requires intelligently and selectively integrating the tools into their work without ‘falling asleep at the wheel’ (<a href="https://www.oneusefulthing.org/p/centaurs-and-cyborgs-on-the-jagged" rel="nofollow" target="_blank">link</a>).</p>



<p class="wp-block-paragraph"><a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftnref9" id="_ftn9" rel="nofollow" target="_blank">[9]</a> Otis, N.G., Clarke, R., Delecourt, S., Holtz, D. and Koning, R., 2023. The Uneven Impact of Generative AI on Entrepreneurial Performance: Evidence from a Field Experiment in Kenya (No. 24-042). Harvard Business School Working Paper.</p>



<p class="wp-block-paragraph"><a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftnref10" id="_ftn10" rel="nofollow" target="_blank">[10]</a> Becker, J., Rush, N., Barnes, B., &#038; Rein, D. (2025, July 10). Measuring the impact of early-2025 AI on experienced open-source developer productivity. METR. <a href="https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/" rel="nofollow" target="_blank">https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/</a></p>



<p class="wp-block-paragraph"><a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftnref11" id="_ftn11" rel="nofollow" target="_blank">[11]</a> Becker, J., Rush, N., Cunningham, T., Rein, D., &#038; Mahamud, K. (2026, February 24). We are changing our developer productivity experiment design. METR. <a href="https://metr.org/blog/2026-02-24-uplift-update/" rel="nofollow" target="_blank">https://metr.org/blog/2026-02-24-uplift-update/</a></p>



<p class="wp-block-paragraph"><a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftnref12" id="_ftn12" rel="nofollow" target="_blank">[12]</a> The authors note it had become increasingly in their 2026 follow-up study to recruit developers willing to work without AI, which likely resulted in estimates of AI-assisted speedup being biased downward.</p>



<p class="wp-block-paragraph"><a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftnref13" id="_ftn13" rel="nofollow" target="_blank">[13]</a> Although the authors recommend their results are interpreted with caution, the 2025 study not only found that developers using AI were slower, but that more experienced developers tended to overestimate the time they would save from using AI.</p>



<p class="wp-block-paragraph"><a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftnref14" id="_ftn14" rel="nofollow" target="_blank">[14]</a>Like humans, LLMs exhibit systematic biases in the way they work and evaluate problems. This might come in subtle forms that influence outputs subtly, such as assuming doctors are more likely to be men than women. Or explicit, such as producing outputs that reflect how prominent a solution is in its training set, rather than necessarily being appropriate for the task at hand (<a href="https://arxiv.org/html/2411.10915v1#S3" rel="nofollow" target="_blank">link</a>).  </p>



<p class="wp-block-paragraph"><a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftnref15" id="_ftn15" rel="nofollow" target="_blank">[15]</a> Scherer, R., Siddiq, F. and Sánchez Viveros, B., 2019. The cognitive benefits of learning computer programming: A meta-analysis of transfer effects. Journal of Educational Psychology, 111(5), p.764.</p>



<p class="wp-block-paragraph"><a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftnref16" id="_ftn16" rel="nofollow" target="_blank">[16]</a> Thorgeirsson, S., Weidmann, T.B. and Su, Z., 2026, April. Computer science achievement and writing skills predict vibe coding proficiency. In Proceedings of the 2026 CHI Conference on Human Factors in Computing Systems (pp. 1-17).</p>



<p class="wp-block-paragraph"><a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftnref17" id="_ftn17" rel="nofollow" target="_blank">[17]</a> Specifically, code tracing, code completion, conditionals, loops and logical operators, see: Parker, M.C., Solomon, A., Pritchett, B., Illingworth, D.A., Marguilieux, L.E. and Guzdial, M., 2018, August. Socioeconomic status and computer science achievement: Spatial ability as a mediating variable in a novel model of understanding. In Proceedings of the 2018 ACM Conference on International Computing Education Research (pp. 97-105).</p>



<p class="wp-block-paragraph"><a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftnref18" id="_ftn18" rel="nofollow" target="_blank">[18]</a> Garicano, L, Li, J &#038; Wu, Y 2026, Messy Jobs: The Work That AI Cannot Reach, Upriver Press</p>



<p class="wp-block-paragraph"><a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftnref19" id="_ftn19" rel="nofollow" target="_blank">[19]</a> Alibašić, H., 2026. Introduction: The Imperative of Hybrid Intelligence in Digital Governance. In Hybrid Intelligence for Effective Digital Governance: AI in Administration (pp. 3-48). Cham: Springer Nature Switzerland.</p>



<p class="wp-block-paragraph"><a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftnref20" id="_ftn20" rel="nofollow" target="_blank">[20]</a>Saar Alon-Barkat, Madalina Busuioc, Human–AI Interactions in Public Sector Decision Making: “Automation Bias” and “Selective Adherence” to Algorithmic Advice, Journal of Public Administration Research and Theory, Volume 33, Issue 1, January 2023, Pages 153–169, <a href="https://doi.org/10.1093/jopart/muac007" rel="nofollow" target="_blank">https://doi.org/10.1093/jopart/muac007</a> </p>



<p class="wp-block-paragraph"><a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/#_ftnref21" id="_ftn21" rel="nofollow" target="_blank">[21]</a> Karp, P. (2025, November 6). AI-tainted Deloitte report was worse than previously thought. Australian Financial Review. <a href="https://www.afr.com/politics/ai-tainted-deloitte-report-was-worse-than-previously-thought-20251106-p5n863" rel="nofollow" target="_blank">https://www.afr.com/politics/ai-tainted-deloitte-report-was-worse-than-previously-thought-20251106-p5n863</a></p>
<p>The post <a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/" rel="nofollow" target="_blank">X Reasons for policy professionals to get into programming in 202X</a> appeared first on <a href="https://www.gilesd-j.com/" rel="nofollow" target="_blank">Giles</a>.</p>

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://www.gilesd-j.com/2026/09/28/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/"> Data Analytics and AI Archives - Giles</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/09/x-reasons-for-policy-professionals-to-get-into-programming-in-202x/">X Reasons for policy professionals to get into programming in 202X</a>]]></content:encoded>
					
		
		<enclosure url="https://policyanalysislab.com/shared/general/250312_mvp/media/260625_MVP_Demo.mp4" length="64065940" type="video/mp4" />

		<post-id xmlns="com-wordpress:feed-additions:1">403955</post-id>	</item>
		<item>
		<title>Adding a free AI chatbot to a blogdown website</title>
		<link>https://www.r-bloggers.com/2026/09/adding-a-free-ai-chatbot-to-a-blogdown-website/</link>
		
		<dc:creator><![CDATA[R-bloggers on Almost Random]]></dc:creator>
		<pubDate>Mon, 28 Sep 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://www.malte-grosser.com/post/adding-a-free-ai-chatbot/</guid>

					<description><![CDATA[<p>I wanted to add a small “Ask AI” window to this website. Visitors should be able to ask about my projects, publications or background and get a short answer with a useful link. Running it should cost nothing.<br />
I described my website setup to ChatGPT, fo...</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/09/adding-a-free-ai-chatbot-to-a-blogdown-website/">Adding a free AI chatbot to a blogdown website</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://www.malte-grosser.com/post/adding-a-free-ai-chatbot/"> R-bloggers on Almost Random</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
I wanted to add a small “Ask AI” window to this website. Visitors should be able to ask about my projects, publications or background and get a short answer with a useful link. Running it should cost nothing.
I described my website setup to ChatGPT, followed its suggestions and discussed the parts I wanted to change. That covered choosing a provider, writing the code and adjusting the window for desktop and mobile.
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://www.malte-grosser.com/post/adding-a-free-ai-chatbot/"> R-bloggers on Almost Random</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/09/adding-a-free-ai-chatbot-to-a-blogdown-website/">Adding a free AI chatbot to a blogdown website</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">403962</post-id>	</item>
	</channel>
</rss>
