<?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>Wed, 21 Oct 2026 11:37:45 +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>Hybrid Machine Learning &#038; Financial Econometrics: Building Volatility-Aware Dynamic Forecast Bands for Bitcoin</title>
		<link>https://www.r-bloggers.com/2026/10/hybrid-machine-learning-financial-econometrics-building-volatility-aware-dynamic-forecast-bands-for-bitcoin/</link>
		
		<dc:creator><![CDATA[Selcuk Disci]]></dc:creator>
		<pubDate>Wed, 21 Oct 2026 11:37:45 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">http://datageeek.com/?p=12961</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; "> When forecasting high-volatility financial assets like Bitcoin (BTC-USD), standard point-prediction models—whether classical ARIMA or deep neural networks—often fall short. A point estimate tells you where the model expects the price to go, but it completely ignores market regime shifts and changing risk levels. In this article, we construct ...</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/hybrid-machine-learning-financial-econometrics-building-volatility-aware-dynamic-forecast-bands-for-bitcoin/">Hybrid Machine Learning & Financial Econometrics: Building Volatility-Aware Dynamic Forecast Bands for Bitcoin</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://datageeek.com/2026/10/21/hybrid-machine-learning-financial-econometrics-building-volatility-aware-dynamic-forecast-bands-for-bitcoin/"> DataGeeek</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">When forecasting high-volatility financial assets like Bitcoin (BTC-USD), standard point-prediction models—whether classical ARIMA or deep neural networks—often fall short. A point estimate tells you <em>where</em> the model expects the price to go, but it completely ignores market regime shifts and changing risk levels.</p>



<p class="wp-block-paragraph">In this article, we construct an end-to-end forecasting pipeline in R that combines <strong>Machine Learning (Multi-Layer Perceptron via <code>brulee</code> and <code>tidymodels</code>)</strong> for directional trend prediction with <strong>Financial Econometrics (GJR-GARCH via <code>rugarch</code>)</strong> to generate dynamic, volatility-adjusted confidence bounds.</p>



<h2 class="wp-block-heading">The Architecture Overview</h2>



<p class="wp-block-paragraph">Instead of attempting to forecast level prices directly—which introduces non-stationarity issues—our pipeline operates on <strong>log returns</strong> and reconstructs price trajectories dynamically.</p>



<figure data-wp-context="{"imageId":"6ad8a491b70da"}" data-wp-interactive="core/image" data-wp-key="6ad8a491b70da" class="wp-block-image size-large wp-lightbox-container"><img loading="lazy" data-attachment-id="12969" data-permalink="https://datageeek.com/2026/10/21/hybrid-machine-learning-financial-econometrics-building-volatility-aware-dynamic-forecast-bands-for-bitcoin/image-141/" data-orig-file="https://datageeek.com/wp-content/uploads/2026/10/image.png" data-orig-size="981,438" data-comments-opened="1" data-image-title="image" data-image-description="" data-image-caption="" data-large-file="https://i2.wp.com/datageeek.com/wp-content/uploads/2026/10/image.png?w=450&#038;ssl=1" data-wp-class--hide="state.isContentHidden" data-wp-class--show="state.isContentVisible" data-wp-init="callbacks.setButtonStyles" data-wp-on--click="actions.showLightbox" data-wp-on--load="callbacks.setButtonStyles" data-wp-on--pointerdown="actions.preloadImage" data-wp-on--pointerenter="actions.preloadImageWithDelay" data-wp-on--pointerleave="actions.cancelPreload" data-wp-on-window--resize="callbacks.setButtonStyles" src="https://i2.wp.com/datageeek.com/wp-content/uploads/2026/10/image.png?w=450&#038;ssl=1" alt="" class="wp-image-12969" srcset_temp="https://datageeek.com/wp-content/uploads/2026/10/image.png 981w, https://datageeek.com/wp-content/uploads/2026/10/image.png?w=150 150w, https://datageeek.com/wp-content/uploads/2026/10/image.png?w=300 300w, https://datageeek.com/wp-content/uploads/2026/10/image.png?w=768 768w" sizes="(max-width: 981px) 100vw, 981px" data-recalc-dims="1" /><button
			class="lightbox-trigger"
			type="button"
			aria-haspopup="dialog"
			data-wp-bind--aria-label="state.thisImage.triggerButtonAriaLabel"
			data-wp-init="callbacks.initTriggerButton"
			data-wp-on--click="actions.showLightbox"
			data-wp-style--right="state.thisImage.buttonRight"
			data-wp-style--top="state.thisImage.buttonTop"
		>
			<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 12 12">
				<path fill="#fff" d="M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z" />
			</svg>
		</button></figure>



<h2 class="wp-block-heading">Key Architectural Decisions &#038; Methodological Rationale</h2>



<h3 class="wp-block-heading">1. Modeling Log Returns Instead of Level Prices</h3>



<p class="wp-block-paragraph">Directly predicting raw prices often creates spurious regression results because price series are non-stationary. By transforming the price series to <strong>log returns</strong>:<br></p>



<div class="wp-block-math"><math display="block"><semantics><mrow><msub><mi>r</mi><mi>t</mi></msub><mo>=</mo><mrow><mi>ln</mi><mo>⁡</mo></mrow><mrow><mo fence="true" form="prefix">(</mo><mfrac><msub><mi>P</mi><mi>t</mi></msub><msub><mi>P</mi><mrow><mi>t</mi><mo>−</mo><mn>1</mn></mrow></msub></mfrac><mo fence="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">r_t = \ln\left(\frac{P_t}{P_{t-1}}\right)</annotation></semantics></math></div>





<p class="wp-block-paragraph">This transformation achieves stationarity, making the target variable suitable for neural network training. Factual price predictions are reconstructed at test time via cumulative exponentiation:<br></p>



<div class="wp-block-math"><math display="block"><semantics><mrow><msub><mover><mi>P</mi><mo stretchy="false">^</mo></mover><mi>t</mi></msub><mo>=</mo><msub><mi>P</mi><mrow><mi>t</mi><mo>−</mo><mn>1</mn></mrow></msub><mo>×</mo><mrow><mi>exp</mi><mo>⁡</mo></mrow><mo form="prefix" stretchy="false">(</mo><msub><mover><mi>r</mi><mo stretchy="false">^</mo></mover><mi>t</mi></msub><mo form="postfix" stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">\hat{P}_t = P_{t-1} \times \exp(\hat{r}_t)</annotation></semantics></math></div>



<h3 class="wp-block-heading">2. Deep Learning for Trend: Multi-Layer Perceptron (<code>brulee</code>)</h3>



<p class="wp-block-paragraph">We use a 2-layer MLP architecture <code>(32, 16)</code> initialized via the <code>tidymodels</code> framework.</p>



<ul class="wp-block-list">
<li><strong>Lag Features</strong>: We supply 20 days of lagged log returns to capture short- to medium-term autocorrelation.</li>



<li><strong>Normalizations &#038; Dropping Zero Variance</strong>: Handled within a clean <code>recipes</code> pipeline to prevent data leakage</li>
</ul>



<h3 class="wp-block-heading">3. Econometric Volatility Modeling: GJR-GARCH(1,1) on Neural Network Residuals</h3>



<p class="wp-block-paragraph">Machine learning models capture the conditional mean, but their residuals,                                                             <math data-latex=" \epsilon_t = r_t - \hat{r}_t"><semantics><mrow><msub><mi>ϵ</mi><mi>t</mi></msub><mo>=</mo><msub><mi>r</mi><mi>t</mi></msub><mo>−</mo><msub><mover><mi>r</mi><mo stretchy="false">^</mo></mover><mi>t</mi></msub></mrow><annotation encoding="application/x-tex"> \epsilon_t = r_t – \hat{r}_t</annotation></semantics></math> , still contain <strong>volatility clustering</strong>—periods of high turbulence followed by high turbulence, and calm periods followed by calm.</p>



<p class="wp-block-paragraph">To model this conditional variance (<math data-latex="\sigma_t^2"><semantics><msubsup><mi>σ</mi><mi>t</mi><mn>2</mn></msubsup><annotation encoding="application/x-tex">\sigma_t^2</annotation></semantics></math>), we fit a <strong>GJR-GARCH(1,1)</strong> model on the MLP training residuals:</p>



<div class="wp-block-math"><math display="block"><semantics><mrow><msubsup><mi>σ</mi><mi>t</mi><mn>2</mn></msubsup><mo>=</mo><mi>ω</mi><mo>+</mo><mi>α</mi><msubsup><mi>ϵ</mi><mrow><mi>t</mi><mo>−</mo><mn>1</mn></mrow><mn>2</mn></msubsup><mo>+</mo><mi>γ</mi><msub><mi>I</mi><mrow><mi>t</mi><mo>−</mo><mn>1</mn></mrow></msub><msubsup><mi>ϵ</mi><mrow><mi>t</mi><mo>−</mo><mn>1</mn></mrow><mn>2</mn></msubsup><mo>+</mo><mi>β</mi><msubsup><mi>σ</mi><mrow><mi>t</mi><mo>−</mo><mn>1</mn></mrow><mn>2</mn></msubsup></mrow><annotation encoding="application/x-tex">\sigma_t^2 = \omega + \alpha \epsilon_{t-1}^2 + \gamma I_{t-1} \epsilon_{t-1}^2 + \beta \sigma_{t-1}^2</annotation></semantics></math></div>



<ul class="wp-block-list">
<li><strong>Why GJR-GARCH over Standard GARCH?</strong>                                                                Crypto markets exhibit leverage effects: price drops often spark higher panic volatility than equivalent price rallies. The indicator term <math data-latex="I_{t-1}"><semantics><msub><mi>I</mi><mrow><mi>t</mi><mo>−</mo><mn>1</mn></mrow></msub><annotation encoding="application/x-tex">I_{t-1}</annotation></semantics></math> captures this asymmetry (<math data-latex="\gamma"><semantics><mi>γ</mi><annotation encoding="application/x-tex">\gamma</annotation></semantics></math>).</li>



<li><strong>Why <code>garchOrder = c(1, 1)</code>?</strong>                                                                                           The order <code>(1,1)</code> specifies a single lag for the ARCH component (<math data-latex="\epsilon_{t-1}^2"><semantics><msubsup><mi>ϵ</mi><mrow><mi>t</mi><mo>−</mo><mn>1</mn></mrow><mn>2</mn></msubsup><annotation encoding="application/x-tex">\epsilon_{t-1}^2</annotation></semantics></math>) and a single lag for the GARCH component (<math data-latex="\sigma_{t-1}^2"><semantics><msubsup><mi>σ</mi><mrow><mi>t</mi><mo>−</mo><mn>1</mn></mrow><mn>2</mn></msubsup><annotation encoding="application/x-tex">\sigma_{t-1}^2</annotation></semantics></math>). Empirical literature demonstrates that a single lag captures the vast majority of volatility persistence without overfitting.</li>



<li><strong>Why Skewed Student-t (<code>sstd</code>) Distribution?</strong>                                                           Bitcoin returns violate Gaussian normal distribution assumptions. They display <strong>fat tails</strong> (extreme events occur frequently) and <strong>skewness</strong> (asymmetric distribution). The <code>sstd</code> distribution explicitly models both heavy tails and skewness.</li>



<li><strong>Solver Selection</strong>:                                                                                                                     We use <code>solver = &quot;hybrid&quot;</code> in <code>rugarch</code> to combine optimization algorithms, ensuring convergence even under complex distribution parameters.</li>
</ul>



<div class="wp-block-math"><math display="block"><semantics><mrow><msub><mtext>Upper Bound</mtext><mi>t</mi></msub><mo>=</mo><msub><mover><mi>P</mi><mo stretchy="false">^</mo></mover><mi>t</mi></msub><mo>×</mo><mrow><mi>exp</mi><mo>⁡</mo></mrow><mo form="prefix" stretchy="false">(</mo><mo form="prefix" stretchy="false">+</mo><mn>1.96</mn><mo>×</mo><msub><mi>σ</mi><mi>t</mi></msub><mo form="postfix" stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">\text{Upper Bound}_t = \hat{P}_t \times \exp(+1.96 \times \sigma_t)</annotation></semantics></math></div>



<div class="wp-block-math"><math display="block"><semantics><mrow><msub><mtext>Lower Bound</mtext><mi>t</mi></msub><mo>=</mo><msub><mover><mi>P</mi><mo stretchy="false">^</mo></mover><mi>t</mi></msub><mo>×</mo><mrow><mi>exp</mi><mo>⁡</mo></mrow><mo form="prefix" stretchy="false">(</mo><mo form="prefix" stretchy="false">−</mo><mn>1.96</mn><mo>×</mo><msub><mi>σ</mi><mi>t</mi></msub><mo form="postfix" stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">\text{Lower Bound}_t = \hat{P}_t \times \exp(-1.96 \times \sigma_t)</annotation></semantics></math></div>



<p class="wp-block-paragraph">When the market is quiet, these confidence bounds contract automatically. During volatile regimes, the bounds expand, giving a realistic probabilistic envelope rather than static, uninformative confidence intervals.</p>



<h2 class="wp-block-heading">Complete R Implementation</h2>



<p class="wp-block-paragraph">Below is the complete, self-contained R script implementing the full pipeline using <code>tidymodels</code>, <code>brulee</code>, <code>rugarch</code>, and <code>plotly</code>.</p>


<pre>
# Integrated Forecasting Pipeline with tidymodels, yardstick, and plotly
library(tidymodels)
library(timetk)
library(brulee)
library(modeltime)
library(tidyquant)
library(plotly)
library(scales)
library(glue)
library(ggtext)
library(rugarch) # Added for GJR-GARCH volatility modeling

# 1. Fetch Data & Calculate Log Returns
df_raw &lt;- tq_get(&quot;BTC-USD&quot;) %&gt;%
  select(date, close) %&gt;%
  filter(date &gt;= last(date) - months(24)) %&gt;%
  drop_na() %&gt;%
  mutate(log_ret = log(close / lag(close))) %&gt;%
  drop_na()

# 2. Add Lag Features on Log Returns
df_prepared &lt;- df_raw %&gt;%
  tk_augment_lags(log_ret, .lags = 1:20) %&gt;%
  drop_na()

# 3. Time Series Split (10 Assessment Window)
split &lt;- time_series_split(
  df_prepared,
  assess = 10,
  cumulative = TRUE
)

df_train &lt;- training(split)
df_test  &lt;- testing(split)

# 4. Recipe Setup (Predict Log Return, Not Level Price)
recipe_mlp &lt;- recipe(log_ret ~ ., data = df_train) %&gt;%
  step_rm(date, close) %&gt;%
  step_dummy(all_nominal_predictors()) %&gt;%
  step_zv(all_predictors()) %&gt;%
  step_normalize(all_numeric_predictors())

# modeltime::naive_reg() requires the date column as a predictor to parse the time index
recipe_naive &lt;- recipe(log_ret ~ date, data = df_train)

# 5. Model Specifications
model_spec_mlp &lt;- mlp(
  hidden_units = c(32, 16),
  epochs = 100,
  dropout = 0.05,
  learn_rate = 0.005
) %&gt;%
  set_engine(&quot;brulee&quot;) %&gt;%
  set_mode(&quot;regression&quot;)

model_spec_naive &lt;- naive_reg() %&gt;%
  set_engine(&quot;naive&quot;)

# 6. Workflow Construction & Model Fitting
workflow_mlp &lt;- workflow() %&gt;%
  add_recipe(recipe_mlp) %&gt;%
  add_model(model_spec_mlp) %&gt;%
  fit(df_train)

workflow_naive &lt;- workflow() %&gt;%
  add_recipe(recipe_naive) %&gt;%
  add_model(model_spec_naive) %&gt;%
  fit(df_train)

# 7. Predictions & Price Reconstruction (Reference Code Logic)
preds_mlp_ret   &lt;- predict(workflow_mlp, df_test)
preds_naive_ret &lt;- predict(workflow_naive, df_test)

df_results &lt;- df_test %&gt;%
  mutate(
    yesterday_close = lag(close, default = first(close)),
    actual      = close,
    pred_brulee = yesterday_close * exp(preds_mlp_ret$.pred),
    pred_naive  = yesterday_close * exp(preds_naive_ret$.pred)
  )

# ==============================================================================
# 7.5. GJR-GARCH (Skewed Student-t) Dynamic Volatility & Confidence Bands
# ==============================================================================
# Calculate MLP residuals on the training set
train_preds_mlp &lt;- predict(workflow_mlp, df_train)$.pred
train_residuals &lt;- df_train$log_ret - train_preds_mlp

# GJR-GARCH(1,1) specification with Skewed Student-t distribution for fat-tails and asymmetry
garch_spec &lt;- ugarchspec(
  variance.model = list(model = &quot;gjrGARCH&quot;, garchOrder = c(1, 1)),
  mean.model = list(armaOrder = c(0, 0), include.mean = FALSE),
  distribution.model = &quot;sstd&quot;
)

# Fit GJR-GARCH model on training residuals
garch_fit &lt;- ugarchfit(spec = garch_spec, data = train_residuals, solver = &quot;hybrid&quot;)

# Forecast conditional volatility for the 10-day test horizon
garch_fore &lt;- ugarchforecast(garch_fit, n.ahead = 10)
dynamic_sigmas &lt;- sigma(garch_fore) # 10-day dynamic conditional standard deviations

# Attach dynamic standard deviations to test results and compute dynamic confidence bounds
df_results &lt;- df_results %&gt;%
  mutate(
    conditional_sigma = as.vector(dynamic_sigmas),
    # Dynamically scaled confidence bounds in price space (± 1.96 * conditional_sigma)
    conf_hi = pred_brulee * exp(1.96 * conditional_sigma),
    conf_lo = pred_brulee * exp(-1.96 * conditional_sigma)
  )

# 8. Accuracy Table Construction (Yardstick on Original Price Scale)
accuracy_table &lt;- tibble(
  .model_id   = c(1, 2),
  .model_desc = c(&quot;BRULEE MLP (32, 16) + GJR-GARCH&quot;, &quot;NAIVE&quot;),
  rmse        = c(rmse_vec(df_results$actual, df_results$pred_brulee),
                  rmse_vec(df_results$actual, df_results$pred_naive)),
  rsq         = c(rsq_vec(df_results$actual, df_results$pred_brulee),
                  rsq_vec(df_results$actual, df_results$pred_naive)),
  mape        = c(mape_vec(df_results$actual, df_results$pred_brulee),
                  mape_vec(df_results$actual, df_results$pred_naive)),
  mae         = c(mae_vec(df_results$actual, df_results$pred_brulee),
                  mae_vec(df_results$actual, df_results$pred_naive))
)

# Print Model Comparison Table
cat(&quot;=== MODEL ACCURACY EVALUATION TABLE (PRICE SCALE) ===\n&quot;)
cat(knitr::kable(accuracy_table, digits = 4, format = &quot;simple&quot;), sep = &quot;\n&quot;)

# 9. Modern Interactive Plotly Visualization (Reference Style)
brulee_mape &lt;- accuracy_table$mape[1]
df_eval &lt;- df_results

# Custom Hover Text Data Frames
df_plot_actual &lt;- df_eval %&gt;% select(date, actual)  %&gt;% mutate(text_actual = glue(&quot;&lt;b&gt;Actual Price:&lt;/b&gt; {scales::dollar(actual, accuracy = 0.01)}&lt;br&gt;&lt;b&gt;Date:&lt;/b&gt; {format(date, &#039;%b %d, %Y&#039;)}&quot;))
df_plot_pred   &lt;- df_eval %&gt;% select(date, pred_brulee) %&gt;% mutate(text_pred   = glue(&quot;&lt;b&gt;AI Prediction Price:&lt;/b&gt; {scales::dollar(pred_brulee, accuracy = 0.01)}&lt;br&gt;&lt;b&gt;Date:&lt;/b&gt; {format(date, &#039;%b %d, %Y&#039;)}&quot;))
df_plot_hi     &lt;- df_eval %&gt;% select(date, conf_hi) %&gt;% mutate(text_hi     = glue(&quot;&lt;b&gt;GJR-GARCH Upper Bound:&lt;/b&gt; {scales::dollar(conf_hi, accuracy = 0.01)}&lt;br&gt;&lt;b&gt;Date:&lt;/b&gt; {format(date, &#039;%b %d, %Y&#039;)}&quot;))
df_plot_lo     &lt;- df_eval %&gt;% select(date, conf_lo) %&gt;% mutate(text_lo     = glue(&quot;&lt;b&gt;GJR-GARCH Lower Bound:&lt;/b&gt; {scales::dollar(conf_lo, accuracy = 0.01)}&lt;br&gt;&lt;b&gt;Date:&lt;/b&gt; {format(date, &#039;%b %d, %Y&#039;)}&quot;))

# Build Base GGPlot Layer
p &lt;- ggplot() +
  geom_ribbon(data = df_eval, aes(x = date, ymin = conf_lo, ymax = conf_hi), fill = &quot;#808080&quot;, alpha = 0.18) +
  geom_point(data = df_plot_hi, aes(x = date, y = conf_hi, text = text_hi), color = &quot;transparent&quot;, alpha = 0, size = 3) +
  geom_point(data = df_plot_lo, aes(x = date, y = conf_lo, text = text_lo), color = &quot;transparent&quot;, alpha = 0, size = 3) +
  geom_line(data = df_plot_actual, aes(x = date, y = actual), color = &quot;#2c3e50&quot;, linewidth = 1.2) +
  geom_point(data = df_plot_actual, aes(x = date, y = actual, text = text_actual), color = &quot;#2c3e50&quot;, size = 2) +
  geom_line(data = df_plot_pred, aes(x = date, y = pred_brulee), color = &quot;#e74c3c&quot;, linetype = &quot;dashed&quot;, linewidth = 1.2) +
  geom_point(data = df_plot_pred, aes(x = date, y = pred_brulee, text = text_pred), color = &quot;#e74c3c&quot;, size = 2) +
  scale_y_continuous(labels = dollar_format(accuracy = 1)) +
  labs(
    x = &quot;&quot;, y = &quot;&quot;,
    title = paste0(
      &quot;&lt;span style=&#039;color:#2c3e50&#039;&gt;BTC-USD Prices&lt;/span&gt; vs &quot;,
      &quot;&lt;span style=&#039;color:#e74c3c&#039;&gt;MLP + GJR-GARCH Dynamic Forecast&lt;/span&gt;&lt;br&gt;&quot;,
      &quot;&lt;span style=&#039;font-size:12px; color:#555555;&#039;&gt;10-Day Horizon | Out-of-Sample MAPE: &quot;, round(brulee_mape, 2), &quot;&lt;/span&gt;&quot;
    )
  ) +
  theme_minimal() +
  theme(
    plot.title = element_markdown(hjust = 0.5, face = &quot;bold&quot;),
    plot.background = element_rect(fill = &quot;#ffffff&quot;, color = NA),
    panel.background = element_rect(fill = &quot;#ffffff&quot;, color = NA),
    panel.grid.minor = element_blank()
  )

# Typography Settings & Plotly Output
font_family &lt;- list(family = &quot;Roboto Slab, Sans-Serif&quot;, size = 16)
label_font  &lt;- list(font = list(family = &quot;Roboto Slab, Sans-Serif&quot;, size = 13))

ggplotly(p, tooltip = &quot;text&quot;) %&gt;%
  style(hoverlabel = label_font) %&gt;%
  layout(font = font_family)
</pre>


<figure data-wp-context="{"imageId":"6ad8a491b821c"}" data-wp-interactive="core/image" data-wp-key="6ad8a491b821c" class="wp-block-image size-large wp-lightbox-container"><img loading="lazy" data-attachment-id="12989" data-permalink="https://datageeek.com/2026/10/21/hybrid-machine-learning-financial-econometrics-building-volatility-aware-dynamic-forecast-bands-for-bitcoin/mlp_garch/" data-orig-file="https://datageeek.com/wp-content/uploads/2026/10/mlp_garch.png" data-orig-size="922,626" data-comments-opened="1" data-image-title="mlp_garch" data-image-description="" data-image-caption="" data-large-file="https://i1.wp.com/datageeek.com/wp-content/uploads/2026/10/mlp_garch.png?w=450&#038;ssl=1" data-wp-class--hide="state.isContentHidden" data-wp-class--show="state.isContentVisible" data-wp-init="callbacks.setButtonStyles" data-wp-on--click="actions.showLightbox" data-wp-on--load="callbacks.setButtonStyles" data-wp-on--pointerdown="actions.preloadImage" data-wp-on--pointerenter="actions.preloadImageWithDelay" data-wp-on--pointerleave="actions.cancelPreload" data-wp-on-window--resize="callbacks.setButtonStyles" src="https://i1.wp.com/datageeek.com/wp-content/uploads/2026/10/mlp_garch.png?w=450&#038;ssl=1" alt="" class="wp-image-12989" srcset_temp="https://datageeek.com/wp-content/uploads/2026/10/mlp_garch.png 922w, https://datageeek.com/wp-content/uploads/2026/10/mlp_garch.png?w=150 150w, https://datageeek.com/wp-content/uploads/2026/10/mlp_garch.png?w=300 300w, https://datageeek.com/wp-content/uploads/2026/10/mlp_garch.png?w=768 768w" sizes="(max-width: 922px) 100vw, 922px" data-recalc-dims="1" /><button
			class="lightbox-trigger"
			type="button"
			aria-haspopup="dialog"
			data-wp-bind--aria-label="state.thisImage.triggerButtonAriaLabel"
			data-wp-init="callbacks.initTriggerButton"
			data-wp-on--click="actions.showLightbox"
			data-wp-style--right="state.thisImage.buttonRight"
			data-wp-style--top="state.thisImage.buttonTop"
		>
			<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 12 12">
				<path fill="#fff" d="M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z" />
			</svg>
		</button></figure>



<h2 class="wp-block-heading">Conclusion</h2>



<p class="wp-block-paragraph">Combining machine learning with traditional econometrics produces a hybrid system that overcomes the limitations of both fields:</p>



<ol class="wp-block-list">
<li><strong>Machine Learning</strong> captures non-linear relationships and trend movements.</li>



<li><strong>Econometrics (GARCH)</strong> supplies explicit statistical parameters for uncertainty and volatility clustering.</li>
</ol>



<p class="wp-block-paragraph">The resulting output provides realistic, dynamic risk envelopes that evolve with changing market regimes—giving quantitative traders and analysts a robust framework for volatility-aware decision-making.</p>



<p class="wp-block-paragraph"></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://datageeek.com/2026/10/21/hybrid-machine-learning-financial-econometrics-building-volatility-aware-dynamic-forecast-bands-for-bitcoin/"> DataGeeek</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/hybrid-machine-learning-financial-econometrics-building-volatility-aware-dynamic-forecast-bands-for-bitcoin/">Hybrid Machine Learning & Financial Econometrics: Building Volatility-Aware Dynamic Forecast Bands for Bitcoin</a>]]></content:encoded>
					
		
		<enclosure url="https://datageeek.com/wp-content/uploads/2026/10/mlp_garch.png" length="0" type="" />
<enclosure url="https://1.gravatar.com/avatar/db5e3f9ef188ea98fe38ab05c5a3fad9fb52fe3472715a8fc02f7ea41731f77c?s=96&#038;d=identicon&#038;r=G" length="0" type="" />
<enclosure url="https://datageeek.com/wp-content/uploads/2026/10/image.png?w=981" length="0" type="" />
<enclosure url="https://datageeek.com/wp-content/uploads/2026/10/mlp_garch.png?w=922" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404358</post-id>	</item>
		<item>
		<title>Mark Padgham: Building the Machinery of Software Peer Review</title>
		<link>https://www.r-bloggers.com/2026/10/mark-padgham-building-the-machinery-of-software-peer-review/</link>
		
		<dc:creator><![CDATA[rOpenSci]]></dc:creator>
		<pubDate>Wed, 21 Oct 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://ropensci.org/blog/2026/10/21/mark-building-machinery-software-review/</guid>

					<description><![CDATA[<p>Mark Padgham is leaving rOpenSci at the end of October, and we want to take this moment to recognize his impact and say thank you for helping to shape rOpenSci.<br />
Mark joined rOpenSci as a staff member in December 2019 to help us expand software peer re...</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/mark-padgham-building-the-machinery-of-software-peer-review/">Mark Padgham: Building the Machinery of Software Peer 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://ropensci.org/blog/2026/10/21/mark-building-machinery-software-review/"> 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>Mark Padgham is leaving rOpenSci at the end of October, and we want to take this moment to recognize his impact and say thank you for helping to shape rOpenSci.</p>
<p><a href="http://ropensci.or/blog/2019/12/03/mark-padgham/" rel="nofollow" target="_blank">Mark joined rOpenSci as a staff member in December 2019</a> to help us expand software peer review to statistical topics.
However, Mark’s impact on rOpenSci has gone far beyond this one project.
Mark has not only shaped how rOpenSci reviews statistical software, but has also built much of the tooling that supports the peer review processes, and R packages which not only support but also strengthen our community.
If you have submitted a package to rOpenSci, reviewed or edited one, you have almost certainly used something Mark made!</p>
<h2>
Making a Mark
</h2><p>Mark’s most visible contribution is to statistical software peer review.
Reviewing a package that implements statistical methods is a different job from reviewing a package that accesses an API.
Mark tackled the task of translating what “good” means in a statistical package and how a reviewer can assess it.
He recruited a new set of editors with the expertise to tackel this work and turned insights into a concrete set of statistical software standards and the guide that accompanies them (<a href="https://ropensci.org/commcalls/feb2021-statsreview/" rel="nofollow" target="_blank">rOpenSci Statistical Software Testing and Peer Review Community Call</a>).
Mark also built the machinery to go with this new type review: <a href="https://docs.ropensci.org/pkgstats/" rel="nofollow" target="_blank">pkgstats</a> gathers information about a package’s structure, helps authors and editors see what a submission contains and how it is organised; <a href="https://docs.ropensci.org/srr/" rel="nofollow" target="_blank">srr</a> allows package authors to document their adherence to statistical software standards.</p>
<p>The result of all this work was <a href="https://ropensci.org/blog/2022/11/30/first-peer-reviewed-stats-packages/" rel="nofollow" target="_blank">our first peer-reviewed statistical packages</a>, published in November 2022, and to date we’ve had over 26 statistical packages go through this review!
Anyone who has gone through statistical software review has benefited from the groundwork Mark laid.</p>
<h2>
The engine of software review
</h2><p>Peer review at rOpenSci is a human process, but it runs on a lot of automation, and a good part of that automation is Mark’s work.</p>
<p>Mark built and maintained <a href="https://docs.ropensci.org/pkgcheck/" rel="nofollow" target="_blank">pkgcheck</a>, which runs automated checks on submitted packages, and the <a href="https://ropensci.org/blog/2022/02/01/pkgcheck-action/" rel="nofollow" target="_blank">GitHub Action</a> that allows anyone to run the same checks on their own repository.
pkgcheck uses <a href="https://docs.ropensci.org/goodpractice/" rel="nofollow" target="_blank">goodpractice</a>, another package maintained by Mark, that <a href="https://ropensci.org/blog/2026/06/01/goodpractice/" rel="nofollow" target="_blank">recently rolled out a host of updates and extension</a>.
Mark maintained <a href="https://ropensci.org/blog/2022/05/31/chatops-review-system-github/" rel="nofollow" target="_blank">Buffy</a>, the bot behind our chat review system on GitHub, which lets editors assign reviewers, track deadlines and move submissions along with simple comments, and many other pieces of infrastructure that make the whole system work.</p>
<h2>
Review is a human process
</h2><p>In early 2025, <a href="https://ropensci.org/blog/2025/02/27/news-february-2025/#mark-padgham-is-now-software-review-lead" rel="nofollow" target="_blank">Mark became our new Software Review Lead</a>.
In this role he was in charge of editorial management, curating the current editorial board and planning its future.
Mark helped guide peer-review through new challenges, such as how review should handle AI-assisted contributions, resulting in <a href="https://ropensci.org/blog/2026/02/26/ropensci-ai-policy/" rel="nofollow" target="_blank">rOpenSci’s first AI policy</a>; or how to deal with large software submissions, resulting in our first <em>group</em> peer review.</p>
<h2>
Tools for everyone
</h2><p>Mark’s care for rOpenSci goes beyond software.
He researched alternatives to GitHub, sharing the results in <a href="https://ropensci.org/blog/2025/12/17/beyond-github/" rel="nofollow" target="_blank">Beyond GitHub</a>, a thoughtful look at what it would mean for a community like ours to depend less on a single platform.</p>
<p>He has also served on our <a href="https://ropensci.org/code-of-conduct/" rel="nofollow" target="_blank">Code of Conduct committee</a>, work that helps ensure rOpenSci remains a welcoming and safe community.</p>
<p>Mark also builds tools that help the wider R community.
The <a href="https://ropensci.org/blog/2024/11/26/allcontributors/" rel="nofollow" target="_blank">allcontributors</a> package, inspired by the allcontributors project, makes it easy to recognise everyone who contributes to a project, not just those who write code.
His work on <a href="https://ropensci.org/blog/2025/09/11/repometrics-call-help/" rel="nofollow" target="_blank">repometrics</a> explores how to understand the health and activity of software repositories, and his work on <a href="https://ropensci.org/blog/2024/09/17/deposits-in-the-wild/" rel="nofollow" target="_blank">deposits</a> looks at how research data and software can be deposited in trusted repositories.</p>
<h2>
Thank you, Mark
</h2><p>Mark’s work has shaped how we review software, support contributors, and build a more reliable and sustainable open science ecosystem.
Thank you, Mark, for building so much of the infrastructure and process that we rely on, and for leaving it in better shape than you found it.</p>
<p>If you have benefited from Mark’s work, reach out to <a href="https://ropensci.org/author/mark-padgham" rel="nofollow" target="_blank">Mark</a> and let him know!
You can keep following his work through his website and social media.</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://ropensci.org/blog/2026/10/21/mark-building-machinery-software-review/"> 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/10/mark-padgham-building-the-machinery-of-software-peer-review/">Mark Padgham: Building the Machinery of Software Peer Review</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404356</post-id>	</item>
		<item>
		<title>TKCat meets LLMs: an MCP server for well-documented knowledge resources</title>
		<link>https://www.r-bloggers.com/2026/10/tkcat-meets-llms-an-mcp-server-for-well-documented-knowledge-resources/</link>
		
		<dc:creator><![CDATA[Patrice Godard]]></dc:creator>
		<pubDate>Tue, 20 Oct 2026 22:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://patzaw.github.io/posts/tkcat-mcp.html</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>1 Documentation was always the point<br />
TKCat (Tailored Knowledge Catalog) is built on a simple idea: a knowledge resource is more than a set of tables. In TKCat, each resource is a modeled database (MDB) that contains:</p>
<p>a general description of t...</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/tkcat-meets-llms-an-mcp-server-for-well-documented-knowledge-resources/">TKCat meets LLMs: an MCP server for well-documented knowledge resources</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://patzaw.github.io/posts/tkcat-mcp.html"> Patrice Godard</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><img src="https://i1.wp.com/patzaw.github.io/posts/images/tkcat-mcp.jpeg?w=450&#038;ssl=1" class="img-fluid" alt="Illustration of TKCat MCP generated by gemini"  data-recalc-dims="1"></p>
<section id="documentation-was-always-the-point" class="level2" data-number="1">
<h2 data-number="1" class="anchored" data-anchor-id="documentation-was-always-the-point"><span class="header-section-number">1</span> Documentation was always the point</h2>
<p><a href="https://patzaw.github.io/TKCat/" rel="nofollow" target="_blank">TKCat</a> (Tailored Knowledge Catalog) is built on a simple idea: a knowledge resource is more than a set of tables. In TKCat, each resource is a <em>modeled database</em> (MDB) that contains:</p>
<ul>
<li>a <strong>general description</strong> of the resource (source, version, scope),</li>
<li>a <strong>data model</strong> made with <a href="https://patzaw.github.io/ReDaMoR/" rel="nofollow" target="_blank">ReDaMoR</a>, with a comment on every table and field, plus primary keys, foreign keys and indexes,</li>
<li><strong>collection members</strong>, which say which fields refer to the same kind of concept (genes, diseases, tissues…) and make it possible to integrate different MDBs.</li>
</ul>
<p>This documentation was designed from the start to be machine-ready, and it turns out to be exactly what a large language model (LLM) needs in order to work with the data correctly.</p>
</section>
<section id="what-is-mcp" class="level2" data-number="2">
<h2 data-number="2" class="anchored" data-anchor-id="what-is-mcp"><span class="header-section-number">2</span> What is MCP</h2>
<p>The <a href="https://modelcontextprotocol.io/" rel="nofollow" target="_blank">Model Context Protocol (MCP)</a>, introduced by Anthropic, is an open standard that allows AI assistants to securely connect to external data sources, tools, and workflows. Think of it as a universal adapter for AI: instead of writing custom integrations for every combination of LLM and database, MCP provides a standardized client-server architecture.</p>
<p>In this setup, an MCP client (such as Claude Desktop, Claude Code, or Posit Assistant) communicates with an MCP server using a common protocol over local or remote connections. The server exposes a defined set of “tools” and resources that the assistant can discover and invoke dynamically based on the user’s natural language request.</p>
<p>For TKCat, MCP is a natural fit. Rather than forcing developers to build bespoke chat interfaces or write brittle custom prompts for every catalog, TKCat 1.3.0 exposes its modeled databases directly through a standard MCP server. Any MCP-compatible assistant instantly gains the structured vocabulary, relationship maps, and security guardrails it needs to navigate, query, and synthesize your knowledge resources safely.</p>
</section>
<section id="whats-new-in-tkcat-1.3.0" class="level2" data-number="3">
<h2 data-number="3" class="anchored" data-anchor-id="whats-new-in-tkcat-1.3.0"><span class="header-section-number">3</span> What’s new in TKCat 1.3.0</h2>
<p>TKCat 1.3.0 includes a read-only MCP server. With it, AI assistants such as Claude Code, Claude Desktop, Posit Assistant or any other MCP client can explore and query the MDBs of a chTKCat instance (TKCat on ClickHouse).</p>
<p>The server comes in two forms:</p>
<ul>
<li><code>chTKCat_mcp_server()</code>: a <strong>local</strong> server, started by the MCP client on the user’s computer (stdio transport).</li>
<li><code>chTKCat_mcp_http_server()</code>: a <strong>remote</strong> server that runs as a service next to the chTKCat instance (Streamable HTTP transport). Users don’t need R or TKCat on their computer.</li>
</ul>
<p>The assistant has a small set of tools that match how a careful analyst works with an unfamiliar resource:</p>
<table class="caption-top table">
<colgroup>
<col style="width: 46%">
<col style="width: 53%">
</colgroup>
<thead>
<tr class="header">
<th>Step</th>
<th>Tools</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Find relevant resources</td>
<td><code>list_mdbs</code>, <code>search_tkcat</code></td>
</tr>
<tr class="even">
<td>Understand them</td>
<td><code>describe_mdb</code>, <code>describe_tables</code>, <code>list_collections</code></td>
</tr>
<tr class="odd">
<td>Look at the data</td>
<td><code>preview_table</code></td>
</tr>
<tr class="even">
<td>Answer the question</td>
<td><code>query_mdb</code> (ClickHouse SQL), <code>get_matrix_values</code></td>
</tr>
</tbody>
</table>
</section>
<section id="why-documented-databases-make-a-difference" class="level2" data-number="4">
<h2 data-number="4" class="anchored" data-anchor-id="why-documented-databases-make-a-difference"><span class="header-section-number">4</span> Why documented databases make a difference</h2>
<p>Ask an LLM a question about a database it has never seen and it will guess: it may infer table meanings from their names, assume joins and get the semantics of a column wrong. With TKCat, the assistant doesn’t need to guess:</p>
<ul>
<li><strong>It finds the right resource.</strong> MDB descriptions and keyword search over table and field comments point it to the resources that matter, even in a catalog of dozens of databases.</li>
<li><strong>It understands the data.</strong> Field comments explain codes, units and conventions. Foreign keys show how tables can be joined. So the SQL it writes follows the actual data model.</li>
<li><strong>It integrates resources.</strong> Collections say when, for example, a gene identifier in one MDB can be compared with a gene identifier in another one, and under which conditions (same source, same organism). The assistant can then answer questions that span several resources in a single query.</li>
<li><strong>Its answers can be checked.</strong> Every answer is built from explicit SQL queries on identified resources and versions. You can read them, rerun them in R and reuse them.</li>
</ul>
<p>A typical exchange looks like this:</p>
<blockquote class="blockquote">
<p><strong>User:</strong> Which drugs target genes associated with the phenotypes of this disease?</p>
<p><strong>Assistant:</strong> <em>searches the catalog → reads the descriptions of the phenotype, variant and drug MDBs → checks the collections that link them → previews a few records → runs a cross-MDB SQL query</em> → returns the answer, with the queries it ran and the resources it used.</p>
</blockquote>
<p>The better the documentation, the better the answers. The effort put into documenting a resource once now pays off every time someone asks a question about it, in plain language.</p>
</section>
<section id="built-for-shared-governed-environments" class="level2" data-number="5">
<h2 data-number="5" class="anchored" data-anchor-id="built-for-shared-governed-environments"><span class="header-section-number">5</span> Built for shared, governed environments</h2>
<p>Knowledge resources often have different access rules, and they are hosted by an organization rather than on a personal laptop. The MCP server was designed with this in mind:</p>
<ul>
<li><strong>Read-only by design</strong>: the assistant can query data but can’t write, create, delete or change grants.</li>
<li><strong>Per-user access</strong>: with the remote server, users log in with their own chTKCat credentials, which ClickHouse checks. Each user only sees the MDBs they are allowed to read.</li>
<li><strong>Controlled output</strong>: limits on returned rows, output size and query execution time keep tool calls small and fast.</li>
<li><strong>Scalable deployment</strong>: the remote server is stateless, so several instances can run behind a reverse proxy such as nginx, and tool calls are logged.</li>
</ul>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://patzaw.github.io/TKCat/articles/mcp-architecture.svg" class="img-fluid figure-img"></p>
<figcaption>Remote MCP server architecture</figcaption>
</figure>
</div>
<p>As with any AI service, only expose MDBs whose content can be shared with the assistant under your organization’s policies.</p>
</section>
<section id="getting-started" class="level2" data-number="6">
<h2 data-number="6" class="anchored" data-anchor-id="getting-started"><span class="header-section-number">6</span> Getting started</h2>
<p>The best place to start is the new <strong><a href="https://patzaw.github.io/TKCat/articles/TKCat-EndToEnd.html" rel="nofollow" target="_blank">TKCat End-to-End vignette</a></strong>. It covers the whole process:</p>
<ol type="1">
<li>setting up a ClickHouse instance for TKCat,</li>
<li>preparing example MDBs (HPO, ClinVar, ChEMBL),</li>
<li>pushing them to ClickHouse and querying them from R,</li>
<li>deploying the MCP server (local and remote, with systemd and nginx),</li>
<li>configuring MCP clients (Claude Code, Claude Desktop, Posit Assistant).</li>
</ol>
<p>For reference details on the server, its tools and its parameters, see the <a href="https://patzaw.github.io/TKCat/articles/TKCat-MCP.html" rel="nofollow" target="_blank">MCP server vignette</a>.</p>
<p>For a complete introduction to TKCat, refer to the <a href="https://patzaw.github.io/TKCat/articles/TKCat.html" rel="nofollow" target="_blank">main vignette</a>.</p>
<p>Feedback and issues are welcome on <a href="https://github.com/patzaw/TKCat/issues" rel="nofollow" target="_blank">GitHub</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://patzaw.github.io/posts/tkcat-mcp.html"> Patrice Godard</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/tkcat-meets-llms-an-mcp-server-for-well-documented-knowledge-resources/">TKCat meets LLMs: an MCP server for well-documented knowledge resources</a>]]></content:encoded>
					
		
		<enclosure url="https://patzaw.github.io/posts/images/tkcat-mcp.jpeg" length="0" type="image/jpeg" />

		<post-id xmlns="com-wordpress:feed-additions:1">404354</post-id>	</item>
		<item>
		<title>J’avais écrit les règles. Je n’avais pas encore vérifié leurs effets.</title>
		<link>https://www.r-bloggers.com/2026/10/javais-ecrit-les-regles-je-navais-pas-encore-verifie-leurs-effets/</link>
		
		<dc:creator><![CDATA[Boris Guarisma]]></dc:creator>
		<pubDate>Tue, 20 Oct 2026 16:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://blog.bguarisma.com/regles-ecrites-effets-verifies-agents-ia</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; "> REX d’une firme minimale : auditer sa veille IA, protéger ses notes et mesurer les limites du contrôle humain.</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/javais-ecrit-les-regles-je-navais-pas-encore-verifie-leurs-effets/">J’avais écrit les règles. Je n’avais pas encore vérifié leurs effets.</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.bguarisma.com/regles-ecrites-effets-verifies-agents-ia"> Foundations</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>Je dirige seul Qognito, une entreprise de conseil et de formation. Pour réaliser certaines activités de mon cœur de métier, je coordonne plusieurs équipes d’agents IA, <strong>mes « squads »</strong>. C’est ma manière de faire fonctionner une firme minimale : déléguer une partie du travail tout en restant responsable des orientations, des livrables et des engagements pris auprès de mes clients.</p>
<p><strong>La veille sur l’IA alimente ce travail.</strong> Une note peut m’aider à actualiser une formation, à examiner le besoin d’un prospect ou à préparer une proposition. Sa valeur tient à ce que je peux en faire ensuite.</p>
<p>Je conserve ces notes dans <strong>Obsidian</strong>, une application de gestion de connaissances. Des liens internes, les <em>wikilinks</em>, les relient entre elles : je peux retrouver une analyse et la rapprocher d’autres sujets. Ces liens facilitent l’exploration ; ils ne garantissent pas que plusieurs notes apportent des preuves indépendantes. Une erreur peut ainsi devenir un appui pour un raisonnement ultérieur.</p>
<p>J’avais organisé la production avec soin. Un agent <strong>prépare la source</strong>, un autre <strong>rédige l’analyse</strong>, un troisième <strong>évalue la note</strong>. Je décide de sa publication dans ma base. Voici le circuit attendu :</p>
<img src="https://i2.wp.com/cdn.hashnode.com/uploads/covers/61bc41016916c3593a6b04c3/f531f69d-8e91-4a86-a00e-cf9707daf210.png?w=578&#038;ssl=1" alt="Circuit attendu : préparation, rédaction, évaluation distincte puis décision humaine avant publication dans Obsidian." style="display:block;margin:0 auto" data-recalc-dims="1" />

<p><em>Ce schéma décrit les responsabilités prévues. Il reste à vérifier que les outils respectent ce circuit.</em></p>
<p>Dans ma formation <strong>SAGA-IA</strong>, je demande aux décideurs d’examiner ce qu’ils délèguent, les preuves disponibles et le coût du contrôle. <strong>J’ai soumis mon propre dispositif à ces exigences.</strong> L’audit a commencé par une découverte très simple.</p>
<h2>Les règles pouvaient changer avec celui qui les appliquait</h2>
<p>Les fichiers qui définissaient les consignes, les critères du juge et les contrôles étaient <strong>modifiables par les agents</strong>. Celui qui produisait une note avait donc techniquement la possibilité de changer les critères utilisés pour l’évaluer.</p>
<p>Je n’avais pas constaté qu’un agent l’avait fait. Mais la séparation des rôles reposait sur une consigne que les droits d’accès ne faisaient pas respecter.</p>
<p>J’ai fait protéger ces fichiers. Un agent peut proposer une modification ; sa mise en place exige une intervention humaine avec les droits d’administration. <strong>Dix tentatives de modification, par des voies différentes, ont ensuite été refusées.</strong> Les règles sont restées intactes.</p>
<p>Ces premières corrections précédaient la campagne consacrée aux protections, appelée « Phase A » dans mon audit. Elles avaient déplacé la question : les règles tenaient mieux, <strong>mais que pouvait-il encore arriver aux documents sur lesquels je m’appuyais ?</strong></p>
<h2>Publier une note ne suffisait pas à la préserver</h2>
<p>Au départ, une note rejoignait la base dès sa rédaction, avant le verdict du juge. J’avais ensuite instauré une étape d’attente, puis une autorisation humaine de publication. Le premier cycle réel avait suivi ce circuit : <strong>neuf sources traitées, neuf notes examinées et publiées par mes soins.</strong></p>
<p>Pourtant, une note déjà publiée pouvait encore être modifiée. Une source aussi. Le contrôle existant ne repérait pas ces changements. <strong>Une approbation donnée hier ne disait donc pas ce que je retrouverais demain.</strong></p>
<p>La Phase A a traité ce problème. Sur une copie de travail, trois altérations volontaires — une note modifiée, une note ajoutée hors circuit et une transcription modifiée — ont été détectées par <strong>un nouveau contrôle</strong> comparant les documents à une référence conservée. <strong>Des verrous</strong> ont également été posés sur les notes publiées et les sources préparées.</p>
<p>J’ai accepté une discipline pour moi aussi : plus de retouche directe dans Obsidian. Les changements doivent passer par le circuit prévu et laisser une trace. Sinon, comment distinguer ma correction d’une modification anormale ?</p>
<p>Préserver le contenu ne suffit toutefois pas à le rendre juste. Lors des premiers essais, le juge avait relevé « usually » traduit par « toujours », alors que les contrôles automatiques n’avaient rien signalé. <strong>Une nuance devenue certitude peut changer une décision.</strong></p>
<p>Le programme compare les documents ; le juge examine le sens ; je conserve la décision de publication. <strong>Cette répartition rend les responsabilités identifiables. Elle ne dispense pas de vérifier la qualité du jugement.</strong></p>
<h2>J’ai dû examiner mes propres examens</h2>
<p><strong>Un banc d’essai est une répétition sur des cas choisis, dont certains contiennent volontairement des pièges.</strong> J’en avais utilisé pour éprouver la veille. <strong>Deux constats</strong> m’ont obligé à revoir la confiance accordée à leurs résultats.</p>
<p>Le <strong>premier</strong> concernait le corrigé : les réponses attendues restaient accessibles aux agents. Imaginez un examen dont les candidats pourraient ouvrir les solutions. Cela fragilise l’évaluation, même sans preuve qu’ils les aient consultées. <strong>L’accès au corrigé a été protégé, puis son refus éprouvé sur le poste.</strong></p>
<p>Le <strong>second</strong> concernait les permissions. La copie utilisée pour certains essais était plus restrictive que l’environnement réel. Une action pouvait y demander un accord alors qu’elle passait librement au quotidien. Les résultats décrivaient bien les essais réalisés ; ils ne suffisaient pas à établir la même protection en usage réel.</p>
<p>La dernière étape de la Phase A devait donc réunir les protections dans une épreuve avec les permissions de production. Il fallait observer les tentatives, identifier ce qui les arrêtait et vérifier leurs effets sur les documents.</p>
<h2>Vingt tentatives, et une limite qui reste visible</h2>
<p>Le 19 octobre, <strong>vingt tentatives ont été exécutées et tracées</strong> dans les deux environnements d’agents utilisés. Les essais de modification de documents protégés et d’accès au corrigé ont rencontré des refus.</p>
<p>Deux créations de fichiers hors du circuit de publication ont toutefois réussi dans l’un des environnements. Le contrôle les a détectées ; elles ont ensuite été supprimées. <strong>La vérification finale a confirmé le retour de la base à son état initial.</strong></p>
<p><strong>Vingt tentatives documentées ne signifient donc pas vingt actions empêchées.</strong> Dans le fonctionnement courant, un ajout hors circuit peut rester présent jusqu’au contrôle déclenché par la prochaine publication. Cette limite reste ouverte.</p>
<p>La Phase A est close, avec une réserve précise : le prochain cycle réel doit encore montrer qu’une publication autorisée aboutit à une note effectivement verrouillée. Les refus constatés pendant les essais ne remplacent pas cette vérification de bout en bout.</p>
<p>L’évaluation de qualité menée en parallèle a donné sept cas réussis sur dix, avec une fausse alerte de fidélité et deux cas dont la portée restait à trancher. <strong>Protéger les documents n’établit pas que leur contenu s’est amélioré. Un examen rétrospectif des notes publiées reste prévu.</strong></p>
<h2>Le temps que le chronomètre ne voyait pas</h2>
<p>Pour comprendre le coût de cette maîtrise, il faut revenir au premier cycle réel. L’outil avait enregistré une à trois secondes par publication. J’avais pourtant consacré <strong>neuf minutes à relire les neuf notes avant de l’ouvrir.</strong> Ce temps a dû être déclaré séparément.</p>
<p>Ces neuf minutes suffisaient à ma décision de publier ; elles ne constituaient pas une comparaison complète de chaque note avec sa source. Une validation humaine doit préciser ce qui a réellement été vérifié et avec quel temps disponible.</p>
<p>La même exigence vaut pour les corrections. Pendant les premiers travaux, j’ai accepté un défaut de recoupement compté deux fois : une seule occurrence avait été constatée, et le script envisagé pour la traiter aurait été coûteux et lui-même faillible. <strong>Chaque anomalie ne justifie pas une règle supplémentaire.</strong></p>
<p>J’ai aussi fixé <strong>des critères d’arrêt, des responsables et des conditions de réexamen.</strong> Ces dispositions devront vivre dans l’usage. <strong>À ce stade, je n’ai démontré aucun gain global de rentabilité</strong> : la relecture, les reprises, la maintenance et l’audit font partie du coût à apprécier.</p>
<h2>Ce que je peux désormais justifier</h2>
<p>Le chemin parcouru tient en trois étapes :</p>
<img src="https://i1.wp.com/cdn.hashnode.com/uploads/covers/61bc41016916c3593a6b04c3/cbfd4c26-b900-40b4-ab65-381559eb0bfe.png?w=578&#038;ssl=1" alt="Progression : règles définies, protections éprouvées et limites identifiées, puis publication réelle et qualité encore à observer." style="display:block;margin:0 auto" data-recalc-dims="1" />

<p><em>Les protections testées apportent des preuves circonscrites. La dernière étape reste à observer.</em></p>
<p>Je peux mieux expliquer ce que je confie aux agents, ce qui résiste aux tentatives testées et ce qui demande encore un contrôle. <strong>Ma responsabilité de dirigeant consiste aussi à décider quelles incertitudes sont acceptables et lesquelles doivent limiter la délégation.</strong></p>
<p>Si vous coordonnez vous aussi une activité avec des agents, mon livre blanc gratuit <a href="https://qognito.io/livres-blancs/la-facture-fantome-ia/" rel="nofollow" target="_blank"><em>La facture fantôme de l’IA</em></a> propose une première lecture sur la supervision, les coûts complets et l’usage du temps gagné.</p>
<p>Pour travailler ces décisions, <a href="https://qognito.io/formations/saga-ia/volume-1/" rel="nofollow" target="_blank"><em>SAGA-IA — Volume 1</em></a> réunit le diagnostic de délégation et l’évaluation de la viabilité économique, avec des cas et des exercices. C’est le prolongement de ce REX : apprendre à justifier ce que l’on engage, les preuves que l’on demande et les conditions auxquelles on poursuivra.</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.bguarisma.com/regles-ecrites-effets-verifies-agents-ia"> Foundations</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/javais-ecrit-les-regles-je-navais-pas-encore-verifie-leurs-effets/">J’avais écrit les règles. Je n’avais pas encore vérifié leurs effets.</a>]]></content:encoded>
					
		
		<enclosure url="https://cdn.hashnode.com/uploads/covers/61bc41016916c3593a6b04c3/b4541293-3a7d-483d-a286-2f061c4ecf7c.jpg" length="0" type="image/jpeg" />

		<post-id xmlns="com-wordpress:feed-additions:1">404337</post-id>	</item>
		<item>
		<title>Get largest (e.g. mainland) polygons from a map</title>
		<link>https://www.r-bloggers.com/2026/10/get-largest-e-g-mainland-polygons-from-a-map/</link>
		
		<dc:creator><![CDATA[A.M. Barbosa]]></dc:creator>
		<pubDate>Tue, 20 Oct 2026 13:58:12 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">http://modtools.wordpress.com/?p=3571</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; "> I often need to get mainland from country / administrative region polygons, so I finally decided to write a function to help automate it. It selects the N largest feature(s) in a polygons or lines map. Here’s the code: The … Continue reading →</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/get-largest-e-g-mainland-polygons-from-a-map/">Get largest (e.g. mainland) polygons from a map</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://modtools.wordpress.com/2026/10/20/get-largest-e-g-mainland-polygons-from-a-map/"> modTools</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">I often need to get mainland from country / administrative region polygons, so I finally decided to write a function to help automate it. It selects the N largest feature(s) in a polygons or lines map. Here’s the code:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
selectLargest &lt;- function(x,  # terra SpatVector polygons or lines                          n = 1,  # number of largest features to retain                          dissolve = FALSE,  # whether to group contiguous features                          singleparts = TRUE  # whether to disaggregate multi-part features) {  stopifnot(inherits(x, &quot;SpatVector&quot;),            terra::geomtype(x) %in% c(&quot;polygons&quot;, &quot;lines&quot;))  if (dissolve) {    x &lt;- terra::aggregate(x)  }  if (singleparts) {    x &lt;- terra::disagg(x)  }  if (terra::is.polygons(x)) {    size_rank &lt;- order(terra::expanse(x), decreasing = TRUE)  } else if (terra::is.lines(x)) {    size_rank &lt;- order(terra::perim(x), decreasing = TRUE)  }  x[size_rank[1:n], ]}</pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">The dissolving of internal borders is optional. Here are some usage examples:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
library(terra)library(geodata)# import a USA map and get the conterminous and continental parts:usa &lt;- geodata::gadm(&quot;United States&quot;, level = 1, path = tempdir())mainland_usa &lt;- selectLargest(usa, dissolve = TRUE)contin_usa &lt;- selectLargest(usa, dissolve = TRUE, n = 2)  # two largest polygons, to include Alaskaplot(usa, main = &quot;USA&quot;)plot(mainland_usa, main = &quot;Conterminous USA&quot;)plot(contin_usa, main = &quot;Continental USA&quot;)# import a UK map and get Great Britain:uk &lt;- geodata::gadm(&quot;United Kingdom&quot;, path = tempdir())great_britain &lt;- selectLargest(uk, dissolve = TRUE)plot(uk, main = &quot;UK&quot;)plot(great_britain, main = &quot;Great Britain&quot;)# import a Portugal + Spain map and get the Iberian Peninsula:ptes &lt;- geodata::gadm(c(&quot;Portugal&quot;, &quot;Spain&quot;), path = tempdir())iberia &lt;- selectLargest(ptes, dissolve = TRUE)plot(ptes, main = &quot;Portugal and Spain&quot;)plot(iberia, main = &quot;Iberian Peninsula&quot;)</pre>
		</div>
	</div>
</div>


<figure class="wp-block-image size-large"><a href="https://i0.wp.com/modtools.wordpress.com/wp-content/uploads/2026/10/mainlands-1.png?ssl=1" rel="nofollow" target="_blank"><img loading="lazy" data-attachment-id="3578" data-permalink="https://modtools.wordpress.com/2026/10/20/get-largest-e-g-mainland-polygons-from-a-map/mainlands-2/" data-orig-file="https://modtools.wordpress.com/wp-content/uploads/2026/10/mainlands-1.png" data-orig-size="1353,1614" data-comments-opened="1" data-image-title="mainlands" data-image-description="" data-image-caption="" data-large-file="https://modtools.wordpress.com/wp-content/uploads/2026/10/mainlands-1.png?w=584" src="https://i0.wp.com/modtools.wordpress.com/wp-content/uploads/2026/10/mainlands-1.png?w=450&#038;ssl=1" alt="" class="wp-image-3578" srcset_temp="https://i0.wp.com/modtools.wordpress.com/wp-content/uploads/2026/10/mainlands-1.png?w=450&#038;ssl=1 858w, https://modtools.wordpress.com/wp-content/uploads/2026/10/mainlands-1.png?w=126 126w, https://modtools.wordpress.com/wp-content/uploads/2026/10/mainlands-1.png?w=251 251w, https://modtools.wordpress.com/wp-content/uploads/2026/10/mainlands-1.png?w=768 768w, https://modtools.wordpress.com/wp-content/uploads/2026/10/mainlands-1.png 1353w" sizes="(max-width: 858px) 100vw, 858px" data-recalc-dims="1" /></a></figure>



<p class="wp-block-paragraph">As usual, feedback is welcome!</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://modtools.wordpress.com/2026/10/20/get-largest-e-g-mainland-polygons-from-a-map/"> modTools</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/get-largest-e-g-mainland-polygons-from-a-map/">Get largest (e.g. mainland) polygons from a map</a>]]></content:encoded>
					
		
		<enclosure url="https://2.gravatar.com/avatar/e1a349777384e70d2eb7ee3f26788864b1c880ec271cfc1cb8f9d8cc3000cd11?s=96&#038;d=retro&#038;r=G" length="0" type="" />
<enclosure url="https://modtools.wordpress.com/wp-content/uploads/2026/10/mainlands-1.png?w=858" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404339</post-id>	</item>
		<item>
		<title>Getting the pipe shortcut to work in Positron</title>
		<link>https://www.r-bloggers.com/2026/10/getting-the-pipe-shortcut-to-work-in-positron/</link>
		
		<dc:creator><![CDATA[Giles]]></dc:creator>
		<pubDate>Tue, 20 Oct 2026 06:34:12 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://www.gilesd-j.com/?p=4785</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; "> For the life of me, I couldn't get Ctrl+Shift+M to work for inserting pipes (&#124;__) when editing a Quarto document in Positron visual mode. After countless hours fiddling with keybindings.json, I developed a functional, but obtuse workaround for Windows. It's not pretty, but it works. This post outlines ...</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/getting-the-pipe-shortcut-to-work-in-positron/">Getting the pipe shortcut to work in Positron</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/10/20/getting-the-pipe-shortcut-to-work-in-positron/"> 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"><em><strong>TLDR:</strong> For the life of me, I couldn’t get <code>Ctrl+Shift+M</code> to work for inserting pipes (<code>|&gt;</code>) when editing a Quarto document in Positron visual mode. After countless hours fiddling with <code>keybindings.json</code>, I developed a functional, but obtuse workaround for Windows. It’s not pretty, but it works. This post outlines the approach in case it helps others facing the same problem. </em></p>



<p class="wp-block-paragraph"><strong>Note: </strong>this problem is already on the radar of the development team, so will no doubt be fixed soon. I’ve also only outlined a workaround for Windows. Feel free to <a href="https://www.gilesd-j.com/contact/" rel="nofollow" target="_blank">reach out</a> if you’ve found a better solution (or a solution that works on other platforms) and I’ll add this to the post.</p>



<h2 class="wp-block-heading">The problem</h2>



<p class="wp-block-paragraph">I’m a big fan of Quarto as it provides a seamless way to mix data, code and narrative text without ever leaving my IDE. I’ve also been trying to move from RStudio to Positron as I find it snappier, works better with AI tools <em>and </em>works well with Python. Unfortunately, Positron is still experiencing growing pains as a newish IDE, including some shortcuts not working when editing Quarto documents in visual mode. </p>



<p class="wp-block-paragraph">This isn’t a new issue (e.g: <a href="https://github.com/orgs/quarto-dev/discussions/11991" rel="nofollow" target="_blank">1</a>, <a href="https://github.com/posit-dev/positron/issues/1955" rel="nofollow" target="_blank">2</a> and <a href="https://github.com/posit-dev/positron/issues/1805" rel="nofollow" target="_blank">3</a>), so is sure to be fixed in the not-too-distant future (likely just after I post this). But, after countless hours messing around with keybindings.json and hassling people from Posit (apologies, Isabella) I asked Claude if it had a solution. </p>



<p class="wp-block-paragraph">It did.</p>



<p class="wp-block-paragraph">And it works.</p>



<p class="wp-block-paragraph">So, I thought I’d share it here, despite the fact it <em>doesn’t spark joy</em> with the minimalist inside of me:</p>



<h2 class="wp-block-heading">Duct tape and thumbtacks: intercept the shortcut before it reaches Positron</h2>



<p class="wp-block-paragraph">As far as I can tell, the problem comes from Positron / VSCode intercepting some shortcuts when Quarto is in visual mode, which is probably why adjusting the settings did nothing for me. As a (temporary) solution to this, Claude suggested installing a program called <a href="https://www.autohotkey.com/" rel="nofollow" target="_blank">AutoHotkey v2</a> and creating a script operates when Positron is active. </p>



<p class="wp-block-paragraph">Obviously, as an independent-minded person I did what I’m told and here’s how you can do the same:</p>



<h3 class="wp-block-heading">1. Install AutoHotkey v2</h3>



<p class="wp-block-paragraph">Get Version 2 from <a href="https://www.autohotkey.com/" rel="nofollow" target="_blank">autohotkey.com</a> (the code below will not run under v1).</p>



<h3 class="wp-block-heading">2. Create the script</h3>



<p class="wp-block-paragraph"> </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/10/image-2.png?w=450&#038;ssl=1" alt="" class="wp-image-4793" srcset_temp="https://i2.wp.com/www.gilesd-j.com/wp-content/uploads/2026/10/image-2.png?w=450&#038;ssl=1 631w, https://www.gilesd-j.com/wp-content/uploads/2026/10/image-2-300x132.png 300w" sizes="auto, (max-width: 631px) 100vw, 631px" data-recalc-dims="1" /></figure>



<p class="wp-block-paragraph"> </p>



<p class="wp-block-paragraph">Open Autohotkey2 (or AutoHotKey Dash) and select <strong>New</strong> <strong>Script</strong> to create a new blank .ahk script in a secluded folder of your choice. Give it a name like ‘positron-keys.ahk’ so future <em>you </em>knows what it’s doing there and use a text editor to replace the contents of the file with the code below (or the equivalent depending on your shortcut preferences):</p>



<p class="wp-block-paragraph"><em>(SendText sends the literal characters to the Quarto document, so nothing gets re-interpreted by Positron, while #HotIf restricts the behavior to Positron, so it doesn’t interfere with other applications.)</em></p>



<pre>#Requires AutoHotkey v2.0
#SingleInstance Force

#HotIf WinActive(&quot;ahk_exe Positron.exe&quot;)

^+m::SendText(&quot; |&gt; &quot;)    ;Ctrl+Shift+M -&gt; native pipe
!-::SendText(&quot; &lt;- &quot;)     ;Alt+-        -&gt; assignment arrow

#HotIf</pre>



<h3 class="wp-block-heading">3. Run it</h3>



<p class="wp-block-paragraph">To run the code and launch AutoHotkey, open the ‘positron-keys.ahk’ file. If it worked, a green <strong>H</strong> should appear in the system tray. </p>



<h3 class="wp-block-heading">4. Test it</h3>



<p class="wp-block-paragraph">To test it you can now open Positron and try using Ctrl + Shift + M to insert a pipe in a Quarto when in visual mode. Hopefully it works, as this is the only idea covered in this post. </p>



<h3 class="wp-block-heading">5. Make it persistent</h3>



<p class="wp-block-paragraph">To avoid needing to load the code each time you want to use Positron, you can then add a shortcut to the script that loads at startup. For Windows, you can open the startup folder by typing <code>Win+R</code> and executing the command ‘<code>shell:startup</code>‘. Right-drag <code>positron-keys.ahk</code> into the folder and choose <strong>Create shortcut here</strong>. </p>



<p class="wp-block-paragraph"><strong>Note: </strong>if you have other solutions, please <a href="https://www.gilesd-j.com/contact/" rel="nofollow" target="_blank">let me know</a>. I’ll happily update this post if others find it useful. </p>



<p class="wp-block-paragraph"><strong>AI use:</strong> Claude suggested this solution and wrote the first draft of this post, which I heavily edited. </p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://www.gilesd-j.com/2026/10/20/getting-the-pipe-shortcut-to-work-in-positron/" rel="nofollow" target="_blank">Getting the pipe shortcut to work in Positron</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/10/20/getting-the-pipe-shortcut-to-work-in-positron/"> 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/10/getting-the-pipe-shortcut-to-work-in-positron/">Getting the pipe shortcut to work in Positron</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404333</post-id>	</item>
		<item>
		<title>Celebrating 15 Years of rOpenSci: Then, Now &#038; Next</title>
		<link>https://www.r-bloggers.com/2026/10/celebrating-15-years-of-ropensci-then-now-next/</link>
		
		<dc:creator><![CDATA[rOpenSci]]></dc:creator>
		<pubDate>Tue, 20 Oct 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://ropensci.org/blog/2026/10/20/15yo-events/</guid>

					<description><![CDATA[<p>In June 2026, rOpenSci marked its 15th anniversary with three community celebration events: a themed coworking event and two extra events. This post captures the reflections and ideas shared by participants across the two special gatherings named &#038;ldq...</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/celebrating-15-years-of-ropensci-then-now-next/">Celebrating 15 Years of rOpenSci: Then, Now & Next</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/10/20/15yo-events/"> 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>In June 2026, rOpenSci marked its 15th anniversary with three community celebration events: a themed coworking event and two extra events. This post captures the reflections and ideas shared by participants across the two special gatherings named <strong>“rOpenSci Community Celebration: Then, Now &#038; Next”</strong> that were held to accommodate different time zones.</p>
<h2>
How the events worked
</h2><p>Our goal was to create a space for people to have conversations, get to know each other and also reflect on rOpenSci’s past, present and future.</p>
<p>After a welcome and a brief introduction, participants joined three successive breakout rounds of 15 minutes each, one for each theme: the <strong>past</strong>, the <strong>present</strong>, and the <strong>future</strong>. Between rounds, attendees took part in a <strong>Scavenger Hunt</strong> (a regular feature of our <a href="https://ropensci.org/blog/2023/06/21/coworking/" rel="nofollow" target="_blank">co-working sessions</a>), finding something in their physical surroundings that matched a given prompt.</p>
<p>The Scavenger Hunt is a fun moment together, and we also used it as a natural prompt for introductions at the start of the next breakout room — “what did you find?” becomes a nice and easy opening for conversations.</p>
<p>As in our community calls, workshops and co-working sessions, a shared document served as the place where everyone could take notes in real time, add thoughts, and keep contributing for a couple of days after the event ended. <a href="https://ropensci.org/author/noam-ross/" rel="nofollow" target="_blank">Noam Ross</a> named this <em>“process is product”</em> approach, caring about <em>how</em> things happen, not just <em>what</em> happens, as one of rOpenSci’s defining traits.</p>
<div class="row">
<div class="col-md-6">
<figure><img src="https://i1.wp.com/ropensci.org/blog/2026/10/20/15yo-events/15yoevents.jpg?w=578&#038;ssl=1"
alt="Zoom meeting with 15 years old welcoming slide and attendees to the event" data-recalc-dims="1"><figcaption>
<p>First event celebrating 15 years of rOpenSci</p>
</figcaption>
</figure>
</div>
<div class="col-md-6">
<figure><img src="https://i2.wp.com/ropensci.org/blog/2026/10/20/15yo-events/15yosecond_event.jpg?w=578&#038;ssl=1"
alt="Second Zoom meeting with 15 years old welcoming slide and attendees to the event" data-recalc-dims="1"><figcaption>
<p>Second meeting to celebrate 15 years of rOpenSci</p>
</figcaption>
</figure>
</div>
</div>
<h2>
Round 1 &#8211; The Past: What made rOpenSci special?
</h2><p>The first breakout asked participants how they first connected with rOpenSci and what their favourite memory was.</p>
<p>Several people traced their connection back to a talk about rOpenSci or to a personal introduction from a community member who made them feel welcome from the start.</p>
<p>The <a href="https://ropensci.org/tags/unconf/" rel="nofollow" target="_blank"><strong>unconferences</strong></a> were remembered with particular warmth and excitement: the chance to sit next to people, see how they work, talk about R, and be part of something as it was taking shape. <em>“Meeting my heroes”</em>, as one participant put it.</p>
<p>Other things people named as favourite:</p>
<ul>
<li><a href="https://ropensci.org/software-review/" rel="nofollow" target="_blank"><strong>The peer-review process</strong></a>: praised by editors, reviewers and authors alike, both for the quality of the process and for the connections it created.</li>
</ul>
<blockquote>
<p>Being invited to write a blog post after a first package review … made me feel very cool and a valued part of the community.&#8221; &#8211; <a href="https://ropensci.org/author/alex-koiter/" rel="nofollow" target="_blank">Alex Koiter</a></p>
</blockquote>
<ul>
<li>
<p><a href="https://devguide.ropensci.org/" rel="nofollow" target="_blank"><strong>The dev guide</strong></a>: a detailed and evolving documentation with knowledge that isn’t easily found elsewhere.</p>
</li>
<li>
<p><a href="https://ropensci.org/champions/" rel="nofollow" target="_blank"><strong>The Champions Program</strong></a> and <a href="https://ropensci.org/blog/2026/06/02/ftc-guide/" rel="nofollow" target="_blank"><strong>the mini-hackathons</strong></a>, spaces that provide friendly and guided entry points for open source contributors and R package developers.</p>
</li>
<li>
<p><a href="https://ropensci.org/coworking" rel="nofollow" target="_blank"><strong>Co-working sessions</strong></a> are a place were you can find “your people,” a place where you don’t need to explain what R is.</p>
</li>
</ul>
<blockquote>
<p>It’s a satisfying community ‘distraction/procrastination’ from our normal jobs, that keeps us from being lonely and brings us together from different worlds to work on something that’s benefitting people.&#8221; &#8211; <a href="https://ropensci.org/author/ronny-hernandez-mora/" rel="nofollow" target="_blank">Ronny Hernandez Mora</a></p>
</blockquote>
<h2>
Round 2 &#8211; The Present: What does rOpenSci do well?
</h2><p>The second round invited reflection on what rOpenSci does well today and what participants want to get more involved in.</p>
<p>The most common answer was <strong>creating a welcoming space</strong>.
Participants then commented on the different places where they experience the friendly and welcoming community:</p>
<ul>
<li>
<p><a href="https://ropensci.org/coworking" rel="nofollow" target="_blank"><strong>Co-working</strong></a> came up again here, noted as something other communities have adopted from rOpenSci.</p>
</li>
<li>
<p>The <strong>Slack workspace</strong> and the atmosphere it creates: a common space for learning, getting advice, sharing ideas, and feeling part of something larger.</p>
</li>
<li>
<p><strong>Good documentation</strong> for package submission, which lowers the barrier to participating.</p>
</li>
<li>
<p>Some participants expressed interest in getting more involved in the <strong>Champions Program</strong>, along with honest questions about whether the entry points are accessible enough.</p>
</li>
<li>
<p>The sense that rOpenSci’s quality in different activities comes from its community participation.</p>
</li>
</ul>
<blockquote>
<p>You can jump into a role and the community will support you. If there is anything you want to do, start doing it and you will get the support you need.&#8221; &#8211; <a href="https://ropensci.org/author/adam-sparks/" rel="nofollow" target="_blank">Adam Sparks</a></p>
</blockquote>
<h2>
Round 3 &#8211; The Future: What comes next?
</h2><p>The third round asked what participants would like to see rOpenSci do next, and what ideas from other communities could be worth adopting or partnering on.</p>
<p><strong>In-person and hybrid gatherings</strong> were a recurring theme: local meetups, local chapters, satellite events at larger R conferences like <a href="https://www.r-project.org/conferences/" rel="nofollow" target="_blank">useR!</a>, and the <em>return of unconferences</em>, potentially co-organised with partner communities.</p>
<p>Other ideas from across the rooms:</p>
<ul>
<li>
<p><strong>First-time contributor hackathons</strong>, possibly with a GenAI focus</p>
</li>
<li>
<p>A <strong>24-hour unconference</strong> modeled on formats used by other communities like <a href="https://sortee.org/" rel="nofollow" target="_blank">SORTEE</a></p>
</li>
<li>
<p><strong>Career panels</strong> showing the range of roles in research software engineering and data-centric work</p>
</li>
<li>
<p>An rOpenSci-led package for R package development, like <a href="https://usethis.r-lib.org/" rel="nofollow" target="_blank">usethis</a>, that supports GitLab, Codeberg, and other platforms beyond GitHub</p>
</li>
<li>
<p>Deeper <strong>coalitions with open science organizations</strong></p>
</li>
<li>
<p>Connecting <strong>package authors with graphic designers</strong> to help them with their image and visual identity</p>
</li>
<li>
<p>Exploring <strong><a href="https://zarr.dev/" rel="nofollow" target="_blank">Zarr</a> in R</strong> as an area where rOpenSci could help shape the ecosystem</p>
</li>
<li>
<p>Better serving <strong>very beginners</strong>, the people who are still asking “what is R, anyway?”</p>
</li>
<li>
<p><strong>Merchandise and swag</strong>, we all want our rOpenSci hex sticker <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>
</li>
</ul>
<p>One thread in the conversation pointed to a real tension: rOpenSci has spent 15 years building carefully and steadily. GenAI is moving much faster. How do you maintain that care and those foundations when the pace of change accelerates? Even when we didn’t have any easy answers, this question is the right one to be discussing with the community.</p>
<h2>
Closing thoughts
</h2><p>Reading through the notes from both events, a few things are consistent.</p>
<p>People talk about rOpenSci not just as a set of tools or infrastructure, but as a place where they found colleagues, had their work taken seriously, and felt like valued contributors.</p>
<p>The <a href="https://ropensci.org/software-review/" rel="nofollow" target="_blank">software peer-review process</a>, the <a href="https://devguide.ropensci.org/" rel="nofollow" target="_blank">dev guide</a>, the <a href="https://ropensci.org/coworking/" rel="nofollow" target="_blank">co-working sessions</a>, the <a href="https://ropensci.org/champions/" rel="nofollow" target="_blank">Champions Program</a>, these work because the community behind them is invested in supporting them and find them useful.</p>
<p>At 15 years, the ideas that surfaced – local chapters, hackathons, coalitions, better onboarding for newcomers – suggest a community that still wants to try new things.</p>
<blockquote>
<p>It works because everyone is welcome.&#8221; &#8211; several attendees at the events <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f638.png" alt="😸" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
</blockquote>
<p><em>Thank you to everyone who joined and shared their thoughts. <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f499.png" alt="💙" class="wp-smiley" style="height: 1em; max-height: 1em;" /></em></p>
<p><em>These events were part of rOpenSci’s 15th anniversary celebration series. Keep an eye on <a href="https://ropensci.org/events/" rel="nofollow" target="_blank">rOpenSci’s events calendar</a>, our <a href="https://ropensci.org/tags/anniversary/" rel="nofollow" target="_blank">blog</a> and our <a href="https://ropensci.org/news/" rel="nofollow" target="_blank">newsletter</a> for more content around our anniversary.</em></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://ropensci.org/blog/2026/10/20/15yo-events/"> 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/10/celebrating-15-years-of-ropensci-then-now-next/">Celebrating 15 Years of rOpenSci: Then, Now & Next</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404335</post-id>	</item>
		<item>
		<title>From a raw spreadsheet to a published data package in a day</title>
		<link>https://www.r-bloggers.com/2026/10/from-a-raw-spreadsheet-to-a-published-data-package-in-a-day/</link>
		
		<dc:creator><![CDATA[Lars Schöbitz]]></dc:creator>
		<pubDate>Tue, 20 Oct 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://openwashdata.org/pages/blog/posts/2026-10-20-solidwastekampala-story/</guid>

					<description><![CDATA[<p>On 22 July 2026 at 15:05, I made the first commit to a new repository. It held an empty R package and one Excel workbook with field data from a household waste study in Kampala, Uganda. Three minutes later I sent the first prompt to Claude Code:...</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/from-a-raw-spreadsheet-to-a-published-data-package-in-a-day/">From a raw spreadsheet to a published data package in a day</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://openwashdata.org/pages/blog/posts/2026-10-20-solidwastekampala-story/"> openwashdata</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 22 July 2026 at 15:05, I made the first commit to a new repository. It held an empty R package and one Excel workbook with field data from a household waste study in Kampala, Uganda. Three minutes later I sent the first prompt to Claude Code:</p>
<blockquote class="blockquote">
<p>I am building a new data package and received a dataset in <span class="citation" data-cites="data-raw">@data-raw</span>/ to process for clean up. […] strictly start with cleaning the data, writing a dictionary. […] make a plan first.</p>
</blockquote>
<p>On 24 July at 00:18, the repository held version 1.0.0 of <a href="https://github.com/openwashdata/solidwastekampala" rel="nofollow" target="_blank">solidwastekampala</a> with a Zenodo DOI: a tidy and validated dataset of 103 households, a data dictionary for 37 variables, documentation, a website, machine-readable metadata, a citation with the ORCID iDs of the authors, and automated checks on five platforms.</p>
<p>My own time on it was no more than about three hours, spread over four sessions. The repository records every step in its commits, issues and pull requests, and in an archive of every prompt I sent.</p>
<section id="the-data" class="level2">
<h2 class="anchored" data-anchor-id="the-data">The data</h2>
<p>The dataset belongs to the paper <a href="https://doi.org/10.3389/fenvs.2026.1889921" rel="nofollow" target="_blank">Quantity and composition of domestic solid waste in Kampala City as influenced by socioeconomic factors</a> by Katukiza et al. at Makerere University, published in Frontiers in Environmental Science in September 2026. Field teams weighed and sorted the waste of 103 households over seven days in three parishes that stand for three income levels: Bwaise I (low), Bukoto I (middle) and Ggaba (high).</p>
<p>The data arrived the way field data usually arrives. The workbook had sheets with two rows of merged headers, summary rows at the bottom, derived results next to the raw data, and the typos that manual data entry leaves behind.</p>
</section>
<section id="session-1-a-plan-before-any-code" class="level2">
<h2 class="anchored" data-anchor-id="session-1-a-plan-before-any-code">Session 1: a plan before any code</h2>
<p>The first session produced no cleaning code, only a plan based on a close look at every sheet.</p>
<ul>
<li>Only three sheets hold raw data. All other sheets hold derived results.</li>
<li>One number is stored as text. A cell for metals in Ggaba reads <code>2..674</code>, and the SUM formula in Excel skipped it without a warning. The recorded totals stay as they are, because the values per day, per person and the density are calculated from them.</li>
<li>The means per parish reproduce the headline figures of the paper (0.43, 0.53 and 0.98 kg of waste per person per day), which confirmed that these were the right sheets.</li>
</ul>
<p>The plan also set the rule for the rest of the project. We preserve the data as recorded. Seven inconsistencies inside the workbook are documented and tested for, and none was corrected without a record.</p>
</section>
<section id="session-2-from-plan-to-package" class="level2">
<h2 class="anchored" data-anchor-id="session-2-from-plan-to-package">Session 2: from plan to package</h2>
<p>Early on Wednesday the plan became GitHub issues, one per step and each small enough to review on its own. I asked for one issue at a time, with a stop for my review before each commit.</p>
<p>By 06:55 the processing script was in place. It reads all sheets as text so that nothing is lost on import, gives the 37 columns consistent names, fixes the <code>2..674</code> cell and repairs two malformed household IDs. A map per column harmonises the categories, for example <code>IIIiterate</code> to <code>Illiterate</code> and <code>Unpaced</code> to <code>Unpaved</code>. A validation block checks the result, including the inconsistencies we know about: the sums of the waste categories match the recorded totals in 99 of 103 rows, and the script names the four exceptions.</p>
<p>At 07:19 I merged the first pull request with the clean data, CSV and XLSX exports and the dictionary. At 08:03 the second followed with what a stranger needs to use the data: package metadata, documentation, a README that reproduces a figure of the paper, a website, and metadata in schema.org format.</p>
</section>
<section id="session-3-the-review" class="level2">
<h2 class="anchored" data-anchor-id="session-3-the-review">Session 3: the review</h2>
<p>Every openwashdata package goes through a review against a versioned standard, here version 1.0.0. The review consists of four checklist issues, and each one closes with its own pull request.</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Step</th>
<th>Issue</th>
<th>Pull request</th>
<th>Merged</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Metadata and citation</td>
<td>#14</td>
<td>#16</td>
<td>12:22</td>
</tr>
<tr class="even">
<td>Data content and processing</td>
<td>#17</td>
<td>#18</td>
<td>12:58</td>
</tr>
<tr class="odd">
<td>Documentation</td>
<td>#19</td>
<td>#20</td>
<td>13:52</td>
</tr>
<tr class="even">
<td>Tests and automated checks</td>
<td>#21</td>
<td>#22</td>
<td>14:50</td>
</tr>
</tbody>
</table>
<p>The metadata step found five ORCID iDs of the co-authors in the public registry. Two matches were uncertain, so it opened an issue asking the main author to confirm them. The data step reran the processing script and got the committed data back, byte for byte. The last step added checks on five platforms, with 0 errors, 0 warnings and 0 notes.</p>
</section>
<section id="session-4-the-authors-reply" class="level2">
<h2 class="anchored" data-anchor-id="session-4-the-authors-reply">Session 4: the authors reply</h2>
<p>At 23:55 the prompt archive records my message “ORCIDs have arrived”. The main author had confirmed the uncertain matches, sent a missing ORCID iD and settled the spelling of a name. The corrections went into all metadata files. At 00:15 I released version 1.0.0, and at 00:18 Zenodo issued the DOI.</p>
</section>
<section id="how-the-package-meets-the-fair-principles" class="level2">
<h2 class="anchored" data-anchor-id="how-the-package-meets-the-fair-principles">How the package meets the FAIR principles</h2>
<ul>
<li><strong>Findable</strong> through a DOI, <a href="https://doi.org/10.5281/zenodo.21519796" rel="nofollow" target="_blank">10.5281/zenodo.21519796</a>, which always resolves to the latest version, through its own <a href="https://openwashdata.github.io/solidwastekampala/" rel="nofollow" target="_blank">website</a>, and through schema.org metadata.</li>
<li><strong>Accessible</strong> as an R package, as CSV or XLSX files, and as a Zenodo archive that does not depend on GitHub.</li>
<li><strong>Interoperable</strong> because the data are tidy, the names follow one convention, ordered categories are stored as ordered factors, and the files use UTF-8.</li>
<li><strong>Reusable</strong> because the dictionary describes all 37 variables with units, the processing script reproduces the data, the inconsistencies of the source are documented, and the data carry a CC BY 4.0 licence. Version 1.0.1 of 17 September carries the ORCID iDs of all eight authors.</li>
</ul>
<p>There is one more layer that the FAIR principles do not name, the record of how the package was made. The repository archives every prompt in <code>prompts/</code> and every plan in <code>plans/</code>, and commit messages link to the prompts behind them.</p>
</section>
<section id="what-i-take-from-it" class="level2">
<h2 class="anchored" data-anchor-id="what-i-take-from-it">What I take from it</h2>
<p>Cleaning the data was the smallest part. Most of the work went into the plan, the dictionary, the documentation, the metadata, the review, the citation and the exchange with the authors. That work turns a spreadsheet into a publication, and it is the reason why so many datasets never leave the hard drive. With the tools we have built over the years, a versioned review standard and an AI assistant that works through checklists, it now fits into a day.</p>
</section>
<section id="how-i-used-ai" class="level2">
<h2 class="anchored" data-anchor-id="how-i-used-ai">How I used AI</h2>
<p>I used AI for this package and for this post, and the record of that use is public.</p>
<p><strong>The package.</strong> Claude Code (model Claude Fable 5) wrote the processing script, the dictionary, the documentation and the metadata, following my prompts. The repository archives the 11 prompts I sent up to the release, and 14 of the 39 commits made by 24 July carry an <code>Assisted-by</code> line that names the model. I reviewed and merged each of the eight pull requests, handled the exchange with the authors and settled the spelling of their names. The processing script is plain R code, and anyone can rerun it without any AI tool and get the same data.</p>
<p><strong>This post.</strong> Claude (model Claude Fable 5) wrote the first draft on 24 July from the commits, issues, pull requests and prompts. On 7 October and 8 October Claude (model Claude Opus 5.5) revised and shortened it at my request. I chose what the post says, checked every date and number against the repository, and decided to publish it.</p>
<p>The prompts, the commits and this post are public, so anyone can check this statement against the record.</p>
<p><em>The solidwastekampala package was published by <a href="https://openwashdata.org/" rel="nofollow" target="_blank">openwashdata</a> with funding from the Open Research Data Program of the ETH Board. The data come from Katukiza et al. at Makerere University. You can explore them at <a href="https://openwashdata.github.io/solidwastekampala/" rel="nofollow" target="_blank">openwashdata.github.io/solidwastekampala</a>.</em></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://openwashdata.org/pages/blog/posts/2026-10-20-solidwastekampala-story/"> openwashdata</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/from-a-raw-spreadsheet-to-a-published-data-package-in-a-day/">From a raw spreadsheet to a published data package in a day</a>]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">404129</post-id>	</item>
		<item>
		<title>Choosing the size of the calibration set in conformal prediction with ahead::conformalize()</title>
		<link>https://www.r-bloggers.com/2026/10/choosing-the-size-of-the-calibration-set-in-conformal-prediction-with-aheadconformalize/</link>
		
		<dc:creator><![CDATA[T. Moudiki]]></dc:creator>
		<pubDate>Sun, 18 Oct 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://thierrymoudiki.github.io//blog/2026/10/18/r/conformalize-split-prob</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; "> This post explores the impact of the split_prob parameter in the ahead::conformalize() function on the resulting prediction intervals.</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/choosing-the-size-of-the-calibration-set-in-conformal-prediction-with-aheadconformalize/">Choosing the size of the calibration set in conformal prediction with ahead::conformalize()</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/18/r/conformalize-split-prob"> 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>In version 0.39.0 of the <a href="https://github.com/Techtonique/ahead" rel="nofollow" target="_blank"><code>ahead</code></a> package, the <code>conformalize()</code> function has been updated to include a new parameter called <code>split_prob</code>. This parameter allows users to specify the proportion of the data that should be used for calibration when constructing conformal prediction intervals.</p>

<h1 id="0---install-packages">0 &#8211; Install packages</h1>

<pre>install.packages(&quot;pak&quot;)

pak::pak(&quot;Techtonique/ahead&quot;)

library(ahead)
</pre>

<h1 id="1---usaccdeaths">1 &#8211; <code>USAccDeaths</code></h1>

<pre>par(mfrow=c(2, 2))
obj &lt;- ahead::conformalize(FUN = forecast::ets, y = USAccDeaths,
                           h = 20L, split_prob = 0.4)
plot(obj)
obj &lt;- ahead::conformalize(FUN = forecast::ets, y = USAccDeaths,
                           h = 20L, split_prob = 0.3)
plot(obj)
obj &lt;- ahead::conformalize(FUN = forecast::ets, y = USAccDeaths,
                           h = 20L, split_prob = 0.6)
plot(obj)
obj &lt;- ahead::conformalize(FUN = forecast::ets, y = USAccDeaths,
                           h = 20L, split_prob = 0.7)
plot(obj)
</pre>

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

<h1 id="2---airpassengers">2 - <code>AirPassengers</code></h1>

<pre>par(mfrow=c(2, 2))
obj &lt;- ahead::conformalize(FUN = forecast::ets, y = AirPassengers,
                           h = 20L, split_prob = 0.4)
plot(obj)
obj &lt;- ahead::conformalize(FUN = forecast::ets, y = AirPassengers,
                           h = 20L, split_prob = 0.3)
plot(obj)
obj &lt;- ahead::conformalize(FUN = forecast::ets, y = AirPassengers,
                           h = 20L, split_prob = 0.6)
plot(obj)
obj &lt;- ahead::conformalize(FUN = forecast::ets, y = AirPassengers,
                           h = 20L, split_prob = 0.7)
plot(obj)
</pre>

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

<h1 id="3---fdeaths">3 - <code>fdeaths</code></h1>

<pre>par(mfrow=c(2, 2))
obj &lt;- ahead::conformalize(FUN = ahead::dynrmf, y = fdeaths,
                           h = 20L, split_prob = 0.4)
plot(obj)
obj &lt;- ahead::conformalize(FUN = ahead::dynrmf, y = fdeaths,
                           h = 20L, split_prob = 0.3)
plot(obj)
obj &lt;- ahead::conformalize(FUN = ahead::dynrmf, y = fdeaths,
                           h = 20L, split_prob = 0.6)
plot(obj)
obj &lt;- ahead::conformalize(FUN = ahead::dynrmf, y = fdeaths,
                           h = 20L, split_prob = 0.7)
plot(obj)
</pre>

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

<h1 id="4---ukdriverdeaths">4 - <code>UKDriverDeaths</code></h1>

<pre>par(mfrow=c(2, 2))
obj &lt;- ahead::conformalize(FUN = ahead::dynrmf, y = UKDriverDeaths,
                           h = 20L, split_prob = 0.4)
plot(obj)
obj &lt;- ahead::conformalize(FUN = ahead::dynrmf, y = UKDriverDeaths,
                           h = 20L, split_prob = 0.3)
plot(obj)
obj &lt;- ahead::conformalize(FUN = ahead::dynrmf, y = UKDriverDeaths,
                           h = 20L, split_prob = 0.6)
plot(obj)
obj &lt;- ahead::conformalize(FUN = ahead::dynrmf, y = UKDriverDeaths,
                           h = 20L, split_prob = 0.7)
plot(obj)
</pre>

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

<h1 id="5---wwwusage">5 - <code>WWWusage</code></h1>

<pre>par(mfrow=c(2, 2))
obj &lt;- ahead::conformalize(FUN = ahead::dynrmf, y = WWWusage,
                           h = 20L, split_prob = 0.4)
plot(obj)
obj &lt;- ahead::conformalize(FUN = ahead::dynrmf, y = WWWusage,
                           h = 20L, split_prob = 0.3)
plot(obj)
obj &lt;- ahead::conformalize(FUN = ahead::dynrmf, y = WWWusage,
                           h = 20L, split_prob = 0.6)
plot(obj)
obj &lt;- ahead::conformalize(FUN = ahead::dynrmf, y = WWWusage,
                           h = 20L, split_prob = 0.5)
plot(obj)
</pre>

<p><img src="https://i2.wp.com/thierrymoudiki.github.io/images/2026-10-18/2026-10-18-conformalize-split-prob_13_0.png?w=578&#038;ssl=1" alt="image-title-here" class="img-responsive" data-recalc-dims="1" /></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/18/r/conformalize-split-prob"> 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/choosing-the-size-of-the-calibration-set-in-conformal-prediction-with-aheadconformalize/">Choosing the size of the calibration set in conformal prediction with ahead::conformalize()</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404305</post-id>	</item>
		<item>
		<title>Estimate your BAC, now 100% in the browser</title>
		<link>https://www.r-bloggers.com/2026/10/estimate-your-bac-now-100-in-the-browser/</link>
		
		<dc:creator><![CDATA[R on Publishable Stuff]]></dc:creator>
		<pubDate>Sat, 17 Oct 2026 22:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://sumsar.net/blog/drinkr-in-the-browser/</guid>

					<description><![CDATA[<p>Thanks to the awesomeness of<br />
  webR and<br />
  Shinylive,<br />
  the tool I made in back in 2014 to estimate your blood alcohol concentration (BAC) now runs 100% in the browser, no server needed!</p>
<p>  Try the browser version of drinkR here!</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/estimate-your-bac-now-100-in-the-browser/">Estimate your BAC, now 100% in the browser</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://sumsar.net/blog/drinkr-in-the-browser/"> R on Publishable Stuff</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>Thanks to the awesomeness of 
  <a href="https://docs.r-wasm.org/webr/latest/" rel="nofollow" target="_blank">webR</a> and 
  <a href="https://github.com/posit-dev/r-shinylive" rel="nofollow" target="_blank">Shinylive</a>, 
  <a href="https://rasmusab.github.io/drinkr/" rel="nofollow" target="_blank">the tool</a> I made in back in 2014 to estimate your blood alcohol concentration (BAC) now runs 100% in the browser, no server needed!</p>
<p>
  <a href="https://rasmusab.github.io/drinkr/" rel="nofollow" target="_blank">Try the browser version of drinkR here!</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://sumsar.net/blog/drinkr-in-the-browser/"> R on Publishable Stuff</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/estimate-your-bac-now-100-in-the-browser/">Estimate your BAC, now 100% in the browser</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404291</post-id>	</item>
		<item>
		<title>tsmp Is Back: A Faster, Friendlier Matrix Profile Workflow for R</title>
		<link>https://www.r-bloggers.com/2026/10/tsmp-is-back-a-faster-friendlier-matrix-profile-workflow-for-r/</link>
		
		<dc:creator><![CDATA[r-bloggers on Francisco Bischoff]]></dc:creator>
		<pubDate>Fri, 16 Oct 2026 15:15:46 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">/posts/matrixprofiler-and-tsmp-major-updates/</guid>

					<description><![CDATA[<p>A new chapter for the Matrix Profile in R<br />
For a while, tsmp looked quiet. The project was not standing still, though: the Matrix Profile tools for R have been reorganized around a much clearer split between the computational engine and the everyday data-mining interface.<br />
In tsmp 0.5.1, the package that ...</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/tsmp-is-back-a-faster-friendlier-matrix-profile-workflow-for-r/">tsmp Is Back: A Faster, Friendlier Matrix Profile Workflow for R</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://franzbischoff.rbind.io/posts/matrixprofiler-and-tsmp-major-updates/"> r-bloggers on Francisco Bischoff</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>



<style type="text/css">
img {
  display: block; margin-left: auto; margin-right: auto;
}
</style>
<div id="a-new-chapter-for-the-matrix-profile-in-r" class="section level2">
<h2>A new chapter for the Matrix Profile in R</h2>
<p>For a while, <code>tsmp</code> looked quiet. The project was not standing still, though: the Matrix Profile tools for R have been reorganized around a much clearer split between the computational engine and the everyday data-mining interface.</p>
<p>In <code>tsmp</code> 0.5.1, the package that many of us have used for years becomes a pure-R analytical layer. The new <a href="https://cran.r-project.org/package=matrixprofiler" rel="nofollow" target="_blank"><code>matrixprofiler</code></a> package takes over the computationally intensive work in an optimized C++17 engine. The result is not a replacement for <code>tsmp</code>; it is a stronger foundation underneath it.</p>
</div>
<div id="two-packages-two-jobs" class="section level2">
<h2>Two packages, two jobs</h2>
<p>The older <code>tsmp</code> release brought algorithms, analysis helpers, and much of the computation together in one package. That made it a convenient starting point, but as series and workloads grew, computational work implemented around R-level operations could become the bottleneck.</p>
<p>The new architecture separates those responsibilities:</p>
<ul>
<li><strong><code>matrixprofiler</code></strong> provides the low-level Matrix Profile algorithms and optimized numerical primitives, with native multithreading through <code>RcppThread</code>.</li>
<li><strong><code>tsmp</code></strong> keeps the higher-level workflow close to the analyst: motifs, chains, segmentation, snippets, plotting, and the familiar tools for day-to-day time-series mining.</li>
</ul>
<p>The heavy computation now runs in the native engine, while the package you use to explore and interpret results stays approachable. <code>tsmp</code> itself no longer needs compilation when installed from source; it delegates its computational backend to <code>matrixprofiler</code>.</p>
<p>The everyday workflow remains familiar:</p>
<pre>library(tsmp)

profile &lt;- tsmp(series, window_size = 80)
motifs &lt;- find_motif(profile, n_motifs = 3)
plot(motifs)</pre>
<p>The difference is what happens under the hood: computationally intensive steps can use the native backend and its <code>n_workers</code> controls, without asking every analyst to work directly with low-level routines.</p>
</div>
<div id="from-a-contrast-score-to-a-useful-pattern" class="section level2">
<h2>From a contrast score to a useful pattern</h2>
<p>The new engine also adds a native implementation of the <strong>Contrast Profile</strong>. It is designed for a practical question: <em>which subsequence is repeated in the examples I care about, but is absent from the examples I do not care about?</em></p>
<p>Given positive and negative time series, the Contrast Profile compares each positive subsequence’s nearest match in the negative data with its nearest non-trivial match in the positive data. In simplified notation,</p>
<p><span class="math display">\[
C_m = \frac{MP_m^{(+,-)} - MP_m^{(+,+)}}{\sqrt{2m}},
\]</span></p>
<p>where <span class="math inline">\(m\)</span> is the window size. A high score highlights a pattern that is well conserved within the positive class and unlike the negative class. The original paper calls the highest-scoring subsequence the <strong>Plato</strong>: a candidate signature for the behavior or class of interest.</p>
<p>In R, <code>matrixprofiler::contrast()</code> computes this profile directly:</p>
<pre>cp &lt;- matrixprofiler::contrast(negative_data = negative_series, positive_data = positive_series,
    window_size = 80L, distance = &quot;pearson&quot;, n_workers = 4L)

cp$plato
cp$plato_idx</pre>
<p>The original Contrast Profile paper also discusses <strong>Top-K Platos</strong> for cases where the target behavior has more than one shape. Its key idea is important: the next result should be discovered after accounting for the previous pattern, rather than simply taking the next-highest, potentially redundant peak.</p>
<p>The current package does not provide a <code>contrast_top_k()</code> function. For now, users who need several Contrast Profile Platos must implement the iterative orchestration themselves: discover a Plato with <code>contrast()</code>, add its pattern to the negative examples, and recompute before looking for the next one. The expensive Contrast Profile calculation still runs in the native <code>matrixprofiler</code> engine; the Top-K loop is left to the user.</p>
</div>
<div id="rfcp-when-frequency-matters" class="section level2">
<h2>RFCP: when frequency matters</h2>
<p>The new <strong>Relative Frequency Contrast Profile (RFCP)</strong> takes this contrastive idea further. Instead of comparing only the nearest positive and negative matches, RFCP considers ordered ranks of non-overlapping neighbors. This lets it ask not just whether a pattern occurs, but how its recurrence differs between positive and negative data.</p>
<p><code>matrixprofiler::rfcp()</code> returns the contrast profile and the highest-scoring Plato. For a ranked, diverse set of discoveries, <code>matrixprofiler::rfcp_top_k()</code> iteratively finds a pattern, suppresses its occurrences and trivial neighbors, and then searches for the next one. You can request a different frequency rank at each iteration, enable adaptive frequency selection, and use callbacks to checkpoint longer runs.</p>
<pre>top_patterns &lt;- matrixprofiler::rfcp_top_k(positive_data = positive_series, negative_data = negative_series,
    window_size = 80L, k_platos = 3L, max_freq = c(4L, 3L, 2L), adaptive_max_freq = TRUE, n_workers = 4L)

top_patterns$platos</pre>
<p>Here, Top-K means a sequence of distinct, contrastive morphologies, not simply the K tallest overlapping peaks. That makes the result more useful when a class can be expressed in several ways, or when the first discovery would otherwise dominate the rest of the search.</p>
</div>
<div id="optional-cuda-acceleration" class="section level2">
<h2>Optional CUDA acceleration</h2>
<p>The standard <code>matrixprofiler</code> installation is a portable, multithreaded CPU engine. For supported algorithms, an NVIDIA GPU is also an option: the project documents a manual CUDA-enabled build, including support for algorithms such as MPX and RFCP.</p>
<p>With the CUDA Toolkit and a compatible NVIDIA setup, clone the repository and install through its build script:</p>
<pre>git clone https://github.com/matrix-profile-foundation/matrixprofiler.git
cd matrixprofiler
./inst/cuda/build_cuda_rcpp.sh R CMD INSTALL .</pre>
<p>After installation, check whether the package was compiled with CUDA support:</p>
<pre>matrixprofiler::has_cuda()</pre>
<p>CUDA is optional and must be enabled at build time; installing the regular CRAN package does not automatically turn it on.</p>
</div>
<div id="a-practical-way-to-get-started" class="section level2">
<h2>A practical way to get started</h2>
<p>If you already use <code>tsmp</code>, start with the new <a href="https://github.com/matrix-profile-foundation/tsmp/blob/master/MIGRATION.md" rel="nofollow" target="_blank">migration guide</a>, especially if your scripts create <code>doSNOW</code> clusters or assume invalid profile indices are <code>-1</code>. The updated parallel interface uses <code>n_workers</code>, and missing indices now use R’s <code>NA_integer_</code> convention.</p>
<p>For new analyses, install the familiar high-level tools and their computational backend:</p>
<pre>install.packages(c(&quot;tsmp&quot;, &quot;matrixprofiler&quot;))</pre>
<p><code>tsmp</code> remains the friendly place to do everyday Matrix Profile data mining. <code>matrixprofiler</code> makes that experience faster and provides a home for optimized algorithms that need more direct control. Together, they bring the R ecosystem back into active development and make room for more methods without making the daily workflow harder to use.</p>
<p>Explore <a href="https://cran.r-project.org/package=tsmp" rel="nofollow" target="_blank"><code>tsmp</code> on CRAN</a>, <a href="https://cran.r-project.org/package=matrixprofiler" rel="nofollow" target="_blank"><code>matrixprofiler</code> on CRAN</a>, and the <a href="https://matrix-profile-foundation.github.io/matrixprofiler/" rel="nofollow" target="_blank">matrixprofiler documentation</a>. For the research behind the Contrast Profile, see Mercer et al., <a href="https://doi.org/10.1109/ICDM51629.2021.00151" rel="nofollow" target="_blank">“Matrix Profile XXIII: Contrast Profile: A Novel Time Series Primitive that Allows Real World Classification”</a>.</p>
</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://franzbischoff.rbind.io/posts/matrixprofiler-and-tsmp-major-updates/"> r-bloggers on Francisco Bischoff</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/tsmp-is-back-a-faster-friendlier-matrix-profile-workflow-for-r/">tsmp Is Back: A Faster, Friendlier Matrix Profile Workflow for R</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404263</post-id>	</item>
		<item>
		<title>[Statistics] Collinearity in regression</title>
		<link>https://www.r-bloggers.com/2026/10/statistics-collinearity-in-regression/</link>
		
		<dc:creator><![CDATA[R on Zhenguo Zhang&#039;s Blog]]></dc:creator>
		<pubDate>Fri, 16 Oct 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://fortune9.netlify.app/2026/10/16/statistics-collinearity-in-regression/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; "> Zhenguo Zhang's Blog https://fortune9.netlify.app/2026/10/16/statistics-collinearity-in-regression/ -</p>
<p>library(knitr)<br />
library(ggplot2)<br />
library(dplyr)<br />
library(tidyr)<br />
library(broom)</p>
<p>Problem<br />
When performing regression analysis, you may encounter two or ...</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/statistics-collinearity-in-regression/">[Statistics] Collinearity in regression</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://fortune9.netlify.app/2026/10/16/statistics-collinearity-in-regression/"> R on Zhenguo Zhang&#039;s 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>
Zhenguo Zhang&#8217;s Blog https://fortune9.netlify.app/2026/10/16/statistics-collinearity-in-regression/ &#8211;


<pre>library(knitr)
library(ggplot2)
library(dplyr)
library(tidyr)
library(broom)</pre>
<div id="problem" class="section level2">
<h2>Problem</h2>
<p>When performing regression analysis, you may encounter two or more predictors that are highly correlated. This is called collinearity or multicollinearity. It can cause problems in estimating the regression coefficients and interpreting the results.</p>
<p>In this post, I will show how collinearity can affect regression analysis, how to detect
collinearity, and how to deal with it.</p>
</div>
<div id="example-one-predictor-is-a-linear-function-of-another-predictor" class="section level2">
<h2>Example: one predictor is a linear function of another predictor</h2>
<p>Let’s simulate data with the following variables:</p>
<ul>
<li><code>X1</code>: a random variable drawn from a normal distribution with mean 0 and standard deviation 1.</li>
<li><code>X2</code>: another random variable drawn from a normal distribution with mean 0 and standard deviation 1, independent of <code>X1</code>.</li>
<li><code>Z</code>: a variable linearly related to <code>X2</code>, defined as <code>Z = 5*X2 + noise</code>, where the noise is drawn from a normal distribution with mean 0 and standard deviation 1.</li>
<li><code>Y</code>: a linear combination of <code>X1</code> and <code>X2</code> plus noise, defined as <code>Y = 3*X1 + 2*X2 + noise</code>, where the noise is drawn from a normal distribution with mean 0 and standard deviation 1.</li>
</ul>
<pre>set.seed(123)
n &lt;- 100
X1 &lt;- rnorm(n, mean = 0, sd = 1)
X2 &lt;- rnorm(n, mean = 0, sd = 1)
Z &lt;- 5 * X2 + rnorm(n, mean = 0, sd = 1)
Y &lt;- 3 * X1 + 2 * X2 + rnorm(n, mean = 0, sd = 1)</pre>
<p>The correlation coefficients among the three predictors are:</p>
<pre>kable(cor(cbind(X1, X2, Z)), caption=&quot;Correlation of the predictors&quot;)</pre>
<table>
<caption><span id="tab:unnamed-chunk-3">Table 1: </span>Correlation of the predictors</caption>
<thead>
<tr class="header">
<th align="left"></th>
<th align="right">X1</th>
<th align="right">X2</th>
<th align="right">Z</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td align="left">X1</td>
<td align="right">1.0000000</td>
<td align="right">-0.0495321</td>
<td align="right">-0.0730836</td>
</tr>
<tr class="even">
<td align="left">X2</td>
<td align="right">-0.0495321</td>
<td align="right">1.0000000</td>
<td align="right">0.9814767</td>
</tr>
<tr class="odd">
<td align="left">Z</td>
<td align="right">-0.0730836</td>
<td align="right">0.9814767</td>
<td align="right">1.0000000</td>
</tr>
</tbody>
</table>
<p>Now let’s fit two linear regression models: one with <code>X1</code> and <code>X2</code> as predictors, and another with <code>X1</code>, <code>X2</code>, and <code>Z</code> as predictors.
We will then examine how the following metrics change: coefficients, standard errors, t-values, p-values, R-squared, adjusted R-squared.</p>
<pre>fit1 &lt;- lm(Y ~ X1 + X2)
fit2 &lt;- lm(Y ~ X1 + X2 + Z)
coef_results &lt;- rbind(
  tidy(fit1) %&gt;% mutate(model = &quot;fit1&quot;),
  tidy(fit2) %&gt;% mutate(model = &quot;fit2&quot;)
)
# move model to the first column
coef_results &lt;- coef_results %&gt;% select(model, everything())
# now R-squared and adjusted R-squared
rsq_results &lt;- rbind(
  glance(fit1) %&gt;% mutate(model = &quot;fit1&quot;),
  glance(fit2) %&gt;% mutate(model = &quot;fit2&quot;)
) %&gt;%
  select(model, everything())
kable(coef_results, caption = &quot;The coefficients and statistics&quot;)</pre>
<table>
<caption><span id="tab:unnamed-chunk-5">Table 2: </span>The coefficients and statistics</caption>
<thead>
<tr class="header">
<th align="left">model</th>
<th align="left">term</th>
<th align="right">estimate</th>
<th align="right">std.error</th>
<th align="right">statistic</th>
<th align="right">p.value</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td align="left">fit1</td>
<td align="left">(Intercept)</td>
<td align="right">-0.0270771</td>
<td align="right">0.1058626</td>
<td align="right">-0.2557758</td>
<td align="right">0.7986656</td>
</tr>
<tr class="even">
<td align="left">fit1</td>
<td align="left">X1</td>
<td align="right">2.9521917</td>
<td align="right">0.1154749</td>
<td align="right">25.5656651</td>
<td align="right">0.0000000</td>
</tr>
<tr class="odd">
<td align="left">fit1</td>
<td align="left">X2</td>
<td align="right">2.0448517</td>
<td align="right">0.1090059</td>
<td align="right">18.7590842</td>
<td align="right">0.0000000</td>
</tr>
<tr class="even">
<td align="left">fit2</td>
<td align="left">(Intercept)</td>
<td align="right">-0.0193261</td>
<td align="right">0.1073437</td>
<td align="right">-0.1800395</td>
<td align="right">0.8575011</td>
</tr>
<tr class="odd">
<td align="left">fit2</td>
<td align="left">X1</td>
<td align="right">2.9445494</td>
<td align="right">0.1168766</td>
<td align="right">25.1936688</td>
<td align="right">0.0000000</td>
</tr>
<tr class="even">
<td align="left">fit2</td>
<td align="left">X2</td>
<td align="right">2.3331532</td>
<td align="right">0.5743463</td>
<td align="right">4.0622761</td>
<td align="right">0.0000993</td>
</tr>
<tr class="odd">
<td align="left">fit2</td>
<td align="left">Z</td>
<td align="right">-0.0573870</td>
<td align="right">0.1122308</td>
<td align="right">-0.5113302</td>
<td align="right">0.6102939</td>
</tr>
</tbody>
</table>
<pre>kable(rsq_results, caption = &quot;The goodness of fit&quot;)</pre>
<table>
<caption><span id="tab:unnamed-chunk-5">Table 2: </span>The goodness of fit</caption>
<colgroup>
<col width="5%" />
<col width="8%" />
<col width="12%" />
<col width="7%" />
<col width="8%" />
<col width="7%" />
<col width="2%" />
<col width="8%" />
<col width="7%" />
<col width="7%" />
<col width="7%" />
<col width="10%" />
<col width="4%" />
</colgroup>
<thead>
<tr class="header">
<th align="left">model</th>
<th align="right">r.squared</th>
<th align="right">adj.r.squared</th>
<th align="right">sigma</th>
<th align="right">statistic</th>
<th align="right">p.value</th>
<th align="right">df</th>
<th align="right">logLik</th>
<th align="right">AIC</th>
<th align="right">BIC</th>
<th align="right">deviance</th>
<th align="right">df.residual</th>
<th align="right">nobs</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td align="left">fit1</td>
<td align="right">0.9082614</td>
<td align="right">0.9063699</td>
<td align="right">1.047502</td>
<td align="right">480.1763</td>
<td align="right">0</td>
<td align="right">2</td>
<td align="right">-145.0117</td>
<td align="right">298.0234</td>
<td align="right">308.4441</td>
<td align="right">106.4342</td>
<td align="right">97</td>
<td align="right">100</td>
</tr>
<tr class="even">
<td align="left">fit2</td>
<td align="right">0.9085106</td>
<td align="right">0.9056516</td>
<td align="right">1.051513</td>
<td align="right">317.7673</td>
<td align="right">0</td>
<td align="right">3</td>
<td align="right">-144.8757</td>
<td align="right">299.7514</td>
<td align="right">312.7773</td>
<td align="right">106.1452</td>
<td align="right">96</td>
<td align="right">100</td>
</tr>
</tbody>
</table>
<p>As the results show, the first model, <code>fit1</code>, roughly captures the true relationship in the simulated data,
and both coefficients are highly statistically significant.</p>
<p>Adding <code>Z</code> to the regression model produces several changes: (1) the coefficient for <code>X2</code>
becomes smaller, (2) its standard error increases more than fivefold, and (3)
the p-value for the coefficient test consequently increases substantially. Overall,
the model’s R-squared changes little, but its AIC and BIC values increase,
suggesting that the model with three predictors is less favorable than the model with two predictors.</p>
<p>In summary, including a predictor that is highly correlated with an existing
predictor reduces the statistical significance of the existing predictor.</p>
</div>
<div id="example-two-predictors-with-different-levels-of-correlation" class="section level2">
<h2>Example: two predictors with different levels of correlation</h2>
<p>Now let’s simulate datasets containing two variables with different levels of
correlation. We will use <a href="https://en.wikipedia.org/wiki/Cholesky_decomposition" rel="nofollow" target="_blank">Cholesky decomposition</a>
to simulate the data.</p>
<p>First, let’s write a function that simulates data for a specified
correlation coefficient:</p>
<pre>simulate_correlated_data&lt;-function(rho, n=100, seed=NULL) {
  if(is.null(seed)) {
    seed &lt; Sys.time()
  }
  set.seed(seed)
  x &lt;- rnorm(n)
  z &lt;- rnorm(n)
  y &lt;- rho*x + sqrt(1-rho^2)*z
  return(cbind(x,y))
}</pre>
<p>Now let’s simulate five datasets with target correlations of
0.1, 0.3, 0.5, 0.8, and 0.95 between the two predictors. We will also make both predictors
contribute to the response variable so that we can examine how different levels of correlation
affect the regression fit:</p>
<pre>n&lt;-100
rhos&lt;-c(0.1, 0.3, 0.5, 0.8, 0.95)
simulated_data &lt;- lapply(rhos, simulate_correlated_data, n=n)
# let&#39;s calculate the correlation coefficients for the simulated data
simulated_rhos&lt;-sapply(simulated_data, function(d) cor(d[,&quot;x&quot;],d[, &quot;y&quot;]))
kable(cbind(expected_rho=rhos, actual_rho=simulated_rhos))</pre>
<table>
<thead>
<tr class="header">
<th align="right">expected_rho</th>
<th align="right">actual_rho</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td align="right">0.10</td>
<td align="right">-0.0271945</td>
</tr>
<tr class="even">
<td align="right">0.30</td>
<td align="right">0.3337500</td>
</tr>
<tr class="odd">
<td align="right">0.50</td>
<td align="right">0.5365344</td>
</tr>
<tr class="even">
<td align="right">0.80</td>
<td align="right">0.8167663</td>
</tr>
<tr class="odd">
<td align="right">0.95</td>
<td align="right">0.9624687</td>
</tr>
</tbody>
</table>
<p>The correlation coefficient, rho, in each simulated dataset may not exactly
match the specified value because of random sampling, but we can still examine how the level of correlation affects
the regression.</p>
<pre> fits &lt;- lapply(simulated_data, function(d) {
          x1&lt;-d[,&quot;x&quot;]
          x2&lt;-d[, &quot;y&quot;]
          y &lt;- 10*x1 + 3*x2 + rnorm(n)
          fit&lt;-lm(y~x1 + x2)
          return(fit)
          } 
          )
coef_results&lt;-lapply(seq_along(rhos),
                     function(i) {
                       tidy(fits[[i]]) %&gt;%
                         mutate(rho = rhos[i]) %&gt;%
                         select(rho, everything())
                     }
                     )
coef_results &lt;- do.call(rbind, coef_results)
kable(coef_results, caption = &quot;Regression statistics with different correlation strengths among predictors&quot;)</pre>
<table>
<caption><span id="tab:unnamed-chunk-8">Table 3: </span>Regression statistics with different correlation strengths among predictors</caption>
<thead>
<tr class="header">
<th align="right">rho</th>
<th align="left">term</th>
<th align="right">estimate</th>
<th align="right">std.error</th>
<th align="right">statistic</th>
<th align="right">p.value</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td align="right">0.10</td>
<td align="left">(Intercept)</td>
<td align="right">0.1545731</td>
<td align="right">0.1093261</td>
<td align="right">1.4138713</td>
<td align="right">0.1606012</td>
</tr>
<tr class="even">
<td align="right">0.10</td>
<td align="left">x1</td>
<td align="right">10.0038407</td>
<td align="right">0.1047198</td>
<td align="right">95.5296414</td>
<td align="right">0.0000000</td>
</tr>
<tr class="odd">
<td align="right">0.10</td>
<td align="left">x2</td>
<td align="right">3.0724520</td>
<td align="right">0.1118900</td>
<td align="right">27.4595781</td>
<td align="right">0.0000000</td>
</tr>
<tr class="even">
<td align="right">0.30</td>
<td align="left">(Intercept)</td>
<td align="right">0.0349345</td>
<td align="right">0.1183168</td>
<td align="right">0.2952627</td>
<td align="right">0.7684243</td>
</tr>
<tr class="odd">
<td align="right">0.30</td>
<td align="left">x1</td>
<td align="right">9.8936718</td>
<td align="right">0.1339565</td>
<td align="right">73.8573235</td>
<td align="right">0.0000000</td>
</tr>
<tr class="even">
<td align="right">0.30</td>
<td align="left">x2</td>
<td align="right">3.0068927</td>
<td align="right">0.1113956</td>
<td align="right">26.9929282</td>
<td align="right">0.0000000</td>
</tr>
<tr class="odd">
<td align="right">0.50</td>
<td align="left">(Intercept)</td>
<td align="right">0.1417142</td>
<td align="right">0.0972655</td>
<td align="right">1.4569831</td>
<td align="right">0.1483507</td>
</tr>
<tr class="even">
<td align="right">0.50</td>
<td align="left">x1</td>
<td align="right">10.0727790</td>
<td align="right">0.1156101</td>
<td align="right">87.1271918</td>
<td align="right">0.0000000</td>
</tr>
<tr class="odd">
<td align="right">0.50</td>
<td align="left">x2</td>
<td align="right">2.9767374</td>
<td align="right">0.1173528</td>
<td align="right">25.3657122</td>
<td align="right">0.0000000</td>
</tr>
<tr class="even">
<td align="right">0.80</td>
<td align="left">(Intercept)</td>
<td align="right">-0.0609381</td>
<td align="right">0.0979742</td>
<td align="right">-0.6219812</td>
<td align="right">0.5354135</td>
</tr>
<tr class="odd">
<td align="right">0.80</td>
<td align="left">x1</td>
<td align="right">10.3154854</td>
<td align="right">0.1709446</td>
<td align="right">60.3440164</td>
<td align="right">0.0000000</td>
</tr>
<tr class="even">
<td align="right">0.80</td>
<td align="left">x2</td>
<td align="right">2.7154226</td>
<td align="right">0.1638409</td>
<td align="right">16.5735364</td>
<td align="right">0.0000000</td>
</tr>
<tr class="odd">
<td align="right">0.95</td>
<td align="left">(Intercept)</td>
<td align="right">-0.1158642</td>
<td align="right">0.0971670</td>
<td align="right">-1.1924234</td>
<td align="right">0.2360041</td>
</tr>
<tr class="even">
<td align="right">0.95</td>
<td align="left">x1</td>
<td align="right">10.0497393</td>
<td align="right">0.3537788</td>
<td align="right">28.4068467</td>
<td align="right">0.0000000</td>
</tr>
<tr class="odd">
<td align="right">0.95</td>
<td align="left">x2</td>
<td align="right">2.9023068</td>
<td align="right">0.3443712</td>
<td align="right">8.4278435</td>
<td align="right">0.0000000</td>
</tr>
</tbody>
</table>
<p>Let’s plot all the metrics:</p>
<pre>coef_results &lt;- coef_results %&gt;% 
    mutate(rho = factor(rho)) %&gt;%
    pivot_longer(
      cols = estimate:p.value,
      names_to = &quot;metric&quot;,
      values_to = &quot;value&quot;
    )
plt &lt;- ggplot(coef_results, aes(x=rho, y=value)) +
      geom_point(aes(color=term)) +
      facet_wrap(~ metric, ncol=2, scales=&quot;free_y&quot;) +
      theme(legend.position = &quot;top&quot;)
plt</pre>
<p><img src="https://i0.wp.com/fortune9.netlify.app/2026/10/16/statistics-collinearity-in-regression/index_files/figure-html/unnamed-chunk-10-1.png?w=450&#038;ssl=1" alt="" data-recalc-dims="1" /></p>
<p>The plots show the following patterns as the correlation between the predictors increases:</p>
<ul>
<li>The estimated coefficients change little, although some variation remains.</li>
<li>The p-values for both predictors remain very small.</li>
<li>The test statistics become smaller as rho increases.</li>
<li>The standard errors increase as rho increases.</li>
<li>Based on the last two observations, the p-values for the predictors should become larger, i.e., less significant.</li>
</ul>
<p>In summary, when the predictors in a regression are correlated, the test
for each coefficient becomes less statistically significant, even though the magnitudes
of the coefficients remain close to the values used to simulate the data.</p>
</div>
<div id="takeaways" class="section level2">
<h2>Takeaways</h2>
<ul>
<li>When one predictor, <code>Z</code>, is a linear function of another predictor, <code>X</code>, and has no direct
relationship with the response variable, including <code>Z</code> makes the test for the
true predictor <code>X</code> less statistically significant. The coefficient for <code>Z</code>
may not be significant because, after accounting for <code>X</code>, <code>Z</code> has no effect on <code>Y</code>.</li>
<li>When two predictors both affect <code>Y</code>, the statistical significance of their
coefficients decreases as the correlation between the predictors increases, even though the
magnitudes of the coefficients may remain close to the true values.</li>
</ul>
<p>Taken together, these examples suggest that you may want to exclude certain variables from your regression unless you
know they affect the response or want to examine the effects of other predictors
after controlling for those variables.</p>
</div>
- https://fortune9.netlify.app/2026/10/16/statistics-collinearity-in-regression/ - 
<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://fortune9.netlify.app/2026/10/16/statistics-collinearity-in-regression/"> R on Zhenguo Zhang&#039;s 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/statistics-collinearity-in-regression/">[Statistics] Collinearity in regression</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404274</post-id>	</item>
		<item>
		<title>Spring 2027 data science training courses: January to June</title>
		<link>https://www.r-bloggers.com/2026/10/spring-2027-data-science-training-courses-january-to-june/</link>
		
		<dc:creator><![CDATA[The Jumping Rivers Blog]]></dc:creator>
		<pubDate>Thu, 15 Oct 2026 23:59:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://www.jumpingrivers.com/blog/spring-2027-training-courses/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>If you plan to learn R or Python in the new year, book before the end of November. Our spring 2027 data science training courses run from January to June, and early bird pricing on the first one ends on 29 November.<br />
There are 22 courses on the sc...</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/spring-2027-data-science-training-courses-january-to-june/">Spring 2027 data science training courses: January to June</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.jumpingrivers.com/blog/spring-2027-training-courses/"> The Jumping Rivers 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://www.jumpingrivers.com/blog/spring-2027-training-courses/">
<img src="https://i0.wp.com/www.jumpingrivers.com/blog/spring-2027-training-courses/featured.png?w=400&#038;ssl=1" style="width:400px" class="image-center" style="display: block; margin: auto;" data-recalc-dims="1" />
</a>
</p>
<p>If you plan to learn R or Python in the new year, book before the end of November. Our spring 2027 data science training courses run from January to June, and early bird pricing on the first one ends on <strong>29 November</strong>.</p>
<p>There are 22 courses on the schedule. All of them run live online, and the trainers are data scientists and engineers who spend the rest of their week on client projects. Each course runs over two afternoons, 13:30 to 17:00 UK time. The Bayesian inference course is the exception: it runs over four.</p>
<h2 id="three-routes-through-our-spring-2027-training-courses">Three routes through our spring 2027 training courses</h2>
<p>The courses are scheduled so you can take them in order. There are three obvious routes, each ending with a set of skills you can use at work the following week.</p>
<h3 id="learning-r-from-scratch">Learning R from scratch</h3>
<p>Start with <a href="https://www.jumpingrivers.com/training/course/r-introduction-tidyverse-readr-ggplot2-dplyr/#event-2027-01-11-13-30-online" rel="nofollow" target="_blank">Introduction to R</a> on 11–12 January. <a href="https://www.jumpingrivers.com/training/course/data-tidyverse-dplyr-tidyr-lubridate-forcats/#event-2027-01-25-13-30-online" rel="nofollow" target="_blank">Data Wrangling in the Tidyverse</a> follows on 25–26 January, then <a href="https://www.jumpingrivers.com/training/course/r-advanced-graphics-ggplot2-plotly-themes-scaling-faceting/#event-2027-02-01-13-30-online" rel="nofollow" target="_blank">Data Visualisation with ggplot2</a> on 1–2 February and <a href="https://www.jumpingrivers.com/training/course/r-programming-functions-looping-conditionals/#event-2027-02-08-13-30-online" rel="nofollow" target="_blank">Programming with R</a> on 8–9 February. That gets you from your first script to writing your own functions in a month.</p>
<p><strong>Why take this route:</strong> Clean, chart and automate your data in code. Monthly reporting becomes a script you rerun in minutes, with fewer manual errors.</p>
<p>January too soon? The same four courses run again between 12 April and 11 May. If you are still deciding whether R is worth the time, we wrote about <a href="https://www.jumpingrivers.com/blog/why-learning-r-is-a-good-career-move-in-2026/" rel="nofollow" target="_blank">why learning R is a good career move</a>.</p>
<h3 id="learning-python">Learning Python</h3>
<p><a href="https://www.jumpingrivers.com/training/course/python-introduction-visualisation-manipulation/#event-2027-02-15-13-30-online" rel="nofollow" target="_blank">Introduction to Python</a> runs on 15–16 February. <a href="https://www.jumpingrivers.com/training/course/python-programming-control-flow-functions/#event-2027-02-22-13-30-online" rel="nofollow" target="_blank">Programming with Python</a> follows on 22–23 February, and <a href="https://www.jumpingrivers.com/training/course/python-matplotlib-seaborn-visualisation/#event-2027-03-01-13-30-online" rel="nofollow" target="_blank">Data Visualisation with Python</a> on 1–2 March. The same three run again in May and June.</p>
<p><strong>Why take this route:</strong> Analyse data in pandas, automate repeat tasks and produce report-ready charts, in the same language as your engineering team. It is also the groundwork for machine learning in Python.</p>
<h3 id="statistics-and-machine-learning-in-r">Statistics and machine learning in R</h3>
<p>This route is for people who already use R. <a href="https://www.jumpingrivers.com/training/course/r-statistics-modelling-linear-regression-clustering/#event-2027-03-08-13-30-online" rel="nofollow" target="_blank">Statistical Modelling with R</a> on 8–9 March covers hypothesis testing, regression, clustering and principal components analysis. <a href="https://www.jumpingrivers.com/training/course/r-prediction-inference-analytics-machine-learning-tidymodels/#event-2027-03-15-13-30-online" rel="nofollow" target="_blank">Machine Learning with Tidymodels</a> on 15–16 March and <a href="https://www.jumpingrivers.com/training/course/r-prediction-inference-tidymodels-lda-pre-processing-tree-based-models/#event-2027-03-22-13-30-online" rel="nofollow" target="_blank">Advanced Machine Learning with Tidymodels</a> on 22–23 March take you through pre-processing, model fitting and tuning.</p>
<p><strong>Why take this route:</strong> Build, validate and tune predictive models in tidymodels, and explain which variables drive them. Your team moves from reporting what happened to forecasting what happens next, with evidence for how far to trust it.</p>
<h2 id="two-courses-that-run-once-this-spring">Two courses that run once this spring</h2>
<p><a href="https://www.jumpingrivers.com/training/course/introduction-bayesian-inference-rstan-monte-carlo/#event-2027-01-18-13-30-online" rel="nofollow" target="_blank">Introduction to Bayesian Inference using RStan</a> runs over four afternoons, 18–21 January. You write Stan programs for a range of statistical models and learn to read the MCMC diagnostics in R. It is the only date for it in the first half of 2027.</p>
<p><a href="https://www.jumpingrivers.com/training/course/reporting-with-quarto/#event-2027-06-21-13-30-online" rel="nofollow" target="_blank">Reporting with Quarto</a> runs on 21–22 June. You build reports, slides and documents straight from R or Python code, so the numbers in the report change when the data does. It pairs well with the ideas in our post on <a href="https://www.jumpingrivers.com/blog/reproducible-analytical-pipelines/" rel="nofollow" target="_blank">reproducible analytical pipelines</a>.</p>
<aside class="advert">
<p>
Whether you want to start from scratch, or improve your skills, <a href="https://www.jumpingrivers.com/training/?utm_source=blog&#038;utm_medium=banner&#038;utm_campaign=2026-spring-2027-training-courses" rel="nofollow" target="_blank">Jumping Rivers has a training course for you</a>.
</p>
</aside>
<h2 id="early-bird-deadlines">Early bird deadlines</h2>
<p>Early bird pricing ends about six weeks before each course. Booking closes a week before the start date. The first three deadlines are:</p>
<ul>
<li>29 November for Introduction to R</li>
<li>6 December for Bayesian inference</li>
<li>13 December for Data Wrangling in the Tidyverse</li>
</ul>
<p>Dates for the second half of 2027 will follow.</p>
<h2 id="using-this-years-training-budget">Using this year’s training budget</h2>
<p>If your team has training budget left this December, booking a January course now spends it before the year ends. It also gets you the early bird price, because every January deadline falls before Christmas.</p>
<h2 id="what-you-get-on-every-course">What you get on every course</h2>
<ul>
<li>PDF notes and scripts to keep</li>
<li>Live demonstrations and hands-on exercises</li>
<li>Time with the trainer to ask questions about your own work</li>
</ul>
<p>We have run more than 1,000 courses, for organisations including NHS Scotland, Shell, Wessex Water and the Royal Statistical Society.</p>
<h2 id="training-a-whole-team">Training a whole team</h2>
<p>Every course in our <a href="https://www.jumpingrivers.com/training/all-courses/" rel="nofollow" target="_blank">full catalogue</a> can also run privately for one team, using your data and your tools. Group bookings and returning clients get a discount. Email <a href="mailto:training@jumpingrivers.com" rel="nofollow" target="_blank">training@jumpingrivers.com</a> with the size of the team and what they need to learn.</p>
<p><a href="https://www.jumpingrivers.com/training/public/" rel="nofollow" target="_blank">See the full public schedule and book a place</a>.</p>
<p>
For updates and revisions to this article, see the <a href = "https://www.jumpingrivers.com/blog/spring-2027-training-courses/">original post</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.jumpingrivers.com/blog/spring-2027-training-courses/"> The Jumping Rivers 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/spring-2027-data-science-training-courses-january-to-june/">Spring 2027 data science training courses: January to June</a>]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">404237</post-id>	</item>
		<item>
		<title>What is an MCP server?</title>
		<link>https://www.r-bloggers.com/2026/10/what-is-an-mcp-server/</link>
		
		<dc:creator><![CDATA[The Jumping Rivers Blog]]></dc:creator>
		<pubDate>Thu, 15 Oct 2026 23:59:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://www.jumpingrivers.com/blog/what-is-an-mcp-server/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>The Model Context Protocol (MCP) is quietly<br />
becoming one of the most useful ideas in AI tooling.<br />
Here, we describe what MCP is, why it matters, and how to build an MCP server yourself.<br />
MCP is a standard way to link a Large Language Model (LLM) to the outside world.<br />
...</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/what-is-an-mcp-server/">What is an MCP server?</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.jumpingrivers.com/blog/what-is-an-mcp-server/"> The Jumping Rivers 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://www.jumpingrivers.com/blog/what-is-an-mcp-server/">
<img src="https://www.jumpingrivers.com/blog/what-is-an-mcp-server/" width="400" style="width:400px" class="image-center" style="display: block; margin: auto;" />
</a>
</p>
<style>
.blog-post :is(ul, ol) li {
margin-block: 0.2rem;
}
</style>
<p><a href="https://en.wikipedia.org/wiki/Model_Context_Protocol" rel="nofollow" target="_blank">The Model Context Protocol (MCP)</a> is quietly
becoming one of the most useful ideas in AI tooling.
Here, we describe what MCP is, why it matters, and how to build an MCP server yourself.</p>
<p>MCP is a standard way to link a Large Language Model (LLM) to the outside world.
An MCP server is a small piece of software that exposes some data or capability to an AI model.
Everyday examples include connecting your AI agent to your calendar or your file system, or allowing
it to search the web. These are things an LLM cannot do (or would struggle to do) by itself.
On its own, a model only knows what it learned during training, plus whatever you provide it, and it
has no way to check, fetch or act on anything beyond that.
Even then, it can only give out text responses, which you have to be the one to act on.
An MCP server can allow an LLM to carry out actions itself.
Less talking, more doing.</p>
<figure>
<img
src="https://www.jumpingrivers.com/blog/what-is-an-mcp-server/assets/llm-alone-vs-mcp-connected.webp"
alt="Two-panel diagram. Left: an LLM alone, with training data only, frozen at its training
cut-off with no way to check anything new. Right: an LLM connected via MCP to a database, email,
a filesystem and the internet, grounded in live, current data.">
<figcaption>
An LLM alone is frozen at its training cut-off. Connected via MCP, it can check, fetch and act
instead of just recalling.
</figcaption>
</figure>
<aside class="advert">
<p>
Join us for our AI in Production conference! For more details, check out our <a href="https://ai-in-production.jumpingrivers.com/" rel="nofollow" target="_blank">conference website!</a>
</p>
</aside>
<h2 id="why-not-just-use-an-api">Why not just use an API?</h2>
<!--- Clarify this paragraph ; NxM is opaque -->
<p>There are many ways to connect external capabilities to an AI model, usually via an API (Application
Programming Interface), and this was the industry standard.
However, APIs are all configured slightly differently, and once you start to scale up the number of
connections you need between multiple pieces of software, you hit a combinatorial problem: the
number of custom connections a product needs multiplies by the number of tools that use it.</p>
<p><a href="https://www.anthropic.com/news/model-context-protocol" rel="nofollow" target="_blank">Anthropic introduced MCP</a> in late 2024 as an
open standard for connecting tools and data to LLM applications. It works like a USB port on a
computer: any MCP server can be used with any agent/model, whether you are using Claude, Gemini,
ChatGPT or one you’ve made yourself.</p>
<figure>
<img
src="https://www.jumpingrivers.com/blog/what-is-an-mcp-server/assets/mcp-before-after-diagram-v2.webp"
alt="Before-and-after diagram. Before: five products each need their own custom connection to
three AI tools, giving 15 separate connections to build and maintain. After: all five products
connect through MCP, one standard layer, giving eight total connections through one protocol.">
<figcaption>MCP replaces a tangle of custom connections with one standard layer.</figcaption>
</figure>
<h2 id="building-your-own-mcp-server">Building your own MCP server</h2>
<p>Full instructions on how to build an MCP server are available in the
<a href="https://modelcontextprotocol.io/docs/develop/build-server" rel="nofollow" target="_blank">MCP documentation</a>, but here is a very
quick overview using Claude Code and the Sonnet 5 model.</p>
<p>The good news is that a working MCP server can be as short as a few lines of code.
Here’s the shape of it, step by step, using Python.</p>
<h3 id="step-1-install-the-dependencies">Step 1: Install the dependencies</h3>
<p>The
<a href="https://github.com/modelcontextprotocol" rel="nofollow" target="_blank">MCP project</a> maintains official SDKs for Python,
TypeScript and other languages.
The Python ‘mcp’ package embodies this SDK.
However, here we are going to use the ‘fastmcp’ package.
This is built upon ‘mcp’, but has some benefits (in particular, ‘fastmcp’-based tools can readily
be deployed on Posit Connect, see later).</p>
<pre>mkdir -p ~/projects/
cd ~/projects
uv init --python=3.15 dice-roll
# Initialized project `dice-roll`
# at `/home/&lt;user&gt;/projects/dice-roll`
cd dice-roll
uv add fastmcp
</pre><p>This gives us the following project structure, which is typical for a ‘uv’ project.
We will define our MCP server in <code>./src/dice_roll/__init__.py</code>, although if the project got much
larger, we would split it into separate modules or sub-packages.</p>
<pre>tree -L 2
.
├── pyproject.toml
├── README.md
├── src
│   └── dice_roll
└── uv.lock
</pre><h3 id="step-2-define-a-tool">Step 2: Define a tool</h3>
<p>An MCP server exposes “tools”: functions the model can call.
Here is a mini example: a server with a single tool that rolls a dice.</p>
<pre># src/dice_roll/__init__.py
from typing import Annotated
import random

from fastmcp import FastMCP
from pydantic import Field

# Give your server a name: this is what shows up when a model connects
mcp = FastMCP(&quot;dice-pick&quot;)

@mcp.tool
def roll_dice(
 sides: Annotated[int, Field(description=&quot;Number of sides on the dice&quot;, ge=2)] = 6,
) -&gt; int:
 &quot;&quot;&quot;Roll a single fair dice with the given number of sides and return the number rolled.&quot;&quot;&quot;
 return random.randint(1, sides)

def main() -&gt; None:
 mcp.run()
</pre><p>The <code>@mcp.tool</code> docstring (“Roll a single …”) is important.
This becomes the description of the tool that the LLM reads.
It is like the blurb of a book: the LLM will look at the description and from it decide whether it
wishes to use the tool or not.
If this description isn’t clear, it may lead to the tool not being used.</p>
<p>The type annotations for both the parameter <code>sides</code> and the value returned by <code>roll_dice()</code> also
provide useful metadata for the LLM.
Our <code>sides</code> parameter has a constraint that ensures at least 2 sides are present on the dice
(<code>ge=2</code>; this is enforced within Python by
<a href="https://pypi.org/project/pydantic/" rel="nofollow" target="_blank">Pydantic</a>).
When describing the tools provided by this MCP server, an LLM will be able to explain that the
minimum number of sides is 2 (and more advanced models may not call the tool otherwise).</p>
<h3 id="step-3-connect-it">Step 3: Connect it</h3>
<p>Save the code in the project, and we will test whether an MCP-compatible agent can use it.
There are a few ways to register our locally-defined MCP server with Claude Code.</p>
<p>Since we have used the <code>fastmcp</code> framework, there is a command line tool that installs the MCP
server for use from the project directory:</p>
<pre># From the ~/projects/dice-roll/ directory
uv run \
 fastmcp install claude-code src/dice_roll/__init__.py:mcp --with-editable .
</pre><p>The <code>uv run</code> part is necessary to ensure the correct Python environment is used.
The <code>fastmcp</code> command needs to find the <code>mcp</code> object that we have defined in our source code.
If we open <code>claude</code> and ask “What dice-pick tools do you know about?”, it replies:</p>
<pre>The dice-pick MCP server gives me one tool:
roll_dice: rolls one fair dice and returns the number it lands on.
┌───────────┬─────────┬─────────┬───────────────────┐
│ Parameter | Type │ Default │ Constraint │
├───────────┼─────────┼─────────┼───────────────────┤
│ sides │ integer │ 6 │ must be 2 or more │
└───────────┴─────────┴─────────┴───────────────────┘
So I can roll a d6, d20, d100 or any other single dice. It can&#39;t roll several dice in one call, add
modifiers or pick from a list. For something like 3d6, I&#39;d call it three times.
</pre><p>Then we can use the tool:</p>
<pre>❯ Roll a dice
Called dice-pick
I rolled a 6 on a standard six-sided dice.
...
❯ Roll a 20-side dice
Called dice-pick
I rolled a 20 on the d20.
That&#39;s the highest number on both rolls so far. [... blah blah blah ...]
...
❯ Roll a d12
Called dice-pick
I rolled a 4 on the d12, so the run of top scores is over.
</pre><p>We have installed our locally in-development MCP server on our machine, but it’s only usable from
the <code>~/projects/dice_roll</code> directory.
This is fine while you are testing, but you may want to use your MCP server in other locations once
it works well.</p>
<p>Our project is <code>uv</code>-based.
If we tell it which module and function should be called to start the project running, we can make
the project more easily usable from other locations.
But, the entry-point function used to start our MCP server running is <code>dice_roll.main()</code>, which <code>uv</code>
has already added as the entry-point to the project in the <code>pyproject.toml</code>:</p>
<pre>[project.scripts]
dice-roll = &quot;dice_roll:main&quot;
</pre><p>On running <code>uv run python</code> or <code>uv sync</code>, a <code>dice-roll</code> entry-point will be added to the virtual
environment for the project.</p>
<p>For Claude Code, we can then register our MCP server with a single command:</p>
<pre>claude mcp add --scope user dice-pick \
 -- /abs/path/to/projects/dice-roll/.venv/bin/dice-roll
</pre><p>Now, you can ask <code>claude</code> to “roll a 20-sided dice” from any directory on your computer and it will
call your tool directly rather than guessing at an answer.
This is possible because <code>claude mcp add ...</code> appends to the <code>mcpServers</code> entry of a file
(<code>~/.claude.json</code>).</p>
<p>A few tips before you build something bigger:</p>
<ul>
<li>Keep tool descriptions (the docstrings for your <code>@mcp.tool</code>-decorated functions) short and
specific. The model relies on the docstring to decide when to use a tool, so vague descriptions
lead to it being ignored or misused.</li>
<li>Think about what should require confirmation. Read-only tools (like looking something up) are
usually safe to let a model call freely; tools that change or delete something are worth gating
behind an explicit user confirmation step.</li>
</ul>
<p>You can remove the MCP server from your <code>claude</code> setup by using</p>
<pre>claude mcp remove dice-pick
</pre><p>Or just ask Claude to remove it!</p>
<h2 id="deploying-to-posit-connect">Deploying to Posit Connect</h2>
<p>A server running on your own machine is fine for testing, but it is only useful to you.
MCP servers can be deployed to a range of different cloud providers.
<a href="https://posit.co/products/enterprise/connect/" rel="nofollow" target="_blank">Posit Connect</a>, the platform we normally reach for
when publishing Shiny apps, Plumber APIs and Quarto reports, added first-class support for hosting
MCP servers in 2026, complete with a dedicated icon and its own tab in the content list.</p>
<p>Posit Connect does expect you to write your Python-based MCP servers using the <code>fastmcp</code> framework
that we used above.
Connect also needs to import a runnable application object that can handle HTTP-based requests.
When deployed, ‘fastmcp’-based MCP servers can expose an ASGI <code>app</code> object that handles these
requests.
Locally run servers use ‘stdio’ routing, but we only have to modify our current source code slightly
to deploy to Posit Connect.</p>
<pre># imports ...

mcp = ...

def roll_dice():
 ...

app = mcp.http_app() # For deployment on Connect

def main():
 mcp.run() # For running using stdio locally
</pre><p>For deployment of the MCP server to Posit Connect, we also need to create a <code>requirements.txt</code> file.
These are used to specify the run-time (and sometimes development) dependencies of a project.
Since we used <code>uv</code> to manage the project, this is easy to create:</p>
<pre>uv export --no-dev --no-hashes -o requirements.txt
</pre><p>The <code>requirements.txt</code> file contains a line <code>-e .</code> that dynamically includes any source code from
<code>src/</code>.
The Python environment that is built for your MCP server depends upon <code>requirements.txt</code> and is
rebuilt when that file is updated.</p>
<!---
Note that you need the "-e ." line, so that the fresh code is installed `.` causes cache problems
Because the environment within which your MCP is deployed is cache-keyed by requirements.txt
(So subsequent uploads where src/ changes won't invalidate the environment cache)
-->
<p>We use the <code>rsconnect-python</code> package (and the <code>rsconnect</code> command line tool that it installs) to
interact with Posit Connect.
You may first have to register with your Connect system using <code>rsconnect add ...</code> or
<code>rsconnect login ...</code>.</p>
<p>Then you can deploy with the <code>rsconnect</code> tool, using the FastAPI deployment mode and pointing the
entrypoint at the <code>app</code> object that we have just added to the source code.
This is done using a <code>--entrypoint</code> argument of the form <code>module:object_name</code>. Here this is
<code>dice_roll:app</code>, since our <code>app</code> object is in the <code>dice_roll</code> module:</p>
<pre>rsconnect deploy fastapi -n &lt;nickname-of-posit-server&gt; --entrypoint dice_roll:app ./
</pre><p>The Posit Publisher extension for VS Code and Positron works too, if you’d rather deploy from your
editor.
Posit has some <a href="https://docs.posit.co/connect/user/mcp-servers/" rel="nofollow" target="_blank">good documentation</a> on publishing
MCP servers.</p>
<p>On first deployment, Connect scans <code>requirements.txt</code>, recognises <code>fastmcp</code>, and automatically sets
the content type to MCP.
Once deployed, it keeps at least one process warm so there is no cold-start delay, and gives
it its own icon in the content list, with no extra configuration needed.</p>
<h2 id="using-your-deployed-mcp-server">Using your deployed MCP server</h2>
<p>To use an MCP server that is deployed in the cloud, the process is very similar to how we added our
locally-defined MCP server.
On Posit Connect (or wherever you have published), you will have a URL for the MCP server and you
can configure <code>claude</code> to use this URL as follows:</p>
<pre>claude mcp add --transport http &lt;mcp-name&gt; https://&lt;connect-server&gt;/content/&lt;content-hash&gt;/mcp
</pre><p>Here we used <code>dice-pick</code> as the name of the MCP server, consistent with the name in the source code.
When you next open <code>claude</code> you will be prompted to authenticate before you can use the tool.</p>
<p>Want to see this in action? Watch
<a href="https://www.youtube.com/watch?v=1mtWbxQE8S8" rel="nofollow" target="_blank">this walkthrough</a>, recorded live at our
<a href="https://aip.jmpr.io/" rel="nofollow" target="_blank">AI in Production 2026</a> conference, of an MCP server being built and used from
scratch.</p>
<h2 id="key-takeaways">Key takeaways</h2>
<ul>
<li>An MCP server is the standard way to give an AI model access to things it can’t do alone:
checking a calendar, reading a file, searching the web, or looking up live data.</li>
<li>It works like a USB port: build the connection once, and any compatible model can use it.</li>
<li>A working MCP server can be as short as a few lines of code. The model calls your tools directly
instead of guessing at an answer.</li>
<li>Keep tool descriptions short and specific, and gate anything destructive behind explicit
confirmation.</li>
<li>Posit Connect can host your MCP server directly, so your whole team can use it rather than just
your own machine.</li>
</ul>
<p>
For updates and revisions to this article, see the <a href = "https://www.jumpingrivers.com/blog/what-is-an-mcp-server/">original post</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.jumpingrivers.com/blog/what-is-an-mcp-server/"> The Jumping Rivers 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/what-is-an-mcp-server/">What is an MCP server?</a>]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">404235</post-id>	</item>
		<item>
		<title>Can we trust the correlation coefficient in designed field experiments?</title>
		<link>https://www.r-bloggers.com/2026/10/can-we-trust-the-correlation-coefficient-in-designed-field-experiments/</link>
		
		<dc:creator><![CDATA[Andrea Onofri]]></dc:creator>
		<pubDate>Thu, 15 Oct 2026 22:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://www.statforbiology.com/posts/general_CorrelationIndependence1.html</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>One of the fundamental aims of field experiments is to explorare the relationships between several experimental traits, which have been recorded/measured altogheter for all the experimental units. For example, think about a genotype experiment w...</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/can-we-trust-the-correlation-coefficient-in-designed-field-experiments/">Can we trust the correlation coefficient in designed field experiments?</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_CorrelationIndependence1.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>One of the fundamental aims of field experiments is to explorare the relationships between several experimental traits, which have been recorded/measured altogheter for all the experimental units. For example, think about a genotype experiment with 27 winter wheat genotypes in a Randomised Complete Block Design with three replicates. If we aim at assessing yield and yield quality, we have to measure, in each plot, several traits, such as grain yield and the weight of thousand kernels (TKW). Apart from separately analysing these two variables, we might be interested in describing their reciprocal relationships, by using the Pearson’s correlation coefficient. Let’s do this, by using the dataset ‘WheatQuality’, which is available in the ‘statforbiology’ package; we have 81 plots and just as many couples of measures in all.</p>
<div class="cell">
<pre>library(Hmisc)
library(dplyr)
library(statforbiology)
dataset &lt;- getAgroData(&quot;WheatQuality&quot;)
head(dataset)
##     Genotype Block Height  TKW Whectol Yield
## 1 arcobaleno     1     90 44.5    83.2 64.40
## 2 arcobaleno     2     90 42.8    82.2 60.58
## 3 arcobaleno     3     88 42.7    83.1 59.42
## 4       baio     1     80 40.6    81.8 51.93
## 5       baio     2     75 42.7    81.3 51.34
## 6       baio     3     76 41.1    81.1 47.78</pre>
</div>
<p>Now, we consider the whole lot of measures and calculate the correlation coefficient between yield and TKW; we also use a t-test for the hypothesis that the correlation coefficient is equal to 0. This is the result:</p>
<div class="cell">
<pre>ra &lt;- with(dataset, rcorr(Yield, TKW) )
# 
# Correlation coefficient
ra$r[1,2] 
## [1] 0.540957
#
# P-level
ra$P[1,2] 
## [1] 1.850931e-07</pre>
</div>
<p>The conclusion is: the correlation coefficient is positive and significantly different from 0, which supports the idea that plots with a high value on yield tend to have a high value on TKW, as well.</p>
<p>Such an approach is very often seen also in high ranking Journals, but: is it correct? Can we trust the conclusions? The answer is no, not completely. And here is why. Please, note that in this post I am following the point made by Bland and Altman (1994), by using an example that is more closely related to plant/crop science.</p>
<section id="statistical-testing-is-invalid" class="level1">
<h1>Statistical testing is invalid</h1>
<p>The correlation coefficient is a measure of joint variability and makes some basic assumptions (e.g. linearity), which I have detailed elsewhere (<a href="https://www.statforbiology.com/2019/stat_general_correlation_alookat/" rel="nofollow" target="_blank">see here</a>). In particular, the t-test requires that all the couples of observations are taken on independent subjects.</p>
<p>In our case, the 81 plots are not independent, as they are grouped by block and genotype, so that observations in the same block or for the same genotype are more alike than observations in different blocks and different genotypes (intra-class correlation). Consequently, the t-test is invalid and <strong>we are not allowed to conclude that the correlation is significant, because such a conclusion is not necessarily supported by the data</strong>.</p>
<p>Unfortunately, such lack of independence relates to all designed experiments, which seems to be a fact that goes largely unnoticed. <strong>In general correlation coefficients calculated for raw data from designed experiments, cannot be submitted to t-tests, without accounting for the effects of all treatment and grouping factors.</strong></p>
</section>
<section id="correlation-is-not-unique" class="level1">
<h1>Correlation is not unique</h1>
<p>Apart from statistical testing, the correlation coefficient we have calculated above (0.541) maintain its value as a descriptive measure of joint variability. But are we sure that using the raw observed data is the best way to go?</p>
<p>For one, such correlation coefficient highlights that yield and TKW are positively correlated, but is this true for all genotypes and for all blocks? Let’s check this.</p>
<div class="cell">
<pre># Correlations within genotypes
wgCors &lt;- by(dataset, list(dataset$Genotype), function(x) cor(x$Yield, x$TKW))
wgCors
## : arcobaleno
## [1] 0.9847228
## ------------------------------------------------------------ 
## : baio
## [1] 0.1611952
## ------------------------------------------------------------ 
## : claudio
## [1] -0.9993872
## ------------------------------------------------------------ 
## : colorado
## [1] 0.9837293
## ------------------------------------------------------------ 
## : colosseo
## [1] 0.4564855
## ------------------------------------------------------------ 
## : creso
## [1] -0.5910193
## ------------------------------------------------------------ 
## : duilio
## [1] -0.988233
## ------------------------------------------------------------ 
## : gianni
## [1] -0.7603802
## ------------------------------------------------------------ 
## : giotto
## [1] 0.9520211
## ------------------------------------------------------------ 
## : grazia
## [1] 0.4980828
## ------------------------------------------------------------ 
## : iride
## [1] 0.7563338
## ------------------------------------------------------------ 
## : meridano
## [1] 0.1174342
## ------------------------------------------------------------ 
## : neodur
## [1] 0.4805871
## ------------------------------------------------------------ 
## : orobel
## [1] 0.8907754
## ------------------------------------------------------------ 
## : pietrafitta
## [1] -0.9633891
## ------------------------------------------------------------ 
## : portobello
## [1] 0.9210135
## ------------------------------------------------------------ 
## : portofino
## [1] -0.9900764
## ------------------------------------------------------------ 
## : portorico
## [1] 0.1394211
## ------------------------------------------------------------ 
## : preco
## [1] 0.9007067
## ------------------------------------------------------------ 
## : quadrato
## [1] -0.5840238
## ------------------------------------------------------------ 
## : sancarlo
## [1] -0.646067
## ------------------------------------------------------------ 
## : simeto
## [1] -0.4051779
## ------------------------------------------------------------ 
## : solex
## [1] -0.6066363
## ------------------------------------------------------------ 
## : terrabianca
## [1] -0.4076416
## ------------------------------------------------------------ 
## : verdi
## [1] 0.5801404
## ------------------------------------------------------------ 
## : vesuvio
## [1] -0.7797493
## ------------------------------------------------------------ 
## : vitromax
## [1] -0.8056514

# Correlations within blocks
wbCors &lt;- by(dataset, list(dataset$Block), function(x) cor(x$Yield, x$TKW))
wbCors
## : 1
## [1] 0.5998137
## ------------------------------------------------------------ 
## : 2
## [1] 0.539999
## ------------------------------------------------------------ 
## : 3
## [1] 0.5370398</pre>
</div>
<p>As for the genotypes, we have 27 correlation coefficients, ranging from -0.999 to 0.985. We only have three couples of measurements per genotype and it is clear that this is not much, to reliably estimate a correlation coefficient. However, it is enough to be suspicious about the extent of correlation between yield and TKW, as it may depend on the genotype.</p>
<p>On the other hand, the correlation within blocks is more constant, independent on the block and similar to the correlation between plots.</p>
<p>It may be interesting to get an estimate of the average within-group correlation. To this aim, we can perform two separate ANOVAs (one per trait), including all relevant effects (blocks and genotypes) and calculate the correlation between the residuals:</p>
<div class="cell">
<pre>mod1 &lt;- lm(Yield ~ factor(Block) + Genotype, data = dataset)
mod2 &lt;- lm(TKW ~ factor(Block) + Genotype, data = dataset)
wCor &lt;- rcorr(residuals(mod1), residuals(mod2))
wCor$r
##            x          y
## x 1.00000000 0.03133109
## y 0.03133109 1.00000000
wCor$P
##           x         y
## x        NA 0.7812693
## y 0.7812693        NA</pre>
</div>
<p>The average within-group correlation is very small and unsignificant. Let’s think about this correlation: residuals represent yield and TKW values for all plots, once the effects of blocks and genotypes have been removed. A high correlation of residuals would mean that, letting aside the effects of the block and genotype to which it belongs, a plot with a high value on yield also shows a high value on TKW. <strong>The correct conclusion, indeed, is that, once the effects of treatment ang grouping factors have been removed, there is no correlation between the traits under study.</strong></p>
<p>But, this is not enough to stop our investigations. We could also consider the means for genotypes/blocks and see whether they are correlated. Blocks and genotypes are independent and, in principle, significance testing is permitted. However, this is not recommended with block means, as three data are too few to make any tests.</p>
<div class="cell">
<pre>means &lt;- aggregate(dataset[,c(4,6)], list(dataset$Genotype), mean)
rgPear &lt;- rcorr( as.matrix(means[,2:3]) )
rgPear$r
##             TKW     Yield
## TKW   1.0000000 0.5855966
## Yield 0.5855966 1.0000000
rgPear$P
##              TKW      Yield
## TKW           NA 0.00133149
## Yield 0.00133149         NA

means &lt;- aggregate(dataset[,c(4,6)], list(dataset$Block), mean)
rbPear &lt;- cor( as.matrix(means[,2:3]) )
rbPear
##               TKW       Yield
## TKW    1.00000000 -0.08812544
## Yield -0.08812544  1.00000000</pre>
</div>
<p>We note that the correlation between genotype means is high and significant. On the contrary, the correlation between block means is near to 0.</p>
</section>
<section id="and-so-what" class="level1">
<h1>And so what?</h1>
<p>At this stage, you may be confused… Let’s try to clear the fog.</p>
<p>A naive correlation analysis on raw data brought us to the conclusion that yield and TKW are significantly correlated. Unfortunately, such a conclusion is overly simplistic and it is hardly supported by the data.</p>
<p>A more careful process of data analysis should not neglect that in every designed field experiment we have groups of subjects and there are several possible types of correlation:</p>
<ol type="1">
<li>correlation between plot measurements (naive correlation)</li>
<li>correlation between the residuals</li>
<li>correlation between treatment/block means</li>
</ol>
<p>All these correlations should be investigated and used for interpretation.</p>
<ol type="1">
<li>The ‘naive’ correlation between the plot measurements is very easily calculated, but it is grossy misleading. Indeed, it disregards the treatment/block effects and it does not permit hypotheses testing, as the subjects are not independent. <strong>I think it is reasonable to suggest that the ‘naive’ correlation coefficient is never used for interpretation</strong>.</li>
<li>The correlation between the residuals is a reliable measure of joint variation, because the experimental design is adequately referenced, by removing the effects of tratments/blocks. In this example, residuals are not correlated. Further analyses show that the correlation between yield and TKW, if any, may depend on the genotype, while it does not depend on the block.</li>
<li>The correlation between treatment/block means permits to assess whether the treatment/block effects on the two traits are correlated. In this case, while we are not allowed to conclude that yield and TKW are, in general, correlated, we can conclude that the genotypes with a high level of yield also show a high level of TKW.</li>
</ol>
<p>An elegant and advanced method to tackle correlation analyses on data from designed field experiments has been put forward by Piepho (2018), within the frame of mixed models. But this deserves another post.</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>
</section>
<section id="references" class="level1">
<h1>References</h1>
<ol type="1">
<li>Bland, J.M., Altman, D.G., 1994. Statistics Notes: Correlation, regression, and repeated data. BMJ 308, 896–896.</li>
<li>Piepho, H.-P., 2018. Allowing for the structure of a designed experiment when estimating and testing trait correlations. The Journal of Agricultural Science 156, 59–70.</li>
</ol>
<hr>
<p>Originally published on 2019-04-30</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_CorrelationIndependence1.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/can-we-trust-the-correlation-coefficient-in-designed-field-experiments/">Can we trust the correlation coefficient in designed field experiments?</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404261</post-id>	</item>
		<item>
		<title>{brochure}: multipage {shiny} apps, now on CRAN</title>
		<link>https://www.r-bloggers.com/2026/10/brochure-multipage-shiny-apps-now-on-cran/</link>
		
		<dc:creator><![CDATA[Colin Fay]]></dc:creator>
		<pubDate>Thu, 15 Oct 2026 16:16:30 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://rtask.thinkr.fr/?p=30028</guid>

					<description><![CDATA[<p>You can read the original post in its original format on Rtask website by ThinkR here: {brochure}: multipage {shiny} apps, now on CRAN<br />
A {shiny} app is a single page: one url, one UI, one server function. That is comfortable while the app stays small. As soon as you want ...</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/brochure-multipage-shiny-apps-now-on-cran/">{brochure}: multipage {shiny} apps, now 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://rtask.thinkr.fr/brochure-multipage-shiny-apps-now-on-cran/"> 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/brochure-multipage-shiny-apps-now-on-cran/" target="_blank">{brochure}: multipage {shiny} apps, now on CRAN</a></p>
<p>A <code>{shiny}</code> app is a single page: one url, one UI, one server function. That is comfortable while the app stays small. As soon as you want an actual site — a home page, a contact page, one page per user, urls people can bookmark or send by email — you end up faking pages with tabs and <code>conditionalPanel()</code>.</p>
<p><code>{brochure}</code> takes the problem the other way around: every page is a real page, with its own url, its own UI, its own server function and its own Shiny session.</p>
<pre>install.packages(&quot;brochure&quot;)
library(shiny)
library(brochure)

brochureApp(
  page(
    href = &quot;/&quot;,
    ui = fluidPage(
      h1(&quot;Home&quot;),
      tags$a(href = &quot;/contact&quot;, &quot;Contact us&quot;)
    ),
    server = function(input, output, session) {
      # ...
    }
  ),
  page(
    href = &quot;/contact&quot;,
    ui = fluidPage(h1(&quot;Contact&quot;))
  )
)</pre>
<p>You run it as you would any Shiny app, and moving from one page to another is a plain link. A url can also carry a parameter — <code>/user/:id</code>, which the page reads back with <code>get_keys()</code>.</p>
<p>Around that, <code>{brochure}</code> gives you what you expect from a web server: request and response handlers to log, serve a healthcheck or answer before Shiny ever runs, helpers to set and read cookies (and so carry state from one page to the next), redirections, and what it takes to serve the app behind a reverse proxy or on Posit Connect.</p>
<p>The trade-off: you leave the single page paradigm behind, and nothing computed on one page survives a navigation to another. That is what <code>vignette(&quot;design&quot;)</code> is about — worth reading before you start.</p>
<p>The full post, with examples and what this release brings:</p>
<p><img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f449.png" alt="👉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://www.golemverse.org/news/brochure-1.0.0-release-on-cran/" rel="nofollow" target="_blank">{brochure} 1.0.0 is now on CRAN</a></p>
<p>Documentation: <a href="https://colinfay.me/brochure/" rel="nofollow" target="_blank">colinfay.me/brochure</a>.</p>
<p>This post is better presented on its original ThinkR website here: <a rel="nofollow" href="https://rtask.thinkr.fr/brochure-multipage-shiny-apps-now-on-cran/" target="_blank">{brochure}: multipage {shiny} apps, now on CRAN</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/brochure-multipage-shiny-apps-now-on-cran/"> 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/10/brochure-multipage-shiny-apps-now-on-cran/">{brochure}: multipage {shiny} apps, now on CRAN</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404245</post-id>	</item>
		<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>Cytometry in R and opportunities to contribute to Bioconductor</title>
		<link>https://www.r-bloggers.com/2026/10/cytometry-in-r-and-opportunities-to-contribute-to-bioconductor/</link>
		
		<dc:creator><![CDATA[Maria Doyle]]></dc:creator>
		<pubDate>Thu, 15 Oct 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://blog.bioconductor.org/posts/2026-10-15-cytometry-in-r/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>Flow cytometry researchers have a growing collection of R and Bioconductor tools available to them. But for someone who is comfortable working with cytometry data and has little programming experience, knowing where to begin can be difficult. T...</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/cytometry-in-r-and-opportunities-to-contribute-to-bioconductor/">Cytometry in R and opportunities to contribute to Bioconductor</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-15-cytometry-in-r/"> 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><img src="https://i0.wp.com/blog.bioconductor.org/posts/2026-10-15-cytometry-in-r/WebsiteBanner.jpg?w=578&#038;ssl=1" class="img-fluid" data-recalc-dims="1"></p>
<p>Flow cytometry researchers have a growing collection of R and Bioconductor tools available to them. But for someone who is comfortable working with cytometry data and has little programming experience, knowing where to begin can be difficult. That is the gap the free Cytometry in R course aims to fill.</p>
<section id="a-starting-point-for-cytometry-researchers" class="level2">
<h2 class="anchored" data-anchor-id="a-starting-point-for-cytometry-researchers">A starting point for cytometry researchers</h2>
<p>Developed by David Rach and colleagues at the Flow Cytometry Shared Resource of the University of Maryland Greenebaum Comprehensive Cancer Center, Cytometry in R began in February 2026 as a free weekly course for people with cytometry experience but little or no coding background.</p>
<p>The course starts with practical foundations, including setting up R, working with GitHub and installing packages, before moving into data handling, visualisation and cytometry analysis workflows. Sessions are offered online and in person, with recordings, code and teaching materials openly available. Learners can work through the materials at their own pace and ask questions through GitHub Discussions.</p>
<p>The response has been encouraging. In his <a href="https://youtu.be/krVI-HXm4_w?si=E3fzttBKJr2zFHfI&#038;t=1475" rel="nofollow" target="_blank">BioC2026 presentation</a>, David reported close to 2,000 expressions of interest worldwide and several hundred viewers for individual weekly topics.</p>
<p><a href="https://umgcccfcsr.github.io/CytometryInR/Schedule.html" rel="nofollow" target="_blank">Explore the course materials and schedule</a> | <a href="https://youtube.com/playlist?list=PL7tGF31b_cVqEkSLFvAX3kbtzUVpMe-VB&#038;si=nsC9Wmg_Jvc5uVG3" rel="nofollow" target="_blank">Watch course recordings</a></p>
</section>
<section id="what-comes-after-the-introductory-course" class="level2">
<h2 class="anchored" data-anchor-id="what-comes-after-the-introductory-course">What comes after the introductory course?</h2>
<p>At the October 2026 <a href="https://training.bioconductor.org/" rel="nofollow" target="_blank">Bioconductor Training Committee</a> meeting, David described an opportunity to build on the course. It introduces many cytometry packages, but a gap remains between the introductory training and the knowledge needed to work through some individual package vignettes.</p>
<p>Additional materials addressing these gaps could help. Package authors, experienced users and instructors might contribute worked examples, reproducible case studies or step-by-step tutorials. These could help learners explore packages beyond those covered by the course and apply them to their own research questions.</p>
</section>
<section id="connecting-course-participants-and-package-developers" class="level2">
<h2 class="anchored" data-anchor-id="connecting-course-participants-and-package-developers">Connecting course participants and package developers</h2>
<p>David, who recently joined Bioconductor’s <a href="https://bioconductor.org/about/community-advisory-board/" rel="nofollow" target="_blank">Community Advisory Board</a>, now coordinates the newly established <a href="https://workinggroups.bioconductor.org/currently-active-working-groups-committees.html#cytometry" rel="nofollow" target="_blank">Bioconductor Cytometry Working Group</a>, which aims to support community efforts around documentation and longer-term maintenance of packages for conventional, spectral, imaging and mass cytometry.</p>
<p>His plan is to reconnect with package developers, understand their priorities and identify where additional documentation or support would be welcome. One possibility is a focused writing or documentation sprint, bringing interested Cytometry in R participants together with package maintainers to work on specific, manageable tasks.</p>
<p>This could give learners a supported route into contributing: learning to use a package, gaining confidence with its documentation and examples, and helping improve the experience for the next person.</p>
</section>
<section id="get-involved" class="level2">
<h2 class="anchored" data-anchor-id="get-involved">Get involved</h2>
<p>There are several ways to contribute, depending on your experience and interests:</p>
<ul>
<li><strong>Package developers</strong> could identify documentation gaps and suggest tasks where community input would be welcome.</li>
<li><strong>Experienced users and instructors</strong> could develop tutorials, examples or bridging materials for learners ready to explore further.</li>
<li><strong>Course participants</strong> could test instructions, point out confusing steps and take part in supported documentation activities as these develop.</li>
</ul>
<p>Interested in joining or following the discussions? <a href="https://community-bioc.zulipchat.com/join/5pgmjejclfuvidu5dbu46xqw/" rel="nofollow" target="_blank">Join Bioconductor Zulip</a> and subscribe to the <strong>cytometry</strong> channel to introduce yourself, share ideas and help shape the group’s first activities. Users, learners, instructors and package developers are all welcome.</p>
<p>For questions about the course itself, the <a href="https://github.com/UMGCCCFCSR/CytometryInR/discussions" rel="nofollow" target="_blank">Cytometry in R GitHub Discussions</a> remain a place to connect with instructors and fellow learners.</p>
<p>The course offers an accessible starting point for cytometry researchers new to R. The working group provides a way to build on that learning and contribute to the tools and resources the community relies on.</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-15-cytometry-in-r/"> 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/cytometry-in-r-and-opportunities-to-contribute-to-bioconductor/">Cytometry in R and opportunities to contribute to Bioconductor</a>]]></content:encoded>
					
		
		<enclosure url="https://blog.bioconductor.org/posts/2026-10-15-cytometry-in-r/WebsiteBanner.jpg" length="0" type="image/jpeg" />

		<post-id xmlns="com-wordpress:feed-additions:1">404239</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>
	</channel>
</rss>
