<?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>Tue, 10 Nov 2026 00:00:00 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=5.5.24</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>Growing the BiocAsia community</title>
		<link>https://www.r-bloggers.com/2026/11/growing-the-biocasia-community/</link>
		
		<dc:creator><![CDATA[Maria Doyle]]></dc:creator>
		<pubDate>Tue, 10 Nov 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://blog.bioconductor.org/posts/2026-11-10-growing-biocasia-community/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>With BiocAsia 2026 coming up in Melbourne, community members have been organising seminars and exploring ways to take part locally.<br />
More than 200 people joined the latest BiocAsia seminar on 13 October 2026. The session ran for two and a half ho...</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/11/growing-the-biocasia-community/">Growing the BiocAsia community</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://blog.bioconductor.org/posts/2026-11-10-growing-biocasia-community/"> 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>With <a href="https://biocasia2026.bioconductor.org/" rel="nofollow" target="_blank">BiocAsia 2026</a> coming up in Melbourne, community members have been organising seminars and exploring ways to take part locally.</p>
<p>More than 200 people joined the latest <a href="https://biocasia2026.bioconductor.org/seminars/" rel="nofollow" target="_blank">BiocAsia seminar</a> on 13 October 2026. The session ran for two and a half hours, with discussion continuing well beyond the planned finish. It was the first seminar in the series hosted from China, with Mandarin as the main language. Shixiang Wang spoke about building an open-source ecosystem around UCSC Xena and visualisation tools, and Zhougeng Xu shared his experience developing open-source tools for AI in science.</p>
<p>The seminar was organised by Zuguang Gu, a member of the <a href="https://bioconductor.org/about/community-advisory-board/" rel="nofollow" target="_blank">Bioconductor Community Advisory Board</a> and an active contributor to BiocAsia based in China. The next seminar will take place in December.</p>
<div style="text-align: center;">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://i2.wp.com/blog.bioconductor.org/posts/2026-11-10-growing-biocasia-community/biocasia-seminar.jpg?w=578&#038;ssl=1" class="img-fluid figure-img" alt="Screenshot of the online seminar during Zhougeng Xu’s talk." data-recalc-dims="1"></p>
<figcaption>Zhougeng Xu’s presentation at the October 2026 BiocAsia seminar</figcaption>
</figure>
</div>
</div>
<p>Alongside the seminars, Jyoti Bhogal and Malvika Kharbanda have been discussing watch party options in India for BiocAsia 2026. The aim is to give people who cannot travel to Melbourne a chance to take part in the conference and meet others nearby.</p>
<p>Join us at <a href="https://biocasia2026.bioconductor.org/" rel="nofollow" target="_blank">BiocAsia 2026</a> on <strong>19 and 20 November at RMIT City Campus, Melbourne</strong>, for workshops, talks and a chance to meet others working with R and Bioconductor. <a href="https://biocasia2026.bioconductor.org/registration/" rel="nofollow" target="_blank"><strong>Registration is open</strong></a>, and we’d love to see you there.</p>
<p>To keep in touch between events, <a href="https://community-bioc.zulipchat.com/join/cr4isltci5v232hkkrhti656/" rel="nofollow" target="_blank">join the <strong>BiocAsia channel on Bioconductor Zulip</strong></a>. It’s a place to hear about future seminars, discuss watch parties and share ideas for other activities.</p>
<p>A big thank you to the speakers, organisers and everyone giving their time to make these activities happen.</p>



<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-11-10-growing-biocasia-community/"> 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/11/growing-the-biocasia-community/">Growing the BiocAsia community</a>]]></content:encoded>
					
		
		<enclosure url="https://blog.bioconductor.org/posts/2026-11-10-growing-biocasia-community/biocasia-seminar.jpg" length="0" type="image/jpeg" />

		<post-id xmlns="com-wordpress:feed-additions:1">404767</post-id>	</item>
		<item>
		<title>Using R to make an “informational video”</title>
		<link>https://www.r-bloggers.com/2026/11/using-r-to-make-an-informational-video/</link>
		
		<dc:creator><![CDATA[nsaunders]]></dc:creator>
		<pubDate>Mon, 09 Nov 2026 00:56:56 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">http://nsaunders.wordpress.com/?p=5750</guid>

					<description><![CDATA[<p>I volunteer at my local junior AFL club and my role as club registrar grants access to a sports management platform called PlayHQ. It’s a rich source of data so for the end of the 2026 season, I thought it would be fun to use some of that data to ...</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/11/using-r-to-make-an-informational-video/">Using R to make an “informational video”</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://nsaunders.wordpress.com/2026/11/09/using-r-to-make-an-informational-video/"> R – What You&#039;re Doing Is Rather Desperate</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>

<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<div class="jetpack-video-wrapper"><iframe class="youtube-player" title="YouTube video player" width="450" src="https://www.youtube.com/embed/Q4rVdOJ904Y?version=3&#038;rel=1&#038;%23038;showsearch=0&#038;%23038;showinfo=1&#038;%23038;iv_load_policy=1&#038;%23038;fs=1&#038;%23038;hl=en&#038;%23038;autohide=2&#038;%23038;wmode=transparent" allowfullscreen="true" style="border:0;" sandbox="allow-scripts allow-same-origin allow-popups allow-presentation allow-popups-to-escape-sandbox"></iframe></div>
</div></figure>



<p class="wp-block-paragraph">I volunteer at my local junior AFL club and my role as club registrar grants access to a sports management platform called PlayHQ. It’s a rich source of data so for the end of the 2026 season, I thought it would be fun to use some of that data to create a season summary in an informational video format. You can see the result in the video, above.</p>



<span id="more-5750"></span>



<p class="wp-block-paragraph">This post is not a how-to, just a brief summary of the tools that I used.</p>



<p class="wp-block-paragraph">The input data: CSV files downloaded from the available reports in PlayHQ. These include data describing player registration, games played and venues.</p>



<p class="wp-block-paragraph">A lot of R users seem to be moving towards the Positron IDE and Quarto to generate presentation material. I’m still using RStudio and RMarkdown, simply because that’s what I know best right now. The key packages for this project: <em>ggplot2</em> of course; <em>waffle</em> to show player counts; <em>sf</em>, <em>leaflet</em>, <em>geosphere</em>, <em>tidyterra</em>, <em>osrm</em> and <em>maptiles</em> for geographical data; <em>wordcloud</em> for, well, word clouds.</p>



<p class="wp-block-paragraph">I used the RMarkdown file to sketch out each slide and generate one high-quality graphic per slide in PNG format, or in some cases as an animated GIF. Next, I used Keynote to create the final slides from the imported PNG files. I’ve had Keynote sitting largely unopened on every Mac that I’ve owned for years – turns out that it’s pretty good – not least because you can export the final product straight to video. I used a custom resolution of 1170×2532 to match my (iPhone 13) screen, which seems to work OK on most current phone screens.</p>



<p class="wp-block-paragraph">There’s plenty of free-to-use cheesy corporate music online to choose from, then it’s just a case of fiddling with the slide transitions to sync the music and any animations. Getting the timing right for slides that contain animated GIFs can be a bit tricky – quite honestly I don’t recall what worked best, other than clicking on the item, selecting the “Movie” tab in the right sidebar menu and “adjusting until it works”.</p>



<p class="wp-block-paragraph">Once the run-through in Keynote looks and sounds good, export to video and it’s ready to share.</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://nsaunders.wordpress.com/2026/11/09/using-r-to-make-an-informational-video/"> R – What You&#039;re Doing Is Rather Desperate</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/11/using-r-to-make-an-informational-video/">Using R to make an “informational video”</a>]]></content:encoded>
					
		
		<enclosure url="https://0.gravatar.com/avatar/0ace56430001dc0f2b92cfca798b8b6469cf7fc2c2878945baf9672a3d46e3f6?s=96&#038;d=https://0.gravatar.com/avatar/ad516503a11cd5ca435acc9bb6523536?s=96" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404748</post-id>	</item>
		<item>
		<title>Using caretS3 with ahead::ctxthetaf for Time Series Forecasting</title>
		<link>https://www.r-bloggers.com/2026/11/using-carets3-with-aheadctxthetaf-for-time-series-forecasting/</link>
		
		<dc:creator><![CDATA[T. Moudiki]]></dc:creator>
		<pubDate>Sat, 07 Nov 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://thierrymoudiki.github.io//blog/2026/11/07/r/ctxthetaf-caretS3</guid>

					<description><![CDATA[<p>A demonstration of using caretS3 models for time series forecasting with ahead::ctxthetaf.</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/11/using-carets3-with-aheadctxthetaf-for-time-series-forecasting/">Using caretS3 with ahead::ctxthetaf for Time Series Forecasting</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/11/07/r/ctxthetaf-caretS3"> 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><a href="https://techtonique.r-universe.dev/ahead/doc/manual.html#ctxthetaf" rel="nofollow" target="_blank"><code>ahead::ctxthetaf</code></a> has no more contextual information about the time series (<a href="https://techtonique.r-universe.dev/ahead/doc/manual.html#contextridge2f" rel="nofollow" target="_blank">ahead::contextridge2f</a> still does), but it still works, and can still be used notably with <code>caretS3</code> (a <code>caret</code> derivative introduced in <a href="https://thierrymoudiki.github.io/blog/2026/10/25/r/caretS3-ahead" rel="nofollow" target="_blank">https://thierrymoudiki.github.io/blog/2026/10/25/r/caretS3-ahead</a>) to forecast time series data. The following code demonstrates how to use <code>caretS3</code> models for time series forecasting with <code>ahead::ctxthetaf</code>.</p>

<pre>list_models &lt;- caret::modelLookup()[caret::modelLookup()[,&quot;forReg&quot;]==TRUE, 1]

n_models &lt;- length(list_models)

pb &lt;- utils::txtProgressBar(min = 0, max = n_models, style = 3)

for (i in seq_len(n_models))
{
  ts_model &lt;- try(ahead::ctxthetaf(y=fdeaths, 
                                   theta = 1,
                                   h=25L, 
                                   fit_func = function (x, y) caretS3::caretS3(x, y, 
                                                                               method=list_models[i])), 
                  silent = TRUE)
  if (class(ts_model) != &quot;try-error&quot;)
  {
    plot(ts_model) 
  } 
  utils::setTxtProgressBar(pb, i)
}

close(pb)
</pre>


<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://thierrymoudiki.github.io//blog/2026/11/07/r/ctxthetaf-caretS3"> 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/11/using-carets3-with-aheadctxthetaf-for-time-series-forecasting/">Using caretS3 with ahead::ctxthetaf for Time Series Forecasting</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404732</post-id>	</item>
		<item>
		<title>Free AI and Data Courses with 365 Data Science: Unlimited Access until Nov. 20</title>
		<link>https://www.r-bloggers.com/2026/11/free-ai-and-data-courses-with-365-data-science-unlimited-access-until-nov-20/</link>
		
		<dc:creator><![CDATA[neddd]]></dc:creator>
		<pubDate>Fri, 06 Nov 2026 12:42:11 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://r-posts.com/?p=19631</guid>

					<description><![CDATA[<p>Free AI and Data Courses with 365 Data Science: Unlimited Access until Nov. 20 365 Data Science opens its doors this November 100% for free! From November 5 to November 20, 2026, enjoy unlimited access to the entire platform, including a wide range of R programming courses and projects. This offer features expert-led courses, practical … Continue reading ...</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/11/free-ai-and-data-courses-with-365-data-science-unlimited-access-until-nov-20/">Free AI and Data Courses with 365 Data Science: Unlimited Access until Nov. 20</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="http://r-posts.com/free-ai-and-data-courses-with-365-data-science-unlimited-access-until-nov-20/"> R-posts.com</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><strong>Free AI and Data Courses with 365 Data Science: Unlimited Access until Nov. 20</strong></p>
<p><u><a href="https://bit.ly/48m6TnO" rel="nofollow" target="_blank">365 Data Science opens its doors this November 100% for free!</a></u></p>
<p>From November 5 to November 20, 2026, enjoy unlimited access to the entire platform, including a wide range of <a href="https://bit.ly/48m6TnO" rel="nofollow" target="_blank">R programming</a> courses and projects.</p>
<p>This offer features expert-led courses, practical projects, and interactive exercises covering diverse topics in AI and data science. With flexible 24/7 access, learners can study at their own pace, making it an ideal opportunity to grow and advance in these fields.</p>
<p><img loading="lazy" fetchpriority="high" decoding="async" src="https://i2.wp.com/r-posts.com/wp-content/uploads/2026/11/R-posts-Picture-1-450x236.jpg?resize=450%2C236" alt="" width="450" height="236" class="alignnone size-medium wp-image-19629" srcset_temp="https://i2.wp.com/r-posts.com/wp-content/uploads/2026/11/R-posts-Picture-1-450x236.jpg?resize=450%2C236 450w, http://r-posts.com/wp-content/uploads/2026/11/R-posts-Picture-1.jpg 468w" sizes="(max-width: 450px) 100vw, 450px" data-recalc-dims="1" /></p>
<p> </p>
<p><strong>The Initiative’s Sixth and Biggest Year</strong></p>
<p>Now in its sixth year, 365 Data Science reaffirms its dedication to delivering accessible, high-quality education through its annual Free Access Initiative, originally launched in 2020 during the global pandemic. CEO Ned Krastev emphasizes the accelerating demand for AI-driven skills, stating, <em>“The AI and data landscape is evolving faster than ever, creating extraordinary opportunities for career growth for those ready to embrace new technologies.”</em></p>
<p>The initiative’s reach has grown remarkably: 2025 marked its most successful edition yet, with over 220,000 unique learners from 216 countries participating, accumulating 7.2 million minutes of learning and earning more than 36,000 certificates.</p>
<p>Krastev adds, <em>“Artificial intelligence is reshaping industries at an unprecedented pace. Understanding how AI systems function</em><em> and how to build, deploy, and integrate them</em><em> has become vital for anyone pursuing a data-driven career. At 365 Data Science, our mission is to close that gap by equipping learners not only with data literacy but also with hands-on expertise in AI engineering and intelligent agents: the skills that will define the next generation of tech professionals.”</em></p>
<p><img loading="lazy" decoding="async" src="https://i0.wp.com/r-posts.com/wp-content/uploads/2026/11/R-posts-Picture-2-450x225.png?resize=450%2C225" alt="" width="450" height="225" class="alignnone size-medium wp-image-19630" srcset_temp="https://i0.wp.com/r-posts.com/wp-content/uploads/2026/11/R-posts-Picture-2-450x225.png?resize=450%2C225 450w, http://r-posts.com/wp-content/uploads/2026/11/R-posts-Picture-2.png 624w" sizes="(max-width: 450px) 100vw, 450px" data-recalc-dims="1" /></p>
<p>365 Data Science empowers learners to move beyond traditional analytics and step confidently into the era of AI engineering and intelligent agents. The platform equips users with the skills to design, deploy, and collaborate with AI systems capable of reasoning, planning, and acting autonomously.</p>
<p><strong>What’s Included</strong></p>
<p>During this special period, learners <a href="https://bit.ly/48m6TnO" rel="nofollow" target="_blank">gain unlimited access to the entire 365 Data Science platform</a>, a comprehensive destination for mastering data and AI. The platform offers 134 expert-led courses, covering everything from fundamental data skills to advanced topics in machine learning, AI, and AI engineering.</p>
<p>Participants can apply their knowledge through real-world projects that mirror practical work scenarios, gaining hands-on experience in solving meaningful problems. Newly added interactive exercises and guided challenges deepen understanding and strengthen key concepts.</p>
<p>To support career growth, 365 Data Science also provides structured, career-focused learning paths guiding learners step by step from beginner to job-ready professional with a clear roadmap to success in today’s AI-driven world.</p>
<p><strong>Certifications that Open Doors</strong></p>
<p>In today’s fast-changing job market, recognized certifications are essential to stand out. Through this free access initiative, 365 Data Science offers learners the chance to earn industry-recognized certificates at no cost.</p>
<p>These credentials validate practical expertise in data analytics, AI, and machine learning, enhancing employability and building credibility with employers worldwide. By offering verifiable, career-boosting certifications, the initiative bridges the gap between learning and professional achievement by showcasing real-world competence.</p>
<p><strong>R Programming: A Core Focus</strong></p>
<p>365 Data Science acknowledges <strong>the central role of R</strong> in modern data analysis and visualization, reflected in its carefully curated course selection:</p>
<ul>
	<li><strong>Introduction to R Programming:</strong> A beginner-friendly course that builds a strong foundation in R syntax, data structures, and essential operations—perfect for kick-starting your R journey.</li>
	<li><strong>The Complete Data Visualization Course with Python, R, Tableau, and Excel:</strong> A comprehensive program that highlights R’s powerful visualization tools while exploring multiple platforms for data storytelling.</li>
	<li><strong>Data-Driven Business Growth:</strong> Learn how to apply R to real-world business cases, adopting a growth mindset to generate meaningful insights and drive measurable impact.</li>
</ul>
<p>Additionally, learners can explore a variety of complementary courses designed to deepen their analytical toolkit, covering Statistics, Probability, Statistical Tests in Sales and Marketing, Data Preprocessing, and more—helping you master both R and other key programming languages in data science.</p>
<p><strong>Don’t Miss this Opportunity</strong></p>
<p>Free access runs from November 5 to November 20, 2026.</p>
<p>In a world increasingly powered by data and artificial intelligence, staying ahead of the curve is essential. This three-week open-access period from 365 Data Science offers a rare opportunity to invest in your future: whether you’re just getting started, switching careers, or advancing your expertise in AI and data.</p>
<p>Don’t miss your chance to build in-demand skills, earn industry-recognized certificates, and take the next step toward a successful career in data science and AI engineering.</p>
<p><em>The future belongs to those who prepare for it today: </em><a href="https://bit.ly/48m6TnO" rel="nofollow" target="_blank"><em>start your journey for free with 365 Data Science.</em></a></p>
<p> </p><hr style="border-top: black solid 1px" /><a href="http://r-posts.com/free-ai-and-data-courses-with-365-data-science-unlimited-access-until-nov-20/" rel="nofollow" target="_blank">Free AI and Data Courses with 365 Data Science: Unlimited Access until Nov. 20</a> was first posted on November 6, 2026 at 12:42 pm.<br />
<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="http://r-posts.com/free-ai-and-data-courses-with-365-data-science-unlimited-access-until-nov-20/"> R-posts.com</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/11/free-ai-and-data-courses-with-365-data-science-unlimited-access-until-nov-20/">Free AI and Data Courses with 365 Data Science: Unlimited Access until Nov. 20</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404706</post-id>	</item>
		<item>
		<title>Nuclei, five years later: what a mean Dice hides</title>
		<link>https://www.r-bloggers.com/2026/11/nuclei-five-years-later-what-a-mean-dice-hides/</link>
		
		<dc:creator><![CDATA[Michał Maj]]></dc:creator>
		<pubDate>Fri, 06 Nov 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://datascienceguts.com/posts/2026-11-06-nuclei-mean-dice/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>In 2020 I trained a U-Net on the 2018 Data Science Bowl nuclei with the first version of this package, watched a Dice score climb during training, and finished the post with four predicted masks on the competition’s test images. Those images ha...</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/11/nuclei-five-years-later-what-a-mean-dice-hides/">Nuclei, five years later: what a mean Dice hides</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://datascienceguts.com/posts/2026-11-06-nuclei-mean-dice/"> Data Science Guts</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 2020 I trained a U-Net on the <a href="https://www.kaggle.com/c/data-science-bowl-2018" rel="nofollow" target="_blank">2018 Data Science Bowl</a> nuclei with the first version of this package, watched a Dice score climb during training, and finished the post with four predicted masks on the competition’s test images. Those images have no masks. The pictures looked good, and I had no way of knowing where the model was wrong &#8211; so I never asked.</p>
<p>This post asks. Nothing in it trains: the model is the published <code>dsbowl-unet</code> from the <a href="https://datascienceguts.com/posts/2026-11-03-rplatypus/" rel="nofollow" target="_blank">first post of the series</a>, scored on the images it was held out from. Scoring all of them took about twelve seconds on a processor.</p>
<section id="the-images-it-was-held-out-from" class="level2">
<h2 class="anchored" data-anchor-id="the-images-it-was-held-out-from">The images it was held out from</h2>
<p>The data is <a href="https://bbbc.broadinstitute.org/BBBC038" rel="nofollow" target="_blank">BBBC038v1</a> from the Broad Institute &#8211; the Data Science Bowl’s images under a CC0 waiver. Its <code>stage1_train</code> holds 670 annotated images, one directory each, with one mask file per nucleus.</p>
<div class="tabset-margin-container"></div><div class="panel-tabset" data-group="language">
<ul class="nav nav-tabs"><li class="nav-item"><a class="nav-link active" id="tabset-1-1-tab" data-bs-toggle="tab" data-bs- aria-controls="tabset-1-1" aria-selected="true" href="https://datascienceguts.com/posts/2026-11-06-nuclei-mean-dice/" rel="nofollow" target="_blank">R</a></li><li class="nav-item"><a class="nav-link" id="tabset-1-2-tab" data-bs-toggle="tab" data-bs- aria-controls="tabset-1-2" aria-selected="false" href="https://datascienceguts.com/posts/2026-11-06-nuclei-mean-dice/" rel="nofollow" target="_blank">Python</a></li></ul>
<div class="tab-content" data-group="language">
<div id="tabset-1-1" class="tab-pane active" aria-labelledby="tabset-1-1-tab">
<div class="cell">
<pre>data_dir &lt;- &quot;stage1_train&quot;   # BBBC038v1, unzipped</pre>
</div>
</div>
<div id="tabset-1-2" class="tab-pane" aria-labelledby="tabset-1-2-tab">
<pre>data_dir = &quot;stage1_train&quot;   # BBBC038v1, unzipped</pre>
</div>
</div>
</div>
<p>The weights were measured on an 80/20 split of all 670 at seed 1, so that split can be made again. <code>split_dataset()</code> copies nothing; it writes CSVs listing the files:</p>
<div class="tabset-margin-container"></div><div class="panel-tabset" data-group="language">
<ul class="nav nav-tabs"><li class="nav-item"><a class="nav-link active" id="tabset-2-1-tab" data-bs-toggle="tab" data-bs- aria-controls="tabset-2-1" aria-selected="true" href="https://datascienceguts.com/posts/2026-11-06-nuclei-mean-dice/" rel="nofollow" target="_blank">R</a></li><li class="nav-item"><a class="nav-link" id="tabset-2-2-tab" data-bs-toggle="tab" data-bs- aria-controls="tabset-2-2" aria-selected="false" href="https://datascienceguts.com/posts/2026-11-06-nuclei-mean-dice/" rel="nofollow" target="_blank">Python</a></li></ul>
<div class="tab-content" data-group="language">
<div id="tabset-2-1" class="tab-pane active" aria-labelledby="tabset-2-1-tab">
<div class="cell">
<pre>library(rplatypus)

split &lt;- split_dataset(data_dir, tempfile(&quot;splits&quot;),
                       fractions = c(0.8, 0.2), seed = 1)
split
#&gt; platypus split
#&gt;   train        536 samples
#&gt;   validation   134 samples
#&gt;   every sample its own group
#&gt;   seed 1 - the same inputs give this split again</pre>
</div>
</div>
<div id="tabset-2-2" class="tab-pane" aria-labelledby="tabset-2-2-tab">
<pre>import tempfile
import pyplatypus as pp

split = pp.split_dataset(data_dir, tempfile.mkdtemp(),
                         fractions=(0.8, 0.2), seed=1)
split[&quot;samples&quot;]
#&gt; {'train': 536, 'validation': 134, 'test': 0}</pre>
</div>
</div>
</div>
<p>Making the split again matters more than it looks. My own copy of this dataset has a directory called <code>stage1_validation</code> with exactly 134 images in it - the same count - and 101 of them are in the training part of the split the weights were measured on. Scored there, the model would be graded partly on images it learnt from, and the count would never tell you.</p>
</section>
<section id="one-number" class="level2">
<h2 class="anchored" data-anchor-id="one-number">One number</h2>
<div class="tabset-margin-container"></div><div class="panel-tabset" data-group="language">
<ul class="nav nav-tabs"><li class="nav-item"><a class="nav-link active" id="tabset-3-1-tab" data-bs-toggle="tab" data-bs- aria-controls="tabset-3-1" aria-selected="true" href="https://datascienceguts.com/posts/2026-11-06-nuclei-mean-dice/" rel="nofollow" target="_blank">R</a></li><li class="nav-item"><a class="nav-link" id="tabset-3-2-tab" data-bs-toggle="tab" data-bs- aria-controls="tabset-3-2" aria-selected="false" href="https://datascienceguts.com/posts/2026-11-06-nuclei-mean-dice/" rel="nofollow" target="_blank">Python</a></li></ul>
<div class="tab-content" data-group="language">
<div id="tabset-3-1" class="tab-pane active" aria-labelledby="tabset-3-1-tab">
<div class="cell">
<pre>spec &lt;- platypus_spec(
  data = segmentation_data(split, colormap = binary_colormap),
  models = list(
    u_net(&quot;nuclei&quot;, input_shape = c(256, 256), blocks = 4, filters = 16,
          weights = &quot;dsbowl-unet&quot;, fit = FALSE,
          metrics = list(metric_dice(include_background = FALSE)))
  )
)
fit &lt;- platypus_fit(spec, verbose = FALSE)

cases &lt;- evaluate_cases(fit)
summary(cases)
#&gt;   metric   n      mean         sd    median       min       max
#&gt; 1   dice 134 0.9205236 0.05322748 0.9313503 0.7239583 0.9860661
#&gt;                                                         worst_case
#&gt; 1 7f38885521586fc6011bef1314a9fb2aa1e4935bd581b2991e1d963395eab770</pre>
</div>
</div>
<div id="tabset-3-2" class="tab-pane" aria-labelledby="tabset-3-2-tab">
<pre>engine = pp.build_engine(pp.from_dict({
    &quot;task&quot;: &quot;semantic_segmentation&quot;,
    &quot;data&quot;: {
        &quot;train_path&quot;: str(split[&quot;paths&quot;][&quot;train&quot;]),
        &quot;validation_path&quot;: str(split[&quot;paths&quot;][&quot;validation&quot;]),
        &quot;colormap&quot;: [[0, 0, 0], [255, 255, 255]],
        &quot;mode&quot;: &quot;config_file&quot;,
    },
    &quot;models&quot;: [{
        &quot;name&quot;: &quot;nuclei&quot;,
        &quot;architecture&quot;: &quot;u_net&quot;,
        &quot;input_shape&quot;: [256, 256],
        &quot;blocks&quot;: 4,
        &quot;filters&quot;: 16,
        &quot;weights&quot;: &quot;dsbowl-unet&quot;,
        &quot;fit&quot;: False,
        &quot;metrics&quot;: [{&quot;name&quot;: &quot;dice&quot;, &quot;include_background&quot;: False}],
    }],
}))
engine.fit()

cases = engine.evaluate_cases(&quot;nuclei&quot;)
pp.summarise_cases(cases)
#&gt; [{'metric': 'dice', 'n': 134, 'mean': 0.9205236150257623,
#&gt;   'sd': 0.05322747958922138, 'median': 0.9313502907752991,
#&gt;   'min': 0.7239583134651184, 'max': 0.9860661029815674,
#&gt;   'worst_case':
#&gt;     '7f38885521586fc6011bef1314a9fb2aa1e4935bd581b2991e1d963395eab770'}]</pre>
</div>
</div>
</div>
<p>A mean Dice of 0.9205 and a worst image at 0.7240 - the model card’s numbers to four digits, which is the check that this is the card’s split. <code>include_background = FALSE</code> keeps the background out of the average; with it in, a model that found no nuclei at all would still score around half.</p>
<p>0.92 is a good number, and it is true. The question is what it is an average <em>of</em>.</p>
</section>
<section id="one-row-per-image" class="level2">
<h2 class="anchored" data-anchor-id="one-row-per-image">One row per image</h2>
<p><code>evaluate()</code> gives one row per model. <code>evaluate_cases()</code> gives one row per image, and that is the table worth reading:</p>
<div class="tabset-margin-container"></div><div class="panel-tabset" data-group="language">
<ul class="nav nav-tabs"><li class="nav-item"><a class="nav-link active" id="tabset-4-1-tab" data-bs-toggle="tab" data-bs- aria-controls="tabset-4-1" aria-selected="true" href="https://datascienceguts.com/posts/2026-11-06-nuclei-mean-dice/" rel="nofollow" target="_blank">R</a></li><li class="nav-item"><a class="nav-link" id="tabset-4-2-tab" data-bs-toggle="tab" data-bs- aria-controls="tabset-4-2" aria-selected="false" href="https://datascienceguts.com/posts/2026-11-06-nuclei-mean-dice/" rel="nofollow" target="_blank">Python</a></li></ul>
<div class="tab-content" data-group="language">
<div id="tabset-4-1" class="tab-pane active" aria-labelledby="tabset-4-1-tab">
<div class="cell">
<pre>head(cases[order(cases$dice), ], 5)
#&gt; platypus scores by case (5 rows)
#&gt;                                                              case      dice
#&gt;  7f38885521586fc6011bef1314a9fb2aa1e4935bd581b2991e1d963395eab770 0.7239583
#&gt;  29ea4f6eb4545f43868a9b40a60000426bf8dfd9d062546656a37bd2a2aaf9ec 0.7531800
#&gt;  8f94a80b95a881d0efdec36affc915dca9609f4cba8134c4a91b219d418778aa 0.7541562
#&gt;  a6515d73077866808ad4cb837ecdac33612527b8a1041e82135e40fce2bb9380 0.7588775
#&gt;  2a1a294e21d76efd0399e4eb321b45f44f7510911acd92c988480195c5b4c812 0.7702851</pre>
</div>
</div>
<div id="tabset-4-2" class="tab-pane" aria-labelledby="tabset-4-2-tab">
<pre>worst = sorted(cases, key=lambda row: row[&quot;dice&quot;])[:5]
[(row[&quot;case&quot;][:16], round(row[&quot;dice&quot;], 4)) for row in worst]
#&gt; [('7f38885521586fc6', 0.724), ('29ea4f6eb4545f43', 0.7532),
#&gt;  ('8f94a80b95a881d0', 0.7542), ('a6515d7307786680', 0.7589),
#&gt;  ('2a1a294e21d76efd', 0.7703)]</pre>
</div>
</div>
</div>
<p>Those names say nothing, so give each image a label. The Data Science Bowl mixed three kinds of microscopy, and two numbers tell them apart well enough: whether the picture has colour, and whether its background is light or dark.</p>
<div class="tabset-margin-container"></div><div class="panel-tabset" data-group="language">
<ul class="nav nav-tabs"><li class="nav-item"><a class="nav-link active" id="tabset-5-1-tab" data-bs-toggle="tab" data-bs- aria-controls="tabset-5-1" aria-selected="true" href="https://datascienceguts.com/posts/2026-11-06-nuclei-mean-dice/" rel="nofollow" target="_blank">R</a></li><li class="nav-item"><a class="nav-link" id="tabset-5-2-tab" data-bs-toggle="tab" data-bs- aria-controls="tabset-5-2" aria-selected="false" href="https://datascienceguts.com/posts/2026-11-06-nuclei-mean-dice/" rel="nofollow" target="_blank">Python</a></li></ul>
<div class="tab-content" data-group="language">
<div id="tabset-5-1" class="tab-pane active" aria-labelledby="tabset-5-1-tab">
<div class="cell">
<pre>image_kind &lt;- function(case) {
  path &lt;- list.files(file.path(data_dir, case, &quot;images&quot;), full.names = TRUE)
  pixels &lt;- png::readPNG(path[1])[, , 1:3]
  colour &lt;- mean(abs(pixels[, , 1] - pixels[, , 2])) +
    mean(abs(pixels[, , 2] - pixels[, , 3]))
  if (colour &gt; 0.02) return(&quot;stained&quot;)
  if (mean(pixels) &gt; 0.5) &quot;brightfield&quot; else &quot;fluorescence&quot;
}
cases$kind &lt;- vapply(cases$case, image_kind, character(1))

aggregate(dice ~ kind, cases, function(x) {
  round(c(n = length(x), mean = mean(x), min = min(x)), 3)
})
#&gt;           kind  dice.n dice.mean dice.min
#&gt; 1  brightfield   3.000     0.749    0.724
#&gt; 2 fluorescence 105.000     0.935    0.753
#&gt; 3      stained  26.000     0.883    0.805</pre>
</div>
</div>
<div id="tabset-5-2" class="tab-pane" aria-labelledby="tabset-5-2-tab">
<pre>from pathlib import Path
import numpy as np
from PIL import Image

def image_kind(case):
    path = next((Path(data_dir) / case / &quot;images&quot;).glob(&quot;*.png&quot;))
    pixels = np.asarray(Image.open(path).convert(&quot;RGB&quot;)) / 255
    colour = (np.abs(pixels[..., 0] - pixels[..., 1]).mean()
              + np.abs(pixels[..., 1] - pixels[..., 2]).mean())
    if colour &gt; 0.02:
        return &quot;stained&quot;
    return &quot;brightfield&quot; if pixels.mean() &gt; 0.5 else &quot;fluorescence&quot;

for row in cases:
    row[&quot;kind&quot;] = image_kind(row[&quot;case&quot;])

kinds = sorted({row[&quot;kind&quot;] for row in cases})
dice = {k: [r[&quot;dice&quot;] for r in cases if r[&quot;kind&quot;] == k] for k in kinds}
{k: (len(d), round(float(np.mean(d)), 3), round(min(d), 3))
 for k, d in dice.items()}
#&gt; {'brightfield': (3, 0.749, 0.724), 'fluorescence': (105, 0.935, 0.753),
#&gt;  'stained': (26, 0.883, 0.805)}</pre>
</div>
</div>
</div>
<div class="tabset-margin-container"></div><div class="panel-tabset" data-group="language">
<ul class="nav nav-tabs"><li class="nav-item"><a class="nav-link active" id="tabset-6-1-tab" data-bs-toggle="tab" data-bs- aria-controls="tabset-6-1" aria-selected="true" href="https://datascienceguts.com/posts/2026-11-06-nuclei-mean-dice/" rel="nofollow" target="_blank">R</a></li><li class="nav-item"><a class="nav-link" id="tabset-6-2-tab" data-bs-toggle="tab" data-bs- aria-controls="tabset-6-2" aria-selected="false" href="https://datascienceguts.com/posts/2026-11-06-nuclei-mean-dice/" rel="nofollow" target="_blank">Python</a></li></ul>
<div class="tab-content" data-group="language">
<div id="tabset-6-1" class="tab-pane active" aria-labelledby="tabset-6-1-tab">
<div class="cell">
<pre>library(ggplot2)
ggplot(cases, aes(dice, kind)) +
  geom_vline(xintercept = mean(cases$dice), linetype = &quot;dashed&quot;,
             colour = &quot;grey50&quot;) +
  geom_jitter(height = 0.15, width = 0, alpha = 0.7, size = 2) +
  labs(x = &quot;Dice, per image&quot;, y = NULL) +
  theme_minimal(base_size = 13)</pre>
</div>
</div>
<div id="tabset-6-2" class="tab-pane" aria-labelledby="tabset-6-2-tab">
<pre>import matplotlib.pyplot as plt

rng = np.random.default_rng(1)
fig, ax = plt.subplots(figsize=(7, 3.2))
ax.axvline(np.mean([r[&quot;dice&quot;] for r in cases]), linestyle=&quot;--&quot;, color=&quot;grey&quot;)
for i, k in enumerate(kinds):
    jitter = rng.uniform(-0.15, 0.15, len(dice[k]))
    ax.scatter(dice[k], i + jitter, alpha=0.7)
ax.set_yticks(range(len(kinds)), kinds)
ax.set_xlabel(&quot;Dice, per image&quot;)</pre>
</div>
</div>
</div>
<div class="cell">
<div class="cell-output-display">
<div id="fig-cases" class="quarto-float quarto-figure quarto-figure-center anchored">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-cases-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<img src="https://i2.wp.com/datascienceguts.com/posts/2026-11-06-nuclei-mean-dice/index_files/figure-html/fig-cases-1.png?w=450&#038;ssl=1" class="img-fluid figure-img"  data-recalc-dims="1">
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig" id="fig-cases-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure 1: Dice for each of the 134 held-out images. The dashed line is the mean.
</figcaption>
</figure>
</div>
</div>
</div>
<p>There are three brightfield images among the 134, and they score 0.72, 0.75 and 0.77 - three of the five worst. Fluorescence averages 0.93 over 105 images, and it is most of what the mean measures. Stained tissue sits between the two, at 0.88.</p>
<div class="tabset-margin-container"></div><div class="panel-tabset" data-group="language">
<ul class="nav nav-tabs"><li class="nav-item"><a class="nav-link active" id="tabset-7-1-tab" data-bs-toggle="tab" data-bs- aria-controls="tabset-7-1" aria-selected="true" href="https://datascienceguts.com/posts/2026-11-06-nuclei-mean-dice/" rel="nofollow" target="_blank">R</a></li><li class="nav-item"><a class="nav-link" id="tabset-7-2-tab" data-bs-toggle="tab" data-bs- aria-controls="tabset-7-2" aria-selected="false" href="https://datascienceguts.com/posts/2026-11-06-nuclei-mean-dice/" rel="nofollow" target="_blank">Python</a></li></ul>
<div class="tab-content" data-group="language">
<div id="tabset-7-1" class="tab-pane active" aria-labelledby="tabset-7-1-tab">
<div class="cell">
<pre>predictions &lt;- predict(fit, split = &quot;validation&quot;)

files_of &lt;- function(case, what) {
  list.files(file.path(data_dir, case, what), full.names = TRUE)
}
truth_of &lt;- function(case) {
  masks &lt;- lapply(files_of(case, &quot;masks&quot;), function(f) {
    read_masks(f, binary_colormap, size = c(256, 256))[1, , ]
  })
  unite_masks(masks)
}

middle &lt;- which.min(abs(cases$dice - median(cases$dice)))
shown &lt;- c(which(cases$kind == &quot;brightfield&quot;), middle)
images &lt;- read_images(
  vapply(cases$case[shown], function(k) files_of(k, &quot;images&quot;)[1], &quot;&quot;),
  size = c(256, 256)
)
truth &lt;- simplify2array(lapply(cases$case[shown], truth_of))

plot_masks(
  images,
  prediction = predictions[shown, , ],
  truth      = aperm(truth, c(3, 1, 2)),
  colormap   = binary_colormap,
  labels     = sprintf(&quot;%s, Dice %.2f&quot;, cases$kind[shown], cases$dice[shown])
)</pre>
</div>
</div>
<div id="tabset-7-2" class="tab-pane" aria-labelledby="tabset-7-2-tab">
<pre>predictions = engine.predict(&quot;nuclei&quot;, &quot;validation&quot;)
data = engine.dataset(engine.runs[&quot;nuclei&quot;].spec, &quot;validation&quot;)

middle = np.median([r[&quot;dice&quot;] for r in cases])
shown = [i for i, r in enumerate(cases) if r[&quot;kind&quot;] == &quot;brightfield&quot;]
shown.append(min(range(len(cases)),
                 key=lambda i: abs(cases[i][&quot;dice&quot;] - middle)))

pp.plot_masks(
    np.stack([data[i][0] for i in shown]),
    prediction=predictions[shown],
    truth=np.stack([data[i][1] for i in shown]),
    labels=[f&quot;{cases[i]['kind']}, Dice {cases[i]['dice']:.2f}&quot;
            for i in shown],
)</pre>
</div>
</div>
</div>
<div class="cell">
<div class="cell-output-display">
<div id="fig-worst" class="quarto-float quarto-figure quarto-figure-center anchored">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-worst-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<img src="https://i2.wp.com/datascienceguts.com/posts/2026-11-06-nuclei-mean-dice/index_files/figure-html/fig-worst-1.png?w=450&#038;ssl=1" class="img-fluid figure-img"  data-recalc-dims="1">
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig" id="fig-worst-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure 2: The three brightfield images, and a fluorescence image from the middle of the ranking. In the last column green is found, red is missed and yellow is invented.
</figcaption>
</figure>
</div>
</div>
</div>
</section>
<section id="the-kind-of-microscope-or-the-size-of-a-nucleus" class="level2">
<h2 class="anchored" data-anchor-id="the-kind-of-microscope-or-the-size-of-a-nucleus">The kind of microscope, or the size of a nucleus?</h2>
<p>The pictures suggest a second reading. The brightfield images are 1024 by 1024 pixels, and the model sees every image at 256 by 256 - so a nucleus sixteen pixels across arrives as four. Measure the average nucleus on the model’s grid:</p>
<div class="tabset-margin-container"></div><div class="panel-tabset" data-group="language">
<ul class="nav nav-tabs"><li class="nav-item"><a class="nav-link active" id="tabset-8-1-tab" data-bs-toggle="tab" data-bs- aria-controls="tabset-8-1" aria-selected="true" href="https://datascienceguts.com/posts/2026-11-06-nuclei-mean-dice/" rel="nofollow" target="_blank">R</a></li><li class="nav-item"><a class="nav-link" id="tabset-8-2-tab" data-bs-toggle="tab" data-bs- aria-controls="tabset-8-2" aria-selected="false" href="https://datascienceguts.com/posts/2026-11-06-nuclei-mean-dice/" rel="nofollow" target="_blank">Python</a></li></ul>
<div class="tab-content" data-group="language">
<div id="tabset-8-1" class="tab-pane active" aria-labelledby="tabset-8-1-tab">
<div class="cell">
<pre>nucleus_area &lt;- function(case) {
  files &lt;- list.files(file.path(data_dir, case, &quot;masks&quot;), full.names = TRUE)
  areas &lt;- vapply(files, function(f) {
    sum(read_masks(f, binary_colormap) == 2)
  }, numeric(1))
  size &lt;- dim(read_masks(files[1], binary_colormap))[2:3]
  mean(areas) * 256^2 / prod(size)
}
cases$area &lt;- vapply(cases$case, nucleus_area, numeric(1))

aggregate(area ~ kind, cases, function(x) round(range(x)))
#&gt;           kind area.1 area.2
#&gt; 1  brightfield     11     17
#&gt; 2 fluorescence     69    957
#&gt; 3      stained    137   1893</pre>
</div>
</div>
<div id="tabset-8-2" class="tab-pane" aria-labelledby="tabset-8-2-tab">
<pre>def nucleus_area(case):
    files = sorted((Path(data_dir) / case / &quot;masks&quot;).glob(&quot;*.png&quot;))
    masks = [np.asarray(Image.open(f)) &gt; 0 for f in files]
    height, width = masks[0].shape[:2]
    return np.mean([m.sum() for m in masks]) * 256**2 / (height * width)

for row in cases:
    row[&quot;area&quot;] = nucleus_area(row[&quot;case&quot;])

areas = {k: [r[&quot;area&quot;] for r in cases if r[&quot;kind&quot;] == k] for k in kinds}
{k: (round(min(a)), round(max(a))) for k, a in areas.items()}
#&gt; {'brightfield': (11, 17), 'fluorescence': (69, 957),
#&gt;  'stained': (137, 1893)}</pre>
</div>
</div>
</div>
<p>On every other image an average nucleus covers at least 69 pixels of the grid. On the three brightfield images it covers eleven to seventeen. So the three images are both the only brightfield in the split and the only ones where the nuclei are this small, and <strong>three images cannot tell those two explanations apart</strong>. Training does not help either: of the 536 training images, 13 are brightfield, all of them 1024 by 1024 as well.</p>
<p>Scoring at the files’ own resolution does not separate them either, because the detail was lost before the score was taken - the prediction was made at 256:</p>
<div class="tabset-margin-container"></div><div class="panel-tabset" data-group="language">
<ul class="nav nav-tabs"><li class="nav-item"><a class="nav-link active" id="tabset-9-1-tab" data-bs-toggle="tab" data-bs- aria-controls="tabset-9-1" aria-selected="true" href="https://datascienceguts.com/posts/2026-11-06-nuclei-mean-dice/" rel="nofollow" target="_blank">R</a></li><li class="nav-item"><a class="nav-link" id="tabset-9-2-tab" data-bs-toggle="tab" data-bs- aria-controls="tabset-9-2" aria-selected="false" href="https://datascienceguts.com/posts/2026-11-06-nuclei-mean-dice/" rel="nofollow" target="_blank">Python</a></li></ul>
<div class="tab-content" data-group="language">
<div id="tabset-9-1" class="tab-pane active" aria-labelledby="tabset-9-1-tab">
<div class="cell">
<pre>summary(evaluate_cases(fit, at = &quot;source&quot;))
#&gt;   metric   n     mean         sd    median       min       max
#&gt; 1   dice 134 0.920808 0.05371718 0.9313503 0.7164674 0.9860661
#&gt;                                                         worst_case
#&gt; 1 7f38885521586fc6011bef1314a9fb2aa1e4935bd581b2991e1d963395eab770</pre>
</div>
</div>
<div id="tabset-9-2" class="tab-pane" aria-labelledby="tabset-9-2-tab">
<pre>pp.summarise_cases(engine.evaluate_cases(&quot;nuclei&quot;, at=&quot;source&quot;))
#&gt; [{'metric': 'dice', 'n': 134, 'mean': 0.920808045276955,
#&gt;   'sd': 0.053717183966230486, 'median': 0.9313502907752991,
#&gt;   'min': 0.7164673805236816, 'max': 0.9860661029815674,
#&gt;   'worst_case':
#&gt;     '7f38885521586fc6011bef1314a9fb2aa1e4935bd581b2991e1d963395eab770'}]</pre>
</div>
</div>
</div>
<p>What would separate them is a model that sees these images at a resolution where a nucleus is as large as it is everywhere else - tiling instead of resizing, which is a post of its own later in the series.</p>
</section>
<section id="what-to-take-from-this" class="level2">
<h2 class="anchored" data-anchor-id="what-to-take-from-this">What to take from this</h2>
<p>The mean was not wrong. 0.92 is what this model scores on these 134 images. But it is mostly an average over 105 fluorescence images, and the three images it does worst on share two properties at once, either of which could be the cause. None of that is visible in one number; all of it is visible in a table with one row per image and a column you add yourself.</p>
<p>If you report a segmentation score, report the worst case beside the mean, and look at what the worst cases have in common. Here it took one function call and a few lines of R or Python.</p>
</section>
<section id="the-series-so-far" class="level2">
<h2 class="anchored" data-anchor-id="the-series-so-far">The series so far</h2>
<ol type="1">
<li><a href="https://datascienceguts.com/posts/2026-11-03-rplatypus/" rel="nofollow" target="_blank">rplatypus rebuilt: deep learning for medical images in R</a> - why the 2020 package had to be rewritten, how it now installs its own Python, and a first mask from the same published weights used here.</li>
<li><strong>Nuclei, five years later</strong> - this post.</li>
</ol>
<p>The next one stays with segmentation but steps back from a single dataset: what rplatypus offers for it - four architectures and how they differ, the losses and metrics to choose from, and the YAML file that describes a whole experiment, field by field.</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://datascienceguts.com/posts/2026-11-06-nuclei-mean-dice/"> Data Science Guts</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/11/nuclei-five-years-later-what-a-mean-dice-hides/">Nuclei, five years later: what a mean Dice hides</a>]]></content:encoded>
					
		
		<enclosure url="https://datascienceguts.com/posts/2026-11-06-nuclei-mean-dice/thumbnail.png" length="0" type="image/png" />

		<post-id xmlns="com-wordpress:feed-additions:1">404718</post-id>	</item>
		<item>
		<title>Postal codes and tessellations</title>
		<link>https://www.r-bloggers.com/2026/11/postal-codes-and-tessellations/</link>
		
		<dc:creator><![CDATA[Diego HH]]></dc:creator>
		<pubDate>Thu, 05 Nov 2026 23:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://dieghernan.github.io/202611_postal-codes/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; "> From five imaginary points to a map of Slovenia: postal code areas with  Voronoi, giscoR and sf.</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/11/postal-codes-and-tessellations/">Postal codes and tessellations</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://dieghernan.github.io/202611_postal-codes/"> One World | R packages, maps and open-source projects - R Project</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>

		
		<h3>From postal points to areas using Voronoi polygons</h3>
		
  		
			<img src='https://dieghernan.github.io/assets/img/blog/2026-11-06-postal-codes-slovenia-postal-codes-1.webp' >
			
	<p>13 min.</p>
			<p>I recently published my project <a href="https://dieghernan.github.io/codigospostales/" rel="nofollow" target="_blank">Postal codes of
Spain</a>, with estimated postal
areas, an interactive map and GeoPackage downloads. But there is something we
have not looked at yet: how do those address points become polygons? The answer
is… <strong>Voronoi polygons</strong>.</p>

<h2 id="what-is-a-voronoi-polygon">What is a Voronoi polygon?</h2>

<p>Imagine a few shops on a flat map. Which one is closest to your house? Ask that
question for every location and give each shop its own color. The result is a
<strong>Voronoi diagram</strong>, also known as a <strong>Voronoi tessellation</strong>, <strong>Voronoi
decomposition</strong>, <strong>Voronoi partition</strong> or <strong>Dirichlet tessellation</strong>. This
technique divides a plane into regions based on the distance to a given set of
points.</p>

<p>In our example, each shop is a <strong>site</strong>, or generating point. Its <strong>Voronoi
cell</strong>, also called a <strong>Thiessen polygon</strong>, contains the locations at least as
close to that site as to any other. Along a shared edge, two sites are
equally close. Where several cells meet, there may be a tie between more sites.
With ordinary straight-line distance, the edges follow perpendicular bisectors
between sites. Some cells extend indefinitely, so maps usually show them clipped
to a study area.</p>

<p>The name honors Georgy Voronoi, who studied these partitions in the early
twentieth century. The idea has earlier roots, including Dirichlet’s
nineteenth-century work (hence the name Dirichlet tessellation). <a href="https://doi.org/10.2478/pcr-2018-0009" rel="nofollow" target="_blank">Pokojski and
Pokojska (2018)</a> review the history and
geographical applications.</p>

<p>These partitions are useful for exploring proximity to shops or public services,
assigning areas to weather stations and studying spatial patterns. For example,
Thiessen polygons can supply area weights when estimating rainfall from rain
gauges. The underlying question is always about the chosen distance: the nearest
hospital in a straight line may not be the quickest one to reach.</p>

<h3 id="a-tiny-example-in-r">A tiny example in <strong>R</strong></h3>

<p>Let’s start with five points in an imaginary square, including one at its
center, using just <strong>sf</strong> and <strong>ggplot2</strong>. The coordinates have arbitrary units
and no geographical coordinate reference system.</p>

<pre>library(sf)
library(ggplot2)

# Define a common theme for the maps.
background &lt;- &quot;#faf8f2&quot;
ink &lt;- &quot;#203b47&quot;

theme_set(
  theme_void() +
    theme(
      plot.background = element_rect(fill = background, colour = NA),
      panel.background = element_rect(fill = background, colour = NA),
      plot.title = element_text(face = &quot;bold&quot;, colour = ink),
      plot.subtitle = element_text(colour = ink),
      plot.caption = element_text(colour = &quot;grey40&quot;, hjust = 0),
      legend.position = &quot;bottom&quot;,
      plot.margin = margin(10, 10, 10, 10)
    )
)

sites &lt;- data.frame(
  site = c(&quot;A&quot;, &quot;B&quot;, &quot;C&quot;, &quot;D&quot;, &quot;E&quot;),
  x = c(2, 7, 3, 8, 5),
  y = c(2, 3, 8, 7, 5)
)

toy_points &lt;- st_as_sf(sites, coords = c(&quot;x&quot;, &quot;y&quot;))

study_area &lt;- st_as_sfc(st_bbox(c(xmin = 0, ymin = 0, xmax = 10, ymax = 10)))

toy_cells &lt;- toy_points |&gt;
  st_union() |&gt;
  st_voronoi(envelope = study_area) |&gt;
  st_collection_extract(&quot;POLYGON&quot;) |&gt;
  st_intersection(study_area)

ggplot() +
  geom_sf(
    data = toy_cells,
    fill = c(&quot;#a8dadc&quot;, &quot;#f4c89a&quot;, &quot;#c6b8df&quot;, &quot;#bfd59b&quot;, &quot;#f2b5c4&quot;),
    colour = &quot;white&quot;, linewidth = 1
  ) +
  geom_sf(data = toy_points, colour = &quot;#203b47&quot;, size = 3) +
  geom_text(
    data = sites, aes(x, y, label = site),
    nudge_y = 0.5, colour = &quot;#203b47&quot;, fontface = &quot;bold&quot;
  ) +
  coord_sf(datum = NA) +
  labs(
    title = &quot;Five points, five Voronoi cells&quot;,
    subtitle = &quot;Each location belongs to the closest site&quot;
  )
</pre>

<p><img src="https://dieghernan.github.io/assets/img/blog/2026-11-06-postal-codes-voronoi-toy-example-1.webp" alt="Five labeled sites in a square: A and B below, C and D above, and E at the
center, surrounded by its own bounded Voronoi cell.
" width="450" /></p>

<p>Five points, five cells. The point E at <code>(5, 5)</code> has a bounded cell
surrounded by the other four. The outer cells extend beyond the square and are
clipped to its edges. <code>st_union()</code> collects the points into one geometry,
<code>st_voronoi()</code> builds the diagram and <code>st_intersection()</code> clips it to our
square. The colors help us tell the cells apart. We have not assigned any postal
codes yet.</p>

<p>The diagram knows nothing about roads, rivers or administrative boundaries. As
we move to postal points, we will need to make our choices about those
boundaries explicit.</p>

<h2 id="before-we-start-what-are-we-estimating">Before we start: what are we estimating?</h2>

<p>The Spanish project uses CartoCiudad address points with their postal codes. It
follows <a href="https://doi.org/10.12842/WPIVIE_0322" rel="nofollow" target="_blank">Goerlich (2022)</a>, who develops an
idea presented by <a href="https://www.nosolosig.com/articulos/asi-hice-el-mapa-de-los-codigos-postales-de-espana-con-sig-y-datos-abiertos" rel="nofollow" target="_blank">Sánchez
(2020)</a>.
The <a href="https://www.uv.es/goerlich/Ivie/CodPost.html" rel="nofollow" target="_blank">updated online version</a>
describes the method in detail.</p>

<p>For Slovenia, we will use GISCO’s postal points, retrieved with <strong>giscoR</strong>.
There is an important difference here: a representative point for a code tells
us much less than thousands of addresses with that code. Detailed addresses help
the partition follow their distribution more closely, although proximity alone
still cannot reproduce postal delivery decisions.</p>

<p>We have not established that Slovenian postal areas should follow municipal
boundaries. Imposing those boundaries is a modeling choice for this exercise,
inspired by the Spanish workflow, rather than a verified property of the
Slovenian postal system.</p>

<p class="alert alert-warning p-3 mx-2 mb-3"><i class="fa fa-exclamation-triangle"></i> This post explains the workflow. The
polygons we create are not official postal boundaries, and this exercise does
not validate the GISCO postal data.</p>

<h2 id="getting-the-data">Getting the data</h2>

<p>First, let’s get the municipal boundaries and postal points. We use the 2024
release of both datasets.</p>

<pre>library(giscoR)
library(sf)
library(dplyr)

municipalities &lt;- gisco_get_lau(
  year = 2024, country = &quot;SI&quot;, epsg = 3035
) |&gt;
  select(GISCO_ID, LAU_NAME) |&gt;
  st_make_valid()

raw_points &lt;- gisco_get_postalcodes(
  year = 2024, country = &quot;SI&quot;, epsg = 3035
)
</pre>

<p>We work in <strong>ETRS89 / LAEA Europe (EPSG:3035)</strong>, a projected coordinate system
with coordinates in meters. This makes the 200 m and 50 m margins used later
meaningful. Voronoi uses Euclidean distances in this plane, rather than road
distances. The projection does not preserve every distance exactly either.</p>

<p>Before we start drawing, what have we actually downloaded?</p>

<pre>postal_points &lt;- raw_points |&gt;
  mutate(POSTCODE = trimws(as.character(POSTCODE))) |&gt;
  filter(!is.na(POSTCODE), POSTCODE != &quot;&quot;) |&gt;
  select(POSTCODE)

code_counts &lt;- postal_points |&gt;
  st_drop_geometry() |&gt;
  count(POSTCODE, name = &quot;points_per_code&quot;)

knitr::kable(data.frame(
  Indicator = c(
    &quot;Municipalities&quot;, &quot;Downloaded points&quot;, &quot;Points with a postal code&quot;,
    &quot;Distinct postal codes&quot;, &quot;Codes represented by one point&quot;
  ),
  Value = c(
    nrow(municipalities), nrow(raw_points), nrow(postal_points),
    nrow(code_counts), sum(code_counts$points_per_code == 1)
  )
))
</pre>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Indicator</th>
      <th style="text-align: right">Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">Municipalities</td>
      <td style="text-align: right">212</td>
    </tr>
    <tr>
      <td style="text-align: left">Downloaded points</td>
      <td style="text-align: right">557</td>
    </tr>
    <tr>
      <td style="text-align: left">Points with a postal code</td>
      <td style="text-align: right">557</td>
    </tr>
    <tr>
      <td style="text-align: left">Distinct postal codes</td>
      <td style="text-align: right">557</td>
    </tr>
    <tr>
      <td style="text-align: left">Codes represented by one point</td>
      <td style="text-align: right">557</td>
    </tr>
  </tbody>
</table>

<p>These counts give us a first look at the file, but they are not a quality check.
They do not establish whether every code is present or whether the coordinates
are correct. With one point per code, we will be drawing areas around
representatives rather than estimating them from individual addresses.</p>

<p>Two points at exactly the same coordinates cannot generate separate Voronoi
cells. We sort by postal code and keep the first point at each location, as in
the original workflow. This is a reproducible tie-breaking rule, not evidence
that the retained code is correct. The counts below show how many records we
remove and how many locations have conflicting codes.</p>

<pre>postal_points &lt;- postal_points |&gt; arrange(POSTCODE)

conflicts &lt;- postal_points |&gt;
  group_by(geometry) |&gt;
  summarise(codes = n_distinct(POSTCODE), .groups = &quot;drop&quot;) |&gt;
  filter(codes &gt; 1)

duplicates &lt;- duplicated(postal_points$geometry)

knitr::kable(data.frame(
  Indicator = c(&quot;Duplicate locations removed&quot;, &quot;Locations with conflicting codes&quot;),
  Value = c(sum(duplicates), nrow(conflicts))
))
</pre>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Indicator</th>
      <th style="text-align: right">Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">Duplicate locations removed</td>
      <td style="text-align: right">79</td>
    </tr>
    <tr>
      <td style="text-align: left">Locations with conflicting codes</td>
      <td style="text-align: right">16</td>
    </tr>
  </tbody>
</table>

<pre>
postal_points &lt;- postal_points[!duplicates, ]
</pre>

<h2 id="starting-with-ljubljana">Starting with Ljubljana</h2>

<p>Let’s pick Ljubljana and keep the points inside it. We use <code>st_within()</code> so that
a point exactly on a shared boundary is not assigned to both neighboring
municipalities.</p>

<pre>city &lt;- municipalities |&gt; filter(LAU_NAME == &quot;Ljubljana&quot;)

city_points &lt;- st_filter(postal_points, city, .predicate = st_within)

base_map &lt;- ggplot() +
  geom_sf(data = city, fill = &quot;#e8e4d8&quot;, colour = ink, linewidth = 0.4)

gisco_attr &lt;- gisco_attributions()

base_map +
  geom_sf(data = city_points, colour = &quot;#bd5137&quot;, size = 2) +
  labs(
    title = &quot;Ljubljana: the starting point&quot;,
    subtitle = &quot;A municipal boundary and points with postal codes&quot;,
    caption = gisco_attr
  )
</pre>

<p><img src="https://dieghernan.github.io/assets/img/blog/2026-11-06-postal-codes-ljubljana-1.webp" alt="Outline of Ljubljana municipality with six postal point locations marked
inside it.
" width="450" /></p>

<h3 id="building-the-cells">Building the cells</h3>

<p>Each cell contains locations at least as close to its generating point as to
any other point. Cell boundaries occur where distances to two points are equal.
For this first demonstration, we keep all interior points, without the margins used in
the national workflow.</p>

<pre>cells &lt;- city_points |&gt;
  st_union() |&gt;
  st_voronoi(envelope = st_as_sfc(st_bbox(city))) |&gt;
  st_collection_extract(&quot;POLYGON&quot;) |&gt;
  st_sf(geometry = _)

base_map +
  geom_sf(data = cells, fill = NA, colour = &quot;#bd5137&quot;, linewidth = 0.5) +
  geom_sf(data = city_points, colour = ink, size = 2) +
  labs(
    title = &quot;An area of proximity for every point&quot;,
    subtitle = &quot;Cells still extend beyond the municipal boundary&quot;,
    caption = &quot;Own elaboration using data from Eurostat / GISCO, 2024&quot;
  )
</pre>

<p><img src="https://dieghernan.github.io/assets/img/blog/2026-11-06-postal-codes-voronoi-1.webp" alt="Unclipped Voronoi cells outlined around six postal points, extending beyond
the shaded Ljubljana municipal boundary.
" width="450" /></p>

<p><code>st_union()</code> gathers the points so we build one diagram. One detail to keep in
mind: <code>envelope</code> supplies a working extent, but <strong>does not clip</strong> the cells to
the municipality. We need an intersection for that.</p>

<h3 id="clipping-and-recovering-postal-codes">Clipping and recovering postal codes</h3>

<p>It would be convenient if cells came out in the same order as the input points,
but we should not rely on that. Instead, we recover each cell’s code with a
spatial relationship before clipping. Each cell must contain exactly one
generating point.</p>

<pre>point_matches &lt;- st_intersects(cells, city_points)

cells$POSTCODE &lt;- city_points$POSTCODE[unlist(point_matches)]

clipped_geometry &lt;- st_intersection(st_geometry(cells), st_geometry(city))

city_cells &lt;- st_sf(
  POSTCODE = cells$POSTCODE[attr(clipped_geometry, &quot;idx&quot;)[, 1]],
  geometry = clipped_geometry
)

city_areas &lt;- city_cells |&gt;
  group_by(POSTCODE) |&gt;
  summarise(.groups = &quot;drop&quot;) |&gt;
  st_make_valid()

city_colours &lt;- setNames(
  hcl.colors(nrow(city_areas), palette = &quot;Dark 3&quot;),
  sort(city_areas$POSTCODE)
)
city_labels &lt;- st_sf(
  POSTCODE = city_areas$POSTCODE,
  geometry = st_point_on_surface(st_geometry(city_areas))
)

ggplot(city_areas) +
  geom_sf(aes(fill = POSTCODE), colour = background, linewidth = 0.6) +
  geom_sf(
    data = city_points, shape = 21, fill = ink,
    colour = background, size = 2.4
  ) +
  geom_sf_text(
    data = city_labels,
    aes(label = POSTCODE), colour = ink, fontface = &quot;bold&quot;, size = 3.5
  ) +
  scale_fill_manual(values = city_colours, guide = &quot;none&quot;) +
  coord_sf(datum = NA) +
  labs(
    title = &quot;From points to estimated areas&quot;,
    subtitle = &quot;Cells clipped and merged by postal code&quot;,
    caption = &quot;Teaching example · Not official postal boundaries&quot;
  )
</pre>

<p><img src="https://dieghernan.github.io/assets/img/blog/2026-11-06-postal-codes-clip-cells-1.webp" alt="Six colored areas clipped to Ljubljana, labeled 1000, 1210, 1211, 1231, 1260
and 1261, with their generating postal points marked.
" width="450" /></p>

<p>We intersect the geometry columns, then use the intersection’s <code>idx</code> matrix to
recover each cell’s postal code. This keeps the attribute assignment explicit.
Label positions are also calculated from geometries alone.</p>

<p>Grouping by postal code with <code>summarise()</code> merges the geometries. With many
addresses sharing a code, their cells would dissolve into one geometry, possibly
with several separate parts.</p>

<h2 id="from-one-municipality-to-the-whole-country">From one municipality to the whole country</h2>

<p>Ljubljana is done. Now let’s repeat the operation for each component of each
municipality. Municipalities can have separate pieces: normalizing to
<code>MULTIPOLYGON</code> and then casting to <code>POLYGON</code> lets us handle them independently,
without automatically extending a code to a piece with no points.</p>

<p>The municipality ID and name apply to every component, so we mark those
attributes as spatially constant before splitting the geometries.</p>

<pre>components &lt;- municipalities |&gt;
  st_set_agr(&quot;constant&quot;) |&gt;
  st_cast(&quot;MULTIPOLYGON&quot;) |&gt;
  st_cast(&quot;POLYGON&quot;) |&gt;
  mutate(component_id = row_number())
</pre>

<h3 id="why-use-an-inward-margin">Why use an inward margin?</h3>

<p>In the Spanish project, we first select addresses within an inward margin of
<strong>200 m</strong>. If none are found, we try <strong>50 m</strong>. This reduces the influence of
points close to a boundary, where small mismatches between layers may affect
their assignment.</p>

<p>There is no universal rule behind these numbers. Goerlich describes a 20 m
margin. The 200 m and 50 m choices come from my implementation. We keep them
here to explain that workflow, although sparse postal points make it easier to
exclude useful information. Comparing other margins would be a useful next step.</p>

<pre>select_points &lt;- function(component, postal_points) {
  for (margin in c(200, 50)) {
    interior &lt;- st_buffer(st_geometry(component), -margin)

    if (all(st_is_empty(interior))) next

    selected_points &lt;- st_filter(postal_points, interior,
      .predicate = st_within
    )

    if (nrow(selected_points) &gt; 0) {
      return(selected_points)
    }
  }

  postal_points[0, ]
}
</pre>

<p>The margin only selects points: the resulting areas cover the <strong>original</strong>
component, not the smaller buffered geometry. If both attempts fail, we leave
the component unassigned. We do not borrow the nearest point from outside the
municipality.</p>

<h3 id="three-cases-one-function">Three cases, one function</h3>

<p>There are three cases to handle. With no points, we keep the component and
assign <code>NA</code>. If all selected points share a code, we assign it to the whole
component. We only need Voronoi when several codes are present: no need to split
an area that already has a single assignment.</p>

<pre>build_areas &lt;- function(component, postal_points) {
  selected_points &lt;- select_points(component, postal_points)

  codes &lt;- unique(selected_points$POSTCODE)

  if (length(codes) &lt;= 1L) {
    component$POSTCODE &lt;- if (length(codes)) codes else NA_character_
    return(component)
  }

  cells &lt;- selected_points |&gt;
    st_union() |&gt;
    st_voronoi(envelope = st_as_sfc(st_bbox(component))) |&gt;
    st_collection_extract(&quot;POLYGON&quot;) |&gt;
    st_sf(geometry = _)

  point_matches &lt;- st_intersects(cells, selected_points)

  cells$POSTCODE &lt;- selected_points$POSTCODE[unlist(point_matches)]

  clipped_geometry &lt;- st_intersection(
    st_geometry(cells),
    st_geometry(component)
  )

  result &lt;- st_sf(
    POSTCODE = cells$POSTCODE[attr(clipped_geometry, &quot;idx&quot;)[, 1]],
    geometry = clipped_geometry
  ) |&gt;
    group_by(POSTCODE) |&gt;
    summarise(.groups = &quot;drop&quot;) |&gt;
    st_make_valid()

  result$GISCO_ID &lt;- component$GISCO_ID

  result$LAU_NAME &lt;- component$LAU_NAME

  result$component_id &lt;- component$component_id

  result
}

result &lt;- lapply(seq_len(nrow(components)), function(i) {
  build_areas(components[i, ], postal_points)
}) |&gt;
  bind_rows()
</pre>

<h3 id="handling-unassigned-areas">Handling unassigned areas</h3>

<p>Can we fill any of the gaps? A component without points may belong to a
municipality whose other components have information. We recover its code only
if the municipality has <strong>one distinct recovered code</strong>. This is an imputation
from the available data, not proof that the municipality really has just one
code.</p>

<pre>unique_codes &lt;- result |&gt;
  st_drop_geometry() |&gt;
  filter(!is.na(POSTCODE)) |&gt;
  distinct(GISCO_ID, POSTCODE) |&gt;
  add_count(GISCO_ID) |&gt;
  filter(n == 1)

unassigned_before &lt;- sum(is.na(result$POSTCODE))

index &lt;- match(result$GISCO_ID, unique_codes$GISCO_ID)

recoverable &lt;- is.na(result$POSTCODE) &#038; !is.na(index)

result$POSTCODE[recoverable] &lt;- unique_codes$POSTCODE[index[recoverable]]
</pre>

<p>Before drawing the final map, let’s tidy up the geometries and keep only
surfaces. Repairs can produce collections containing residual lines or points,
which do not belong in an area layer. Dissolving by postal code also removes
municipal boundaries between areas sharing that code.</p>

<pre>keep_surfaces &lt;- function(x) {
  geometry &lt;- st_make_valid(st_geometry(x))

  geometry &lt;- lapply(geometry, function(part) {
    if (!inherits(part, c(&quot;POLYGON&quot;, &quot;MULTIPOLYGON&quot;, &quot;GEOMETRYCOLLECTION&quot;))) {
      return(st_multipolygon())
    }

    surfaces &lt;- suppressWarnings(st_collection_extract(st_sfc(part), &quot;POLYGON&quot;))

    if (length(surfaces) == 0L || all(st_is_empty(surfaces))) {
      return(st_multipolygon())
    }

    st_cast(st_union(surfaces), &quot;MULTIPOLYGON&quot;, warn = FALSE)[[1]]
  })

  st_geometry(x) &lt;- st_sfc(geometry, crs = st_crs(x))

  x[!st_is_empty(x), ]
}

unassigned &lt;- result |&gt;
  filter(is.na(POSTCODE)) |&gt;
  keep_surfaces()

postal_areas &lt;- result |&gt;
  filter(!is.na(POSTCODE)) |&gt;
  group_by(POSTCODE) |&gt;
  summarise(.groups = &quot;drop&quot;) |&gt;
  keep_surfaces() |&gt;
  st_cast(&quot;MULTIPOLYGON&quot;)
</pre>

<h2 id="the-complete-map">The complete map</h2>

<p>For the national overview, let’s color the areas by the first digit of their
code. This keeps the map readable, without claiming that we have validated a
classification of postal regions. Gray is reserved for unassigned components,
with their own legend entry.</p>

<pre>postal_colours &lt;- c(
  &quot;1&quot; = &quot;#007F7F&quot;, &quot;2&quot; = &quot;#E87500&quot;, &quot;3&quot; = &quot;#3B6FB6&quot;, &quot;4&quot; = &quot;#A64E9A&quot;,
  &quot;5&quot; = &quot;#649A32&quot;, &quot;6&quot; = &quot;#D9AF00&quot;, &quot;8&quot; = &quot;#B94350&quot;, &quot;9&quot; = &quot;#6255A4&quot;,
  &quot;Unassigned&quot; = &quot;#727A80&quot;
)

postal_groups &lt;- sort(unique(substr(postal_areas$POSTCODE, 1, 1)))

ggplot() +
  geom_sf(data = municipalities, fill = &quot;#d5d2ca&quot;, colour = NA) +
  geom_sf(
    data = postal_areas,
    aes(fill = substr(POSTCODE, 1, 1)), colour = background, linewidth = 0.15
  ) +
  geom_sf(
    data = unassigned, aes(fill = &quot;Unassigned&quot;),
    colour = background, linewidth = 0.15
  ) +
  scale_fill_manual(
    values = postal_colours, name = &quot;Postal code group&quot;,
    breaks = c(postal_groups, &quot;Unassigned&quot;),
    labels = c(paste0(postal_groups, &quot;xxx&quot;), &quot;Unassigned&quot;)
  ) +
  coord_sf(datum = NA) +
  labs(
    title = &quot;Slovenia, point by point&quot;,
    subtitle = &quot;Proximity areas clipped to municipal boundaries&quot;,
    caption = paste(&quot;Eurostat / GISCO, 2024 · Own elaboration&quot;,
      &quot;Dark grey: unassigned areas · Estimated boundaries, not official&quot;,
      sep = &quot;\n&quot;
    )
  )
</pre>

<p><img src="https://dieghernan.github.io/assets/img/blog/2026-11-06-postal-codes-slovenia-postal-codes-1.webp" alt="Map of Slovenia showing estimated postal areas for the 20 municipalities processed in this draft. Colors identify postal prefixes, dark gray marks unassigned components and light gray shows the remaining municipal background." width="450" /></p>

<h2 id="checking-the-result">Checking the result</h2>

<p>The map is ready, but let’s check the geometry before going further. Are the
polygons valid? Do assigned and unassigned areas cover the territory? Are there
any appreciable overlaps? We allow one square meter for numerical differences in
the geometry operations.</p>

<pre>territory &lt;- st_union(st_geometry(municipalities))

coverage &lt;- st_union(c(st_geometry(postal_areas), st_geometry(unassigned)))

difference_m2 &lt;- as.numeric(sum(st_area(st_sym_difference(territory, coverage))))

overlap_m2 &lt;- as.numeric(
  sum(st_area(postal_areas)) + sum(st_area(unassigned)) - sum(st_area(coverage))
)

knitr::kable(data.frame(
  Indicator = c(
    &quot;Codes with an estimated area&quot;, &quot;Components assigned by municipal recovery&quot;,
    &quot;Unassigned records&quot;, &quot;Unassigned area (km²)&quot;,
    &quot;Coverage difference (m²)&quot;, &quot;Overlap (m²)&quot;
  ),
  Value = round(c(
    nrow(postal_areas), sum(recoverable), nrow(unassigned),
    as.numeric(sum(st_area(unassigned))) / 1e6, difference_m2,
    overlap_m2
  ), 4)
))
</pre>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Indicator</th>
      <th style="text-align: right">Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">Codes with an estimated area</td>
      <td style="text-align: right">468.0000</td>
    </tr>
    <tr>
      <td style="text-align: left">Components assigned by municipal recovery</td>
      <td style="text-align: right">4.0000</td>
    </tr>
    <tr>
      <td style="text-align: left">Unassigned records</td>
      <td style="text-align: right">6.0000</td>
    </tr>
    <tr>
      <td style="text-align: left">Unassigned area (km²)</td>
      <td style="text-align: right">150.2033</td>
    </tr>
    <tr>
      <td style="text-align: left">Coverage difference (m²)</td>
      <td style="text-align: right">0.0000</td>
    </tr>
    <tr>
      <td style="text-align: left">Overlap (m²)</td>
      <td style="text-align: right">-0.0002</td>
    </tr>
  </tbody>
</table>

<p>These checks verify the geometry, <strong>not postal accuracy</strong>. We could cover an
entire country with valid polygons and still have a poor approximation of its
delivery areas.</p>

<h2 id="back-to-spain">Back to Spain</h2>

<p>We now have the full sequence: points, proximity areas, clipping and merging by
postal code. The same steps work with much more detailed addresses, such as the
CartoCiudad data in my <a href="https://dieghernan.github.io/projects/postal-codes-spain" rel="nofollow" target="_blank">Spanish postal codes
project</a>.</p>

<p>The input data matters as much as the algorithm. With one representative point
per code, Voronoi mainly maps distances between representatives. With address
points, it can better approximate where each code’s addresses are concentrated.
It still knows nothing about delivery routes, physical barriers or official
boundaries.</p>

<p>We have not compared these results with an independent postal source for
Slovenia. Before using them for analysis, we would need to review coverage,
coordinates, the relationship with municipalities and sensitivity to the
margins. For now, we have a working example that shows how the polygons are
built, one step at a time.</p>

<h2 id="references">References</h2>

<ul>
  <li>Pokojski, W. & Pokojska, P. (2018). Voronoi diagrams – inventor, method,
applications. <em>Polish Cartographical Review</em>, <em>50</em>(3), 141–150.
<a href="https://doi.org/10.2478/pcr-2018-0009" rel="nofollow" target="_blank">https://doi.org/10.2478/pcr-2018-0009</a>.</li>
  <li>Goerlich, F. J. (2022). Elaboración de un mapa de códigos postales de España
con recursos libres. Cómo evitar pagar por disponer de información de
referencia. <em>Working Papers Ivie</em>, no. 2022-3. València: Ivie.
<a href="https://doi.org/10.12842/WPIVIE_0322" rel="nofollow" target="_blank">https://doi.org/10.12842/WPIVIE_0322</a>.
Updated online version: <a href="https://www.uv.es/goerlich/Ivie/CodPost.html" rel="nofollow" target="_blank">https://www.uv.es/goerlich/Ivie/CodPost.html</a>.</li>
  <li>Sánchez, J. I. (2020, November 13). “Así hice el mapa de los códigos
postales de España con SIG y datos abiertos.” NoSoloSIG.
<a href="https://www.nosolosig.com/articulos/asi-hice-el-mapa-de-los-codigos-postales-de-espana-con-sig-y-datos-abiertos" rel="nofollow" target="_blank">https://www.nosolosig.com/articulos/asi-hice-el-mapa-de-los-codigos-postales-de-espana-con-sig-y-datos-abiertos</a>.</li>
  <li>Hernangómez, D. (2026). Códigos postales de España [Dataset].
<a href="https://doi.org/10.5281/zenodo.23698418" rel="nofollow" target="_blank">https://doi.org/10.5281/zenodo.23698418</a>.
<a href="https://dieghernan.github.io/codigospostales/" rel="nofollow" target="_blank">https://dieghernan.github.io/codigospostales/</a>.</li>
</ul>

		
<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://dieghernan.github.io/202611_postal-codes/"> One World | R packages, maps and open-source projects - R Project</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/11/postal-codes-and-tessellations/">Postal codes and tessellations</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404700</post-id>	</item>
		<item>
		<title>Environmental modelling in your web browser</title>
		<link>https://www.r-bloggers.com/2026/11/environmental-modelling-in-your-web-browser/</link>
		
		<dc:creator><![CDATA[Seascapemodels]]></dc:creator>
		<pubDate>Thu, 05 Nov 2026 13:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://www.seascapemodels.org/posts/2026-11-06-environmental-modelling-in-your-web-browser/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>I recall when the R shiny framework first came out. It suddenly got relatively easy for us ecological modellers and environmental statisticians to turn our R code into web apps. This improved accessibility of complex analyses to more general aud...</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/11/environmental-modelling-in-your-web-browser/">Environmental modelling in your web 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://www.seascapemodels.org/posts/2026-11-06-environmental-modelling-in-your-web-browser/"> Seascapemodels</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
 





<p>I recall when the R shiny framework first came out. It suddenly got relatively easy for us ecological modellers and environmental statisticians to turn our R code into web apps. This improved accessibility of complex analyses to more general audiences.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://i0.wp.com/www.seascapemodels.org/posts/2026-11-06-environmental-modelling-in-your-web-browser/FANGS.png?w=578&#038;ssl=1" class="img-fluid figure-img" data-recalc-dims="1"></p>
<figcaption>FANGS, my browser based Bayesian modelling software</figcaption>
</figure>
</div>
<p>You started seeing ‘web apps’ popping up as outcomes of all sorts of projects. (Here’s one great example, <a href="https://shiny.csiro.au/BioOceanObserver/" rel="nofollow" target="_blank">Biological Ocean Observer</a> let’s you analyse timeseries of plankton data from Australia).</p>
<p>The barrier to doing these apps before was that most of the modelling community didn’t know how to code in the languages of the web. An R based framework for web apps made that possible.</p>
<p>But there was still a downside. With R shiny you still need to host the R code on a server somewhere. This requires either a free, but limited account, or paying for compute.</p>
<p>The ideal situation is to have your app written in Java Script and html. These languages run web pages. The key difference from R Shiny is the computations run in the web browser, on the viewer’s computer. This means you can host a web page cheaply or for free (e.g. as a github page). No server for computing R required.</p>
<p>Back in 2018, we paid a JS programmer to turn my R code into a web app for one of our projects (<a href="https://mangrove-carbon.wetlands.app/" rel="nofollow" target="_blank">on projecting carbon emissions from mangrove deforestation</a>). This app become very useful for people assessing potential outcomes of carbon projects.</p>
<p>Well AI coding agents have removed that barrier coding in JS.</p>
<p>I don’t think the huge potential of this change is widely appreciated in the environmental data analysis and modelling community yet. So I wanted to call it out here.</p>
<p>The tech community itself is only just starting to appreciate the impacts of this change. <a href="https://ftrain.com/aboard-the-web-is-more-than-enough" rel="nofollow" target="_blank">Here’s a nice explanation</a> of how this new power of running apps in the browser is transformative for the tech industry. (Check out <a href="https://tinylittleoffice.com/" rel="nofollow" target="_blank">tinylittleoffice</a>, a browser based office suite that saves the document as a URL you can copy and paste, or <a href="https://simonw.github.io/battledrones/" rel="nofollow" target="_blank">Battle Drones</a> a multiplayer web game that connects two devices through sound and microphones!).</p>
<p>This change is also going to be transformative for modelling and communication of science.</p>
<p>I got inspired by examples like those in the post above, and set-off on my own AI coding experiment. The result is <a href="https://www.seascapemodels.org/FANGS/" rel="nofollow" target="_blank">FANGS</a>, a browser based Bayesian computation engine.</p>
<p>Brief background on my motivation here. I’m a huge fan of Bayesian statistics. More people should be using it. But a barrier to sharing that enthusiasm is the complexity of installing many Bayesian R packages on other people’s computers.</p>
<p>So I built (vibe coded) FANGS as an educational tool. It means my students can write and run simple Bayesian models, directly in their browsers. This means that in a 3 hour lesson I can: cover an intro to Bayesian concepts, have students write models in R like syntax, run the models and interpret the results.</p>
<p>The idea is that is enough to get them motivated to then install the proper R software. The use of model syntax that emulates the serious Bayesian R packages is also a stepping stone to the more powerful R software.</p>
<p>I’ll write up the development pipeline in a later post. But I just want to say it blew me away how well this experiment with vibe coding worked. FANGS gets you the right answers and runs faster on small teaching scale problems than the equivalent R packages.</p>
<p>I think this new power will revolutionize scientific communication and education, through improving accessibility of many types of data and science.</p>
<p>What does this new wave of scientific app development what do you need?</p>
<p>On a personal level, curiosity about what is possible in web browsers. Once you know something is possible its easy to get an AI agent to make it. Once I learned about saving data in URLs from the tinylittleoffice example, it was easy to ask Claude to implement that feature in FANGS (see the <code>copy share link</code> button).</p>
<p>On a community level, translating more of the most common R packages to Java Script libraries will help. I actually haven’t fully explored what is available in JS yet. But for example, efficient JS packages for mapping exist, extending those for complex GIS operations will extend the potential of our apps.</p>
<p>Standards for validation. When you vibe code a package, you need to know it is accurate. I’ll come to how to do this when I write up the FANGS development process.</p>
<p>Finally, a bottleneck with apps has always been user experience (UX). For any scientific application a key question for me has always been: how do we make sure users don’t misinterpret the information this app provides? I’m not sure AI can solve that problem yet. It requires thoughtful design and lots of testing.</p>
<p>Let me know cool ideas you have or have seen, I’m keen to see more examples.</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.seascapemodels.org/posts/2026-11-06-environmental-modelling-in-your-web-browser/"> Seascapemodels</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/11/environmental-modelling-in-your-web-browser/">Environmental modelling in your web browser</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404702</post-id>	</item>
		<item>
		<title>Shrink stored models with butcher</title>
		<link>https://www.r-bloggers.com/2026/11/shrink-stored-models-with-butcher/</link>
		
		<dc:creator><![CDATA[Marc Becker]]></dc:creator>
		<pubDate>Thu, 05 Nov 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://mlr-org.com/gallery/technical/2026-11-05-butcher/</guid>

					<description><![CDATA[<p>Scope<br />
Fitted model objects in R often carry more than they need for prediction. A glm object keeps the training data, the fitted values, the residuals, and the environment of its formula. That is fine for a single model. It adds up when resample...</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/11/shrink-stored-models-with-butcher/">Shrink stored models with butcher</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://mlr-org.com/gallery/technical/2026-11-05-butcher/"> mlr-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>
 




<section id="scope" class="level1">
<h1>Scope</h1>
<p>Fitted model objects in R often carry more than they need for prediction. A <code>glm</code> object keeps the training data, the fitted values, the residuals, and the environment of its formula. That is fine for a single model. It adds up when <code>resample()</code> or <code>tune()</code> keep one model per resampling iteration with <code>store_models = TRUE</code>.</p>
<p>The <a href="https://cran.r-project.org/package=butcher" rel="nofollow" target="_blank">butcher</a> package from the tidymodels ecosystem removes these components while keeping the model usable for prediction. This post shows how to apply it to a single learner and how to wrap it in a callback so that every model stored by <a href="https://mlr3.mlr-org.com/" rel="nofollow" target="_blank">mlr3</a> and <a href="https://mlr3tuning.mlr-org.com/" rel="nofollow" target="_blank">mlr3tuning</a> gets shrunk right after training.</p>
<div class="cell" data-layout-align="center">
<pre>library(mlr3)
library(mlr3learners)
library(mlr3tuning)
library(mlr3misc)
library(butcher)</pre>
</div>
<p>We measure the size of an object by serializing it. This is the number of bytes the object needs on disk or when it is sent to a parallel worker.</p>
<div class="cell" data-layout-align="center">
<pre>serialized_size = function(x) {
  round(length(serialize(x, connection = NULL)) / 1024^2, digits = 2L)
}</pre>
</div>
</section>
<section id="butcher-a-single-model" class="level1">
<h1>Butcher a single model</h1>
<p>We train a logistic regression on the spam task with 4601 emails and 57 features.</p>
<div class="cell" data-layout-align="center">
<pre>task = tsk(&quot;spam&quot;)
learner = lrn(&quot;classif.log_reg&quot;, predict_type = &quot;prob&quot;)
learner$train(task)

serialized_size(learner$model)</pre>
<div class="cell-output cell-output-stdout">
<pre>[1] 4.44</pre>
</div>
</div>
<p>The model needs about 4.4 MB. The function <code>weigh()</code> lists the components of the model object sorted by size in MB.</p>
<div class="cell" data-layout-align="center">
<pre>weigh(learner$model)</pre>
<div class="cell-output cell-output-stdout">
<pre># A tibble: 101 × 2
   object              size
   &lt;chr&gt;              &lt;dbl&gt;
 1 qr.qr             2.16  
 2 residuals         0.333 
 3 fitted.values     0.333 
 4 linear.predictors 0.333 
 5 weights           0.333 
 6 prior.weights     0.333 
 7 y                 0.333 
 8 effects           0.0773
 9 family.variance   0.0440
10 family.dev.resids 0.0440
# &#x2139; 91 more rows</pre>
</div>
</div>
<p>The QR decomposition is the largest component. The residuals, fitted values, linear predictors, weights, and the response each take another 0.33 MB. Prediction needs only a fraction of this.</p>
<p>We predict on the task, butcher the model, and write it back into the learner.</p>
<div class="cell" data-layout-align="center">
<pre>prediction = learner$predict(task)
learner$model = butcher(learner$model)

serialized_size(learner$model)</pre>
<div class="cell-output cell-output-stdout">
<pre>[1] 2.34</pre>
</div>
</div>
<p>The model shrank to roughly half its size. The predictions of the butchered model are identical to the original predictions.</p>
<div class="cell" data-layout-align="center">
<pre>all.equal(prediction, learner$predict(task))</pre>
<div class="cell-output cell-output-stdout">
<pre>[1] TRUE</pre>
</div>
</div>
<p>The function <code>butcher()</code> only removes components that the <code>predict()</code> method of the model class does not use. It keeps the QR decomposition because <code>predict.glm()</code> needs it to compute standard errors.</p>
<div class="callout callout-style-default callout-warning callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Warning
</div>
</div>
<div class="callout-body-container callout-body">
<p>A butchered model has lost information. Functions such as <code>summary()</code>, <code>residuals()</code>, or <code>fitted()</code> may fail or return wrong results on it. Only butcher models that you need for prediction.</p>
</div>
</div>
</section>
<section id="resample-callback" class="level1">
<h1>Resample callback</h1>
<p>We now want to shrink every model that <code>resample()</code> stores. Callbacks run code at specific stages of the resampling loop. We create a resample callback with <a href="https://mlr3.mlr-org.com/reference/callback_resample.html" rel="nofollow" target="_blank"><code>mlr3::callback_resample()</code></a>. The stage <code>on_resample_end</code> runs after the learner has been trained and has made its predictions. The context object gives access to the learner of the current iteration.</p>
<div class="cell" data-layout-align="center">
<pre>callback = callback_resample(&quot;butcher&quot;,
  on_resample_end = function(callback, context) {
    context$learner$model = butcher::butcher(context$learner$model)
  }
)</pre>
</div>
<p>We call <code>butcher()</code> with the package prefix because the callback may run on a parallel worker on which <code>butcher</code> is not attached.</p>
<p>We run a 5-fold cross-validation twice on the same splits, once without and once with the callback.</p>
<div class="cell" data-layout-align="center">
<pre>resampling = rsmp(&quot;cv&quot;, folds = 5L)
resampling$instantiate(task)

rr = resample(task, learner, resampling, store_models = TRUE)
rr_butchered = resample(task, learner, resampling, store_models = TRUE, callbacks = callback)</pre>
</div>
<p>We compare the size of the stored models.</p>
<div class="cell" data-layout-align="center">
<pre>serialized_size(map(rr$learners, &quot;model&quot;))</pre>
<div class="cell-output cell-output-stdout">
<pre>[1] 19.19</pre>
</div>
<pre>serialized_size(map(rr_butchered$learners, &quot;model&quot;))</pre>
<div class="cell-output cell-output-stdout">
<pre>[1] 10.4</pre>
</div>
</div>
<p>The five butchered models need about 9 MB less. The callback only touches the stored models, so the performance estimate does not change.</p>
<div class="cell" data-layout-align="center">
<pre>rr$aggregate(msr(&quot;classif.auc&quot;))</pre>
<div class="cell-output cell-output-stdout">
<pre>classif.auc 
   0.971403 </pre>
</div>
<pre>rr_butchered$aggregate(msr(&quot;classif.auc&quot;))</pre>
<div class="cell-output cell-output-stdout">
<pre>classif.auc 
   0.971403 </pre>
</div>
</div>
<p>The stored learners still predict new data.</p>
<div class="cell" data-layout-align="center">
<pre>rr_butchered$learners[[1L]]$predict(task)</pre>
<div class="cell-output cell-output-stdout">
<pre>
── &lt;PredictionClassif&gt; for 4601 observations: ──────────────────────────────────────────────────────────────────────────
 row_ids   truth response  prob.spam prob.nonspam
       1    spam     spam 0.62852865 3.714714e-01
       2    spam     spam 0.99139839 8.601607e-03
       3    spam     spam 0.99999999 5.408266e-09
     ---     ---      ---        ---          ---
    4599 nonspam  nonspam 0.03097766 9.690223e-01
    4600 nonspam  nonspam 0.07923412 9.207659e-01
    4601 nonspam  nonspam 0.02782719 9.721728e-01</pre>
</div>
</div>
<p><code>benchmark()</code> accepts the same <code>callbacks</code> argument.</p>
</section>
<section id="tuning" class="level1">
<h1>Tuning</h1>
<p>Tuning with <code>store_models = TRUE</code> stores one model per resampling iteration and hyperparameter configuration. Tuning instances take tuning callbacks, which can include the resample stages. We create one with <a href="https://mlr3tuning.mlr-org.com/reference/callback_batch_tuning.html" rel="nofollow" target="_blank"><code>mlr3tuning::callback_batch_tuning()</code></a>.</p>
<div class="cell" data-layout-align="center">
<pre>callback_tuning = callback_batch_tuning(&quot;butcher&quot;,
  on_resample_end = function(callback, context) {
    context$learner$model = butcher::butcher(context$learner$model)
  }
)</pre>
</div>
<p>We tune the complexity parameter of a decision tree with 20 random configurations and a 3-fold cross-validation.</p>
<div class="cell" data-layout-align="center">
<pre>learner_rpart = lrn(&quot;classif.rpart&quot;, cp = to_tune(1e-4, 1e-1, logscale = TRUE))

instance = tune(
  tuner = tnr(&quot;random_search&quot;),
  task = task,
  learner = learner_rpart,
  resampling = rsmp(&quot;cv&quot;, folds = 3L),
  term_evals = 20L,
  store_models = TRUE,
  callbacks = callback_tuning
)</pre>
</div>
<p>The archive contains a benchmark result with the 60 butchered models.</p>
<div class="cell" data-layout-align="center">
<pre>models = map(instance$archive$benchmark_result$resample_results$resample_result, function(rr) {
  map(rr$learners, &quot;model&quot;)
})

serialized_size(models)</pre>
<div class="cell-output cell-output-stdout">
<pre>[1] 2.68</pre>
</div>
</div>
</section>
<section id="conclusion" class="level1">
<h1>Conclusion</h1>
<p>The <code>butcher</code> package removes data, calls, and environments that a model does not need for prediction. Wrapped in a callback, it shrinks every model that <code>resample()</code>, <code>benchmark()</code>, or <code>tune()</code> stores. The stored results take less memory and are faster to save to disk and to transfer from parallel workers. How much it saves depends on the model class. For model classes without a <code>butcher</code> method, <code>butcher()</code> returns the model unchanged. You can check which classes are supported in the <a href="https://butcher.tidymodels.org/articles/available-axe-methods.html" rel="nofollow" target="_blank">butcher documentation</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://mlr-org.com/gallery/technical/2026-11-05-butcher/"> mlr-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/11/shrink-stored-models-with-butcher/">Shrink stored models with butcher</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404697</post-id>	</item>
		<item>
		<title>SAS to R: Why R, and Where Your Migration Budget Should Go</title>
		<link>https://www.r-bloggers.com/2026/11/sas-to-r-why-r-and-where-your-migration-budget-should-go/</link>
		
		<dc:creator><![CDATA[The Jumping Rivers Blog]]></dc:creator>
		<pubDate>Wed, 04 Nov 2026 23:59:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://www.jumpingrivers.com/blog/sas-to-r-migration-risks/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>This post is part of our SAS to R series:</p>
<p>Part 1: Is SAS still used, and is it worth keeping?<br />
Part 2: Why R, and where your migration budget should go (this post)<br />
Part 3: coming soon</p>
<p>If you’re moving regulated analysis from SAS to R, two ...</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/11/sas-to-r-why-r-and-where-your-migration-budget-should-go/">SAS to R: Why R, and Where Your Migration Budget Should Go</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/sas-to-r-migration-risks/"> 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/sas-to-r-migration-risks/">
<img src="https://www.jumpingrivers.com/blog/sas-to-r-migration-risks/" width="400" style="width:400px" class="image-center" style="display: block; margin: auto;" />
</a>
</p>
<p>This post is part of our SAS to R series:</p>
<ul>
<li>Part 1: <a href="https://www.jumpingrivers.com/blog/is-sas-worth-keeping/" rel="nofollow" target="_blank">Is SAS still used, and is it worth keeping?</a></li>
<li>Part 2: Why R, and where your migration budget should go (this post)</li>
<li>Part 3: coming soon</li>
</ul>
<p>If you’re moving regulated analysis from SAS to R, two things are worth knowing up front. R is a choice you can defend: regulators don’t tell you which software to use, and R has already been through FDA review in public pilot submissions. And the language itself is the cheap part. Most of the cost and risk comes from six other places, and most of those are about planning, not programming.</p>
<p><a href="https://www.jumpingrivers.com/blog/is-sas-worth-keeping/" rel="nofollow" target="_blank">Part one</a> asked whether SAS is still worth keeping. This post covers why R, and where to put your budget and attention once you commit.</p>
<h2 id="why-r-is-a-defensible-choice">Why R is a defensible choice</h2>
<h3 id="regulators-ask-for-evidence-not-a-vendor">Regulators ask for evidence, not a vendor</h3>
<p>The FDA’s <a href="https://www.fda.gov/media/109552/download" rel="nofollow" target="_blank">Statistical Software Clarifying Statement</a> says the agency does not require any specific software for statistical analysis. It asks for the software to be documented in the submission, including version and build, and <a href="https://www.ema.europa.eu/en/ich-e9-statistical-principles-clinical-trials-scientific-guideline" rel="nofollow" target="_blank">ICH E9</a>, the international guideline on statistics in clinical trials, asks for documented testing.</p>
<p>The business implication is that the obligation stays the same whichever language you choose. What changes is who does the work. With SAS, much of the evidence arrives with the licence. With R, your organisation produces it.</p>
<h3 id="there-is-public-precedent-to-show-your-risk-committee">There is public precedent to show your risk committee</h3>
<p>The R Consortium’s <a href="https://rconsortium.github.io/submissions-wg/" rel="nofollow" target="_blank">R Submissions Working Group</a> has taken R-based tables, figures, standard analysis datasets and an interactive app through FDA review in a series of public pilots, with the code and lessons published. That is not a guarantee for every submission, but it is the most useful thing you can put in front of a risk committee.</p>
<p><img alt="Timeline of the R Consortium pilot submissions to the FDA, with each pilot’s hex sticker. Pilot 1, tables and figures: submitted November 2021, FDA response March 2022. Pilot 2, Shiny app: submitted November 2022, FDA response September 2023. Pilot 3, analysis datasets: submitted August 2023, FDA response August 2024. Pilot 4a, WebAssembly: submitted September 2024, FDA response May 2026. Pilot 4b, containers: submitted June 2025, FDA response May 2026. Pilot 5, Dataset-JSON: submitted in 2025, resubmitted January 2026." id="h-rh-i-0" src="https://www.jumpingrivers.com/blog/sas-to-r-migration-risks/fda-pilot-timeline.svg"></p>
<p><em>Hex stickers and pilot details: <a href="https://rconsortium.github.io/submissions-wg/" rel="nofollow" target="_blank">R Consortium R Submissions Working Group</a>.</em></p>
<h3 id="the-clinical-tooling-already-exists">The clinical tooling already exists</h3>
<p>Most of what a clinical SAS team does has a maintained R equivalent, much of it built by pharma companies working together in the <a href="https://pharmaverse.org/" rel="nofollow" target="_blank">pharmaverse</a>. You are not funding the tooling, only the move.</p>
<p>One finding comes up repeatedly. A lot of “we have to keep SAS” turns out to mean “we have to produce SAS transport files”, the format regulators expect submitted data in. That format is a published specification, not a licence, and R writes it.</p>
<p><em>For your team:</em> <a href="https://cran.r-project.org/package=admiral" rel="nofollow" target="_blank"><code>admiral</code></a> and <a href="https://cran.r-project.org/package=sdtm.oak" rel="nofollow" target="_blank"><code>sdtm.oak</code></a> build ADaM and SDTM datasets, <a href="https://cran.r-project.org/package=mmrm" rel="nofollow" target="_blank"><code>mmrm</code></a> reproduces SAS mixed models, and <a href="https://cran.r-project.org/package=xportr" rel="nofollow" target="_blank"><code>xportr</code></a> and <a href="https://cran.r-project.org/package=haven" rel="nofollow" target="_blank"><code>haven</code></a> write and read v5 transport files.</p>
<aside class="advert">
<p>
Need help with R package validation to unleash the power of open source? Check out the <a href="https://www.jumpingrivers.com/litmus/?utm_source=blog&#038;utm_medium=banner&#038;utm_campaign=sas-to-r-migration-risks" rel="nofollow" target="_blank">Litmusverse suite of risk assessment tools</a>.
</p>
</aside>
<h3 id="what-about-python">What about Python?</h3>
<p>For statistical analysis and regulated reporting, R is usually the closer match to the work SAS did. Python tends to be the better answer for machine learning engineering and production services. Many organisations run both, on shared infrastructure and review processes, so choosing R for statistics does not lock in the rest of the stack.</p>
<h2 id="where-the-cost-and-risk-come-from">Where the cost and risk come from</h2>
<p>Teams often budget a SAS to R migration as a code rewrite. The rewrite is the smallest of the seven layers below. The six above it are where timelines slip and budgets get cut.</p>
<p><img alt="A stack of seven layers showing where risk comes from in a SAS to R migration. From the bottom: the R language, lowest risk; numerical defaults; data semantics; the macro layer; environment and reproducibility; validation evidence; and at the top, people and process, the highest risk." id="h-rh-i-1" src="https://www.jumpingrivers.com/blog/sas-to-r-migration-risks/risk-stack.svg"></p>
<h3 id="1-the-two-systems-will-disagree-and-both-will-be-right">1. The two systems will disagree, and both will be right</h3>
<p>SAS and R make different default choices for the same statistical method. For example, they handle tied survival times differently. Run the same analysis in both and the numbers can differ in the last decimal places, with neither tool wrong.</p>
<p>The cost comes when nobody notices until validation, and nobody recorded which choice the analysis plan meant. The fix is cheap if you do it early: add the method choices to each analysis specification before conversion starts. That turns an open-ended rewrite into a bounded validation exercise.</p>
<p><em>For your team:</em> the cross-industry <a href="https://psiaims.github.io/CAMIS/" rel="nofollow" target="_blank">CAMIS project</a> documents these differences method by method, including <a href="https://psiaims.github.io/CAMIS/Comp/r-sas_survival.html" rel="nofollow" target="_blank">survival analysis</a> in R’s <a href="https://cran.r-project.org/package=survival" rel="nofollow" target="_blank"><code>survival</code></a> package and <a href="https://psiaims.github.io/CAMIS/Comp/r-sas_rounding.html" rel="nofollow" target="_blank">rounding</a>.</p>
<h3 id="2-data-does-not-mean-quite-the-same-thing">2. Data does not mean quite the same thing</h3>
<p>The two languages treat missing values and dates differently. A filter carried over from SAS can quietly drop rows in R, changing the denominator. The result is a table that is nearly right, which is harder to catch than one that is obviously wrong. Budget for checking the data, not just converting the code.</p>
<p><em>For your team:</em> SAS sorts missing numerics below every value and has 28 kinds of missing, while R has one <code>NA</code>. Date origins also differ (1960 against 1970), which <code>haven</code> handles on import.</p>
<h3 id="3-your-macro-library-holds-rules-nobody-wrote-down">3. Your macro library holds rules nobody wrote down</h3>
<p>As we covered in <a href="https://www.jumpingrivers.com/blog/is-sas-worth-keeping/" rel="nofollow" target="_blank">part one</a>, a mature macro library holds business rules that exist nowhere else. A line-by-line conversion reproduces the code and loses the reasoning. Recovering that intent is analysis work, and it needs people who know the SAS estate. In mature estates it can be one of the longest parts of the migration, so plan for it explicitly.</p>
<h3 id="4-reproducibility-becomes-your-responsibility">4. Reproducibility becomes your responsibility</h3>
<p>SAS gives you one vendor managing one product. R spreads that across R itself and the add-on packages (community-built libraries of functions) your analyses depend on. Being able to rerun an analysis years later is now something your team owns. The tooling is mature, and it should be mandatory from day one rather than added later.</p>
<p><em>For your team:</em> pin dependencies with <code>renv</code>, serve packages from a dated snapshot with Posit Package Manager (which we wrote about alongside <a href="https://www.jumpingrivers.com/blog/diffify-posit-package-manager/" rel="nofollow" target="_blank">diffify</a>), and containerise anything long-lived. In the UK public sector, the same idea goes by the name <a href="https://www.jumpingrivers.com/blog/reproducible-analytical-pipelines/" rel="nofollow" target="_blank">reproducible analytical pipelines</a>.</p>
<h3 id="5-proving-it-works-is-a-separate-project">5. Proving it works is a separate project</h3>
<p>Getting the right answer and being able to prove you will keep getting it are different projects, and only the first one is fun. Budget them separately.</p>
<p>The <a href="https://pharmar.org/white-paper/" rel="nofollow" target="_blank">R Validation Hub</a> set out the widely used approach: assess each package by risk rather than validating everything to the same depth. That keeps the effort proportionate. We build on the same idea in <a href="https://www.jumpingrivers.com/litmus/" rel="nofollow" target="_blank">Litmus</a>, which scores packages at function level.</p>
<p><em>For your team:</em> how the scoring works for <a href="https://www.jumpingrivers.com/blog/litmus-scoring-r-validation/" rel="nofollow" target="_blank">validation</a> and <a href="https://www.jumpingrivers.com/blog/r-validation-code-quality-litmus/" rel="nofollow" target="_blank">code quality</a>, the shorter argument in <a href="https://www.jumpingrivers.com/blog/should-i-use-your-r-pkg/" rel="nofollow" target="_blank">Should I Use Your R Package?</a>, unit testing with <a href="https://www.jumpingrivers.com/blog/r-testthat/" rel="nofollow" target="_blank">testthat</a>, and <a href="https://www.jumpingrivers.com/blog/validating-shiny-apps-in-regulated-environments/" rel="nofollow" target="_blank">validating Shiny apps in regulated environments</a>.</p>
<h3 id="6-dual-running-is-the-cost-that-gets-cut">6. Dual running is the cost that gets cut</h3>
<p>For the whole parallel period you pay for both systems, both sets of people and both sets of sign-off. This is what de-risks the migration, and it is reliably the first thing trimmed when the budget tightens. Protect it.</p>
<p>Agree in advance how close the two sets of results need to be, and why. Then check automatically rather than by eye, so a difference shows up the day it appears.</p>
<p><em>For your team:</em> <code>diffdf</code> was built for this comparison, and <code>arsenal::comparedf</code> and <code>waldo</code> cover adjacent cases. Run them in CI.</p>
<h2 id="what-to-do-before-converting-any-code">What to do before converting any code</h2>
<ol>
<li>Inventory the macro library and the business rules inside it.</li>
<li>Add method choices to every analysis specification.</li>
<li>Decide your package validation approach.</li>
<li>Budget dual running as a protected line.</li>
</ol>
<p>Each of these is manageable once it has been named and written down. That is what an assessment produces before a single line of code is converted.</p>
<p>If you are planning a migration, <a href="https://www.jumpingrivers.com/contact/" rel="nofollow" target="_blank">get in touch</a>. If your team is earlier than that, our <a href="https://www.jumpingrivers.com/training/course/why-use-r/" rel="nofollow" target="_blank">Why Use R?</a> course is written for people arriving from SAS and SPSS.</p>
<h2 id="frequently-asked-questions">Frequently asked questions</h2>
<p><strong>Is R accepted by regulators?</strong></p>
<p>The FDA does not require any specific software, provided the software used is documented with version and build and its testing is documented. The R Consortium’s pilots have taken R-based tables, figures, standard analysis datasets and an interactive app through FDA review.</p>
<p><strong>Will R give exactly the same numbers as SAS?</strong></p>
<p>Not by default, and that is normal. The two make different default choices for some methods, so results can differ slightly while both are correct. The fix is to state those choices in the analysis specification rather than hunt for a bug.</p>
<h2 id="references">References</h2>
<ul>
<li>US FDA, <a href="https://www.fda.gov/media/109552/download" rel="nofollow" target="_blank">Statistical Software Clarifying Statement</a></li>
<li>US FDA, <a href="https://www.fda.gov/media/133219/download" rel="nofollow" target="_blank">Study Data Technical Conformance Guide</a></li>
<li>R Consortium, <a href="https://rconsortium.github.io/submissions-wg/" rel="nofollow" target="_blank">R Submissions Working Group</a> and <a href="https://r-consortium.org/posts/submissions-wg-2026/" rel="nofollow" target="_blank">2026 plans and 2025 progress</a></li>
<li>PHUSE, PSI, ASA and IASCT, <a href="https://psiaims.github.io/CAMIS/" rel="nofollow" target="_blank">CAMIS: Comparing Analysis Method Implementations in Software</a></li>
<li>CAMIS, <a href="https://psiaims.github.io/CAMIS/Comp/r-sas_survival.html" rel="nofollow" target="_blank">R versus SAS: Kaplan-Meier and Cox proportional hazards</a> and <a href="https://psiaims.github.io/CAMIS/Comp/r-sas_rounding.html" rel="nofollow" target="_blank">R versus SAS rounding</a></li>
<li>R Validation Hub, <a href="https://pharmar.org/white-paper/" rel="nofollow" target="_blank">A Risk-based Approach for Assessing R Package Accuracy within a Validated Infrastructure</a></li>
<li>The R Foundation, <a href="https://www.r-project.org/doc/R-FDA.pdf" rel="nofollow" target="_blank">R: Regulatory Compliance and Validation Issues</a></li>
<li>CRAN, <a href="https://cran.r-project.org/package=mmrm" rel="nofollow" target="_blank"><code>mmrm</code></a> for mixed models for repeated measures</li>
<li>CRAN, <a href="https://cran.r-project.org/package=admiral" rel="nofollow" target="_blank"><code>admiral</code></a> and <a href="https://cran.r-project.org/package=sdtm.oak" rel="nofollow" target="_blank"><code>sdtm.oak</code></a> for ADaM and SDTM builds</li>
<li>CRAN, <a href="https://cran.r-project.org/package=xportr" rel="nofollow" target="_blank"><code>xportr</code></a> for writing v5 transport files</li>
</ul>
<p>
For updates and revisions to this article, see the <a href = "https://www.jumpingrivers.com/blog/sas-to-r-migration-risks/">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/sas-to-r-migration-risks/"> 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/11/sas-to-r-why-r-and-where-your-migration-budget-should-go/">SAS to R: Why R, and Where Your Migration Budget Should Go</a>]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">404658</post-id>	</item>
		<item>
		<title>Kindness, Curiosity, and Kitties: A rOpenSci 15th Anniversary Interview with Mo</title>
		<link>https://www.r-bloggers.com/2026/11/kindness-curiosity-and-kitties-a-ropensci-15th-anniversary-interview-with-mo/</link>
		
		<dc:creator><![CDATA[rOpenSci]]></dc:creator>
		<pubDate>Wed, 04 Nov 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://ropensci.org/blog/2026/11/04/celebrating-ropensci/</guid>

					<description><![CDATA[<p>As part of rOpenSci’s 15th anniversary celebrations, Community Manager Yani sits down with community member Mo to talk about how a first software review turned into years of involvement across R-Universe, the Champions Program, the Slack communi...</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/11/kindness-curiosity-and-kitties-a-ropensci-15th-anniversary-interview-with-mo/">Kindness, Curiosity, and Kitties: A rOpenSci 15th Anniversary Interview with Mo</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/11/04/celebrating-ropensci/"> rOpenSci - open tools for open science</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>

<p>As part of <a href="https://ropensci.org/tags/anniversary" rel="nofollow" target="_blank">rOpenSci’s 15th anniversary</a> celebrations, Community Manager Yani sits down with community member Mo to talk about how a first software review turned into years of involvement across R-Universe, the Champions Program, the Slack community, and core package work. Mo reflects on what has kept her engaged and also offers advice for anyone just discovering this community.</p>
<ul>
<li><strong>How did you first end up in rOpenSci’s orbit, what was the very first interaction or moment that pulled you in?</strong></li>
</ul>
<p>I actually don’t recall. I think it was a natural evolution of my general career progression using R as a scientist and slowly transitioning into a research software engineer. Still working in academia, and mostly on tooling, analysis, and data management using R, rOpenSci felt like a very natural community to get involved in.</p>
<p>I believe I was first introduced to it through <a href="https://ropensci.org/author/ma%C3%ABlle-salmon/" rel="nofollow" target="_blank">Maëlle Salmon</a>, who was/is a fellow <a href="https://rladies.org/" rel="nofollow" target="_blank">RLadies+ global</a> team member. My <a href="https://github.com/ropensci/software-review/issues/522" rel="nofollow" target="_blank">first interaction may have been as a software reviewer for a package</a>, which Maëlle asked me if I was interested in doing.</p>
<ul>
<li><strong>Over the years, you’ve shown up in a lot of different ways: <a href="https://ropensci.org/blog/2023/03/30/r-universe-stars-3-en/" rel="nofollow" target="_blank">R-Universe</a>, the <a href="https://ropensci.org/champions/" rel="nofollow" target="_blank">Champions Program</a>, an active participant on the Slack, and a <a href="https://ropensci.org/blog/2026/06/01/goodpractice/" rel="nofollow" target="_blank">contributor to our core packages</a>. Looking back, how would you describe the arc of your involvement?</strong></li>
</ul>
<p>It’s been such an easy community to be involved in.
I’ve felt that every suggestion, discussion, and help request has been met with kindness, collegiality and with a true sense of people wanting to share their knowledge.
Seeing this [atmosphere] in the Slack has lowered any barrier I had for asking <em>“stupid”</em> questions, and always felt like any contribution has been genuinely valued.
I’ve also felt very humble in how some interactions have altered my own perception of myself as an Research Software Engineer (RSE).
I remember in particular when you asked me if <a href="https://ropensci.org/blog/2023/05/05/ropensci-champions-program-teams-meet-carolina-pradier-and-athanasia-monika-mowinckel/" rel="nofollow" target="_blank">I would consider being a Champion’s mentor</a>, and being so humbled. I had actually considered applying as a Champion, but didn’t have the time to set aside for it at that moment. Being asked to be a mentor instead made me shift my own understanding of my expertise level.</p>
<div class="row">
<div class="col-md-6">
<figure><img src="https://i0.wp.com/ropensci.org/blog/2026/11/04/celebrating-ropensci/positconf.jpeg?w=578&#038;ssl=1"
alt="Mo in a stage delivering a talk about R-Universe. In the screen a galaxy picture with the rOpenSci and R-Universe logo" data-recalc-dims="1"><figcaption>
<p>Mo delivering her talk about R-Universe at posit::conf</p>
</figcaption>
</figure>
</div>
<div class="col-md-6">
<figure><img src="https://i0.wp.com/ropensci.org/blog/2026/11/04/celebrating-ropensci/starofruniverse.png?w=578&#038;ssl=1"
alt="Interview with Mo about how she uses R-Universe" data-recalc-dims="1"><figcaption>
<p>Interview with Mo for the series Star of R-Universe</p>
</figcaption>
</figure>
</div>
</div>
<ul>
<li><strong>Of all the ways you’ve participated, is there one that feels like “the” rOpenSci experience for you? The one you’d point to first?</strong></li>
</ul>
<p>I actually think the Slack is the pinnacle of how it is to be part of the community.
Which is likely frustrating for anyone not in that Slack to read.
While it’s not the most active space, the activity is generally always useful and/or enjoying.
It’s like a friendly bubble of R-geeks just wanting to make cool things.</p>
<div class="col-md-6">
<figure><img src="https://i0.wp.com/ropensci.org/blog/2026/11/04/celebrating-ropensci/catslack.png?w=578&#038;ssl=1"
alt="Mo&#39;s cat pictured outside in a spot half sun/half shade with the text "Sølvi can&#39;t decide whether it&#39;s best here in the sun, or in the shade. She&#39;s swapped placed 4 times within the last 10minutes :heart_eyes_cat:&#39;"" data-recalc-dims="1"><figcaption>
<p>Mo’s message in the Slack channel <code>rcatpeople</code></p>
</figcaption>
</figure>
</div>
<ul>
<li><strong>Across all these different roles, has your sense of what rOpenSci <em>is</em>, or what it’s for, changed over time?</strong></li>
</ul>
<p>That’s a hard one.
I feel like it’s been quite stable over the years I have participated, but there have likely been subtle changes over time that are hard to put a pin in.</p>
<div class="row">
<div class="col-md-6">
<figure><img src="https://i1.wp.com/ropensci.org/blog/2026/11/04/celebrating-ropensci/authorprofile.png?w=578&#038;ssl=1"
alt="Mo&#39;s author profile on the rOpenSci website, showing a list of blog posts and interviews she hascontributed" data-recalc-dims="1"><figcaption>
<p>Mo’s author profile on the rOpenSci website</p>
</figcaption>
</figure>
</div>
<div class="col-md-6">
<figure><img src="https://i1.wp.com/ropensci.org/blog/2026/11/04/celebrating-ropensci/ggsegverse.png?w=578&#038;ssl=1"
alt="ggsegverse R-Universe, showing packages, contributors and Mo as primary author of the ggseg package" data-recalc-dims="1"><figcaption>
<p>ggsegverse R-Universe</p>
</figcaption>
</figure>
</div>
</div>
<ul>
<li><strong>Was there ever a moment you felt surprised by how much you’d ended up doing, or by where it led?</strong></li>
</ul>
<p>All the time.
It’s almost like having a second set of colleagues, that technically don’t work with me (as in at the same company), but are equally important to my work and its robustness.
Between the software review, blog posts, collaborative tutorials, Champion mentoring, rOpenSci package tooling development, discussions in Slack (let us not forget the awesome cat pictures in the cat channel), I feel like I have quite immersed myself in the community.
But I have gotten much more back that I have contributed, I believe.
Not only in the obvious ways visible, like mentor training, learning more about collaborative development etc., but also in getting feedback on my funding applications and on my personal development projects.
It’s all been invaluable for my own development.</p>
<ul>
<li><strong>How does participating in rOpenSci activities compare to other ways you’ve been involved in open source more broadly?</strong></li>
</ul>
<p>I’m not someone who has contributed a lot to open source in general.
I’m pretty much in my lane with rOpenSci, <a href="https://rladies.org/" rel="nofollow" target="_blank">RLadies+</a> and <a href="https://carpentries.org/" rel="nofollow" target="_blank">The Carpentries</a>, three organizations I feel all emulate pretty much the same core values in how to treat contributors.
This has given me the confidence to participate openly and without fear, and has also given me strength when I’ve received some less than courteous responses in other places.</p>
<p>But comparing it to certain Stack overflow and social media interactions I have had outside these communities, it’s for sure a much more wholesome and pleasant experience.
Not being afraid of being seen as incompetent for asking what someone else thinks is a basic question, is so important for how a community can keep growing and thriving.</p>
<ul>
<li><strong>What does rOpenSci mean to you at this point, fifteen years in?</strong></li>
</ul>
<p>A lot.
As I am sure you can tell from all that I write here.
rOpenSci is really my second professional “home”, where I know I can get support and feedback from those willing to take the time (and even if no one has the time, one generally always gets a positive nudge to continue).</p>
<ul>
<li><strong>What would you want someone just discovering rOpenSci today to know?</strong></li>
</ul>
<p>Don’t hesitate to contact and get involved.
Start wherever you feel you can; <a href="https://ropensci.org/software-review/" rel="nofollow" target="_blank">doing a software review as a reviewer is a nice experience</a> and a really good place to start.
You get to learn a little about reviewing others’ work, how to work with git and github, and communicate with other professionals.
It really is a very good starting point.</p>
<p><em>What a kind vision of rOpenSci! This post is part of our 15th anniversary celebration series, and we still have more to come!</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/11/04/celebrating-ropensci/"> rOpenSci - open tools for open science</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/11/kindness-curiosity-and-kitties-a-ropensci-15th-anniversary-interview-with-mo/">Kindness, Curiosity, and Kitties: A rOpenSci 15th Anniversary Interview with Mo</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404671</post-id>	</item>
		<item>
		<title>Roche and Appsilon Build on Their Collaboration to Support teal’s Continued Development</title>
		<link>https://www.r-bloggers.com/2026/11/roche-and-appsilon-build-on-their-collaboration-to-support-teals-continued-development/</link>
		
		<dc:creator><![CDATA[Dony Unardi]]></dc:creator>
		<pubDate>Wed, 04 Nov 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://pharmaverse.github.io/blog/posts/2026-11-04-roche-appsilon-teal/roche-appsilon-teal.html</guid>

					<description><![CDATA[<p>Following the recent migration of the teal framework to pharmaverse, Roche and Appsilon are building on their existing collaboration to support its continued development. Appsilon will continue contributing developer expertise and will be recogn...</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/11/roche-and-appsilon-build-on-their-collaboration-to-support-teals-continued-development/">Roche and Appsilon Build on Their Collaboration to Support teal’s Continued Development</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://pharmaverse.github.io/blog/posts/2026-11-04-roche-appsilon-teal/roche-appsilon-teal.html"> pharmaverse 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>Following the recent migration of the teal framework to pharmaverse, Roche and Appsilon are building on their existing collaboration to support its continued development. Appsilon will continue contributing developer expertise and will be recognized as a core contributor and copyright holder alongside Roche.</p>
<p>teal helps developers build Shiny applications for exploring clinical trial data. If you’ve worked with it, you’ll know its modular approach: combining data, filters, and analysis modules into interactive applications.</p>
<p>Appsilon has been working with Roche on {teal} development and has been a core contributor to the open-source library. For example, it has co-implemented the new teal design, teal.reporter, and teal.picks, changes that have improved the quality and capabilities of the framework. Formal recognition as a core contributor is a new step forward that formalizes Appsilon’s role in the project, alongside Roche and the wider community.</p>
<section id="what-changes-for-users-and-contributors" class="level2">
<h2 class="anchored" data-anchor-id="what-changes-for-users-and-contributors">What Changes for Users and Contributors?</h2>
<p>The teal framework packages are now hosted in the pharmaverse GitHub organization, as described in the earlier migration announcement. Users of existing released versions do not need to change their R code as a result of the move. For development versions installed directly from GitHub, we recommend updating package references to their new pharmaverse locations.</p>
<p>Appsilon will continue co-leading the maintenance and development of the teal framework alongside Roche, with input and contributions from the wider community. This continued collaboration supports the framework’s ongoing development while welcoming other companies and individual contributors to help shape its direction.</p>
<p>Users and contributors can continue reporting issues, proposing features, and submitting pull requests in the package repositories. Community feedback and contributions remain an important part of teal’s development.</p>
</section>
<section id="whats-next-for-teal" class="level2">
<h2 class="anchored" data-anchor-id="whats-next-for-teal">What’s Next for teal?</h2>
<p>Future development will focus on making teal easier to contribute to and strengthening its use in validated environments. Work includes expanding guidance for AI-assisted development across the core and module packages, alongside improvements to testing and dependency management. Roche and Appsilon will continue working with the wider community to shape priorities and guide the framework’s development.</p>
</section>
<section id="building-on-the-communitys-work" class="level2">
<h2 class="anchored" data-anchor-id="building-on-the-communitys-work">Building on the Community’s Work</h2>
<p>teal was created at Roche and has grown through the work of developers and users contributing code, modules, documentation, bug reports, and feedback. Appsilon has been a vital part of the teal community and is taking on a greater role in its continued development alongside Roche and other contributors.</p>
<p>Thank you to each author of teal: Dony Unardi, Dawid Kałędkowski, Pawel Rucki, Aleksander Chlebowski, Andre Verissimo, Kartikeya Kirar, Vedha Viyash, Marcin Kosinski, Adrian Waddell, Nikolas Burkoff, Mahmoud Hallal, Maciej Nasinski, Konrad Pagacz, Junlue Zhao, Tadeusz Lewandowski, Lluis Revilla and everyone else who has helped develop and maintain the project. We look forward to continuing to work with you and welcoming new contributors.</p>
<blockquote class="blockquote">
<p>It’s been rewarding to see teal grow and support colleagues across the biotech and pharmaceutical community. Moving to pharmaverse opens a new chapter in that journey, and I’m excited to build on our longstanding collaboration with Appsilon through a shared commitment to open-source development. Together with the wider community, we look forward to shaping teal’s future and expanding what it can help users achieve.</p>
<p>— Dony Unardi, Roche (teal Product Owner and NEST Chief Engineer)</p>
</blockquote>
<blockquote class="blockquote">
<p>We are excited to welcome Appsilon as a partner for teal. Open source ecosystems like pharmaverse thrive when dedicated communities and forward-thinking organizations step up together as one &#8211; enabling powerful frameworks like teal to flourish and drive real impact. We hope this move to the pharmaverse org also encourages further contribution from across the industry, so this continues to prosper.</p>
<p>— Ross Farrugia, Roche (pharmaverse co-founder)</p>
</blockquote>
<blockquote class="blockquote">
<p>teal is a great example of the unique value of open-source software and the power of an active community. Having co-developed teal over the past two years, we are excited to begin this new chapter as copyright holders. We look forward to engaging even more closely with the community as teal moves to the Pharmaverse GitHub organization.</p>
<p>— Oriol Senan, R/Shiny developer at Appsilon, contributor to teal</p>
</blockquote>
<p>If you use teal, your feedback helps shape it. Report an issue, suggest an improvement, contribute a module, or join the conversation in the Pharmaverse Slack’s #teal channel.</p>
<p><a href="https://github.com/pharmaverse/teal" rel="nofollow" target="_blank">Explore the repository</a> · <a href="https://pharmaverse.github.io/teal/" rel="nofollow" target="_blank">Read the documentation</a> · <a href="https://github.com/pharmaverse/teal/blob/main/.github/CONTRIBUTING.md" rel="nofollow" target="_blank">Read the contribution guide</a></p>
</section>
<div class="cell">
<div class="cell-output-display">


</div>
</div>



<div id="quarto-appendix" class="default"><section id="last-updated" class="level2 appendix"><h2 class="anchored quarto-appendix-heading">Last updated</h2><div class="quarto-appendix-contents">

<p>2026-11-03 18:52:55.526917</p>
</div></section><section id="details" class="level2 appendix"><h2 class="anchored quarto-appendix-heading">Details</h2><div class="quarto-appendix-contents">

<p><a href="https://github.com/pharmaverse/blog/tree/main/posts/2026-11-04-roche-appsilon-teal/roche-appsilon-teal.qmd" rel="nofollow" target="_blank">Source</a>, <a href="https://pharmaverse.github.io/blog/session_info.html" rel="nofollow" target="_blank">Session info</a></p>
</div></section><section class="quarto-appendix-contents" id="quarto-reuse"><h2 class="anchored quarto-appendix-heading">Reuse</h2><div class="quarto-appendix-contents"><div><a rel="nofollow" href="https://creativecommons.org/licenses/by/4.0/" target="_blank">CC BY 4.0</a></div></div></section><section class="quarto-appendix-contents" id="quarto-citation"><h2 class="anchored quarto-appendix-heading">Citation</h2><div><div class="quarto-appendix-secondary-label">BibTeX citation:</div><pre>@online{unardi2026,
  author = {Unardi, Dony and Veríssimo, André and Senan, Oriol},
  title = {Roche and {Appsilon} {Build} on {Their} {Collaboration} to
    {Support} Teal’s {Continued} {Development}},
  date = {2026-11-04},
  url = {https://pharmaverse.github.io/blog/posts/2026-11-04-roche-appsilon-teal/roche-appsilon-teal.html},
  langid = {en}
}
</pre><div class="quarto-appendix-secondary-label">For attribution, please cite this work as:</div><div id="ref-unardi2026" class="csl-entry quarto-appendix-citeas">
Unardi, Dony, André Veríssimo, and Oriol Senan. 2026. <span>“Roche and
Appsilon Build on Their Collaboration to Support Teal’s Continued
Development.”</span> November 4, 2026. <a href="https://pharmaverse.github.io/blog/posts/2026-11-04-roche-appsilon-teal/roche-appsilon-teal.html" rel="nofollow" target="_blank">https://pharmaverse.github.io/blog/posts/2026-11-04-roche-appsilon-teal/roche-appsilon-teal.html</a>.
</div></div></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://pharmaverse.github.io/blog/posts/2026-11-04-roche-appsilon-teal/roche-appsilon-teal.html"> pharmaverse 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/11/roche-and-appsilon-build-on-their-collaboration-to-support-teals-continued-development/">Roche and Appsilon Build on Their Collaboration to Support teal’s Continued Development</a>]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">404632</post-id>	</item>
		<item>
		<title>L&#8217;IA travaille. Il vous reste le plus difficile.</title>
		<link>https://www.r-bloggers.com/2026/11/lia-travaille-il-vous-reste-le-plus-difficile/</link>
		
		<dc:creator><![CDATA[Boris Guarisma]]></dc:creator>
		<pubDate>Tue, 03 Nov 2026 11:49:14 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://blog.bguarisma.com/ia-travail-qui-reste</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; "> Automatiser avec l'IA ne retire pas le travail humain : il reste la vérification et le jugement, là où l'erreur de confiance est la plus probable.</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/11/lia-travaille-il-vous-reste-le-plus-difficile/">L’IA travaille. Il vous reste le plus difficile.</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/ia-travail-qui-reste"> 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>En 1983, Lisanne Bainbridge, psychologue à l&#8217;University College London, publie un article de cinq pages devenu un classique : « <em>Les ironies de l&#8217;automatisation</em> ». Elle y observe que le concepteur qui cherche à éliminer l&#8217;opérateur lui laisse encore les tâches qu&#8217;il ne voit pas comment automatiser. Elle pensait aux procédés industriels. Sa phrase décrit tout aussi bien les équipes qui confient aujourd&#8217;hui une partie de leur travail à des agents d&#8217;IA.</p>
<p>Un dossier de projet sait chiffrer le temps que l&#8217;IA fera gagner. Il sait beaucoup moins bien chiffrer ce qui reste. Or ce qui reste n&#8217;est pas un résidu : <strong>c&#8217;est de la surveillance, de la vérification et du jugement</strong>, c&#8217;est-à-dire la part du travail où l&#8217;erreur de confiance est la plus probable. Automatiser ne retire pas le travail humain, cela en change la nature. Ce changement a un coût, mais ce coût n&#8217;est pas une fatalité : il dépend de ce qu&#8217;on retire à l&#8217;humain et de ce qu&#8217;on lui laisse.</p>
<p>Dans <a href="https://blog.bguarisma.com/prix-du-jeton-cout-tache-reussie" rel="nofollow" target="_blank">un précédent article</a>, je montrais que le <strong>coût d&#8217;une tâche réussie</strong> dépend du travail humain qui rattrape les échecs. Celui-ci pose la question suivante : <strong>que fait l&#8217;humain quand l&#8217;IA réussit ?</strong></p>
<h2>Surveiller est un travail</h2>
<p>Bainbridge en tirait une conséquence gênante : l&#8217;opérateur peut se retrouver avec « un assortiment arbitraire de tâches », celles que la machine ne sait pas faire. Il hérite aussi de la surveillance d&#8217;un système où il ne se passe presque rien.</p>
<p>On a longtemps tenu la surveillance pour une tâche peu exigeante. La recherche sur la vigilance dit le contraire. En 2008, une revue de Warm, Parasuraman et Matthews réunit des mesures comportementales, neurales et subjectives. Elles convergent : <strong>la vigilance « demande un travail mental intense et elle est stressante »</strong>. Sa charge de travail est élevée, et ces tâches réduisent l&#8217;engagement tout en augmentant la détresse. Surveiller n&#8217;est pas s&#8217;ennuyer : <strong>c&#8217;est un effort.</strong></p>
<p>Ces résultats viennent d&#8217;études expérimentales. Ils posent pourtant une question simple à tout projet d&#8217;automatisation : qui fournit cet effort, et dans quelles conditions ?</p>
<h2>Le travail glisse vers la vérification</h2>
<p><strong>L&#8217;IA générative rend cette question quotidienne</strong>. En 2025, des chercheurs de Microsoft Research et de l&#8217;université Carnegie Mellon interrogent 319 travailleurs du savoir. Selon leurs réponses, l&#8217;IA générative déplace la pensée critique « <strong>vers la vérification de l&#8217;information, l&#8217;intégration des réponses et la tenue de la tâche</strong> ». On rédige moins, on contrôle davantage. Ces résultats sont déclarés par les participants : ils disent ce que les personnes perçoivent, pas ce qu&#8217;elles font.</p>
<p>Ce qu&#8217;elles font, Xingqi Maggie Ye et Aruna Ranganathan, de Berkeley Haas, l&#8217;ont observé pendant huit mois dans une entreprise technologique de 200 personnes dont les salariés avaient largement accès aux outils d&#8217;IA générative. Leur étude ethnographique s&#8217;appuie aussi sur plus de 40 entretiens. Le temps gagné a été réinvesti dans le travail : les salariés « travaillaient plus vite, prenaient en charge un périmètre plus large et étendaient leur travail à davantage d&#8217;heures de la journée, souvent sans qu&#8217;on le leur demande ». Ye et Ranganathan notent que cette expansion semble souvent venir des salariés eux-mêmes. Une seule entreprise, des travaux encore en cours : c&#8217;est un signal, pas une loi.</p>
<p>Reste la perception. En 2025, l&#8217;organisme de recherche METR mène un essai randomisé auprès de 16 développeurs expérimentés de logiciels libres, sur 246 tâches de projets qu&#8217;ils connaissent depuis cinq ans en moyenne. Avec l&#8217;IA, ils mettent 19 % de temps en plus. Après l&#8217;étude, ils estiment pourtant que l&#8217;IA leur a fait gagner 20 % de temps. L&#8217;effectif est petit et les outils sont ceux de février à juin 2025. L&#8217;écart entre l&#8217;impression et la mesure devrait cependant alerter quiconque évalue un projet sur les seuls retours de ses utilisateurs.</p>
<h2>L&#8217;effet « bûcheron »</h2>
<p>Ce déplacement ne serait qu&#8217;une affaire d&#8217;emploi du temps s&#8217;il épargnait la qualité du jugement. <strong>Il ne l&#8217;épargne pas.</strong></p>
<p>En 2014, une méta-analyse de Linda Onnasch et de ses collègues rassemble 18 expériences. Plus le degré d&#8217;automatisation augmente, meilleure est la performance de routine ; tant que l&#8217;automate fonctionne, la charge baisse aussi, mais plus faiblement. Quand il tombe en panne, en revanche, la performance et la conscience de la situation se dégradent. Les auteurs parlent d&#8217;effet « bûcheron » : <strong>plus les arbres sont hauts, plus ils tombent loin</strong>. Dans toutes les études sauf quatre, les participants étaient des étudiants.</p>
<p><strong>L&#8217;expertise ne protège pas.</strong> En 2023, la revue <em>Radiology</em> publie une expérience menée auprès de 27 radiologues qui lisent des mammographies avec l&#8217;aide d&#8217;un système présenté comme une IA. En moyenne, les plus expérimentés classent correctement 82,3 % des clichés quand le système a raison, et 45,5 % quand il se trompe. Chez les moins expérimentés, la justesse passe de 79,7 % à 19,8 %.</p>
<p>Enfin, une revue systématique de Lyell et Coiera (2017) compare la littérature des facteurs humains et celle de la santé : sur 40 études retenues, 6 portent sur la santé. Les auteurs observent le biais d&#8217;automatisation (le fait de trop s&#8217;en remettre à l&#8217;aide à la décision) jusque dans des tâches uniques, lorsque la vérification est complexe, et l&#8217;associent à la charge cognitive de la décision. Pour le limiter, ils suggèrent de réduire cette charge.</p>
<p>Mettons ces résultats bout à bout. Quand on automatise les vérifications faciles, l&#8217;humain garde les difficiles : les exceptions, les cas ambigus, les erreurs que l&#8217;automate n&#8217;a pas vues. Ses décisions deviennent moins nombreuses, mais chacune porte sur une erreur plus rare et plus difficile à voir. C&#8217;est mon interprétation, pas un résultat mesuré : ce qui reste à vérifier après l&#8217;automatisation est ce qui expose le plus au biais.</p>
<img src="https://i1.wp.com/cdn.hashnode.com/uploads/covers/61bc41016916c3593a6b04c3/515dac84-32a0-471e-addd-2508f89472a1.png?w=578&#038;ssl=1" alt="Schéma en deux colonnes. À gauche, ce que l'automatisation retire : vérifications routinières, contrôles de forme, cas standards ; la routine devient plus fiable. À droite, ce qu'elle laisse à l'humain : jugement de pertinence, exceptions et cas ambigus, lecture des alertes, reprise en main en cas d'erreur ; les erreurs deviennent plus rares mais plus difficiles à voir." style="display:block;margin:0 auto" data-recalc-dims="1" />

<p><em>Ce que l&#8217;automatisation retire, ce qu&#8217;elle laisse. Interprétation de l&#8217;auteur à partir des études citées ; ce schéma ne représente aucune mesure.</em></p>
<h2>L&#8217;objection : l&#8217;IA allège aussi</h2>
<p>L&#8217;objection est sérieuse, et elle s&#8217;appuie sur des études solides. Erik Brynjolfsson, Danielle Li et Lindsey Raymond ont étudié l&#8217;introduction échelonnée d&#8217;un assistant conversationnel d&#8217;IA générative auprès de 5 172 agents de service client. La productivité, mesurée en demandes résolues par heure, augmente de 15 % en moyenne, <strong>avec de fortes différences</strong> : les agents les moins expérimentés et les moins qualifiés gagnent en vitesse et en qualité, les plus expérimentés et les plus qualifiés gagnent un peu en vitesse et perdent un peu en qualité. Les auteurs observent aussi une amélioration de l&#8217;expérience du travail : les clients sont plus polis et demandent moins souvent à parler à un responsable.</p>
<p>En médecine, une étude publiée en 2025 dans <em>JAMA Network Open</em> interroge 263 cliniciens de six systèmes de santé américains, avant et après trente jours d&#8217;usage d&#8217;une même plateforme de scribe IA, qui documente les consultations. La part de ceux qui déclarent un burn-out passe de 51,9 % à 38,8 %, et la charge cognitive liée aux notes diminue. L&#8217;étude n&#8217;a pas de groupe témoin, et trois de ses auteurs déclarent des liens avec l&#8217;entreprise Abridge AI, dont deux comme salariés. Elle va néanmoins dans le même sens.</p>
<p><strong>Ces résultats ne contredisent pas les précédents, ils les précisent.</strong> La méta-analyse d&#8217;Onnasch le montrait déjà : tant que l&#8217;automate fonctionne, la charge baisse. Le scribe allège une charge administrative. L&#8217;assistant propose des réponses dans une conversation. À mon sens, aucun des deux ne laisse à l&#8217;humain une vérification plus difficile que celle qu&#8217;il faisait avant. Le coût d&#8217;attention n&#8217;est donc pas une propriété de l&#8217;IA, mais de la répartition du travail : ce qu&#8217;on retire à l&#8217;humain, et ce qu&#8217;on lui laisse.</p>
<h2>Ce qu&#8217;on ne sait pas encore</h2>
<p>Trois questions restent ouvertes, et je préfère les nommer.</p>
<p>Je n&#8217;ai trouvé aucune étude qui mesure si l&#8217;effort par minute augmente quand l&#8217;humain ne garde que le jugement. L&#8217;intuition est forte, la preuve manque.</p>
<p>Sur la fatigue des personnes qui supervisent des outils d&#8217;IA au travail, la seule source directe que j&#8217;ai trouvée est une étude du BCG, menée auprès de 1 488 salariés américains de grandes entreprises et publiée dans la <em>Harvard Business Review</em>. Le BCG conseille lui-même les entreprises sur leur transformation par l&#8217;IA. L&#8217;étude conclut que <strong>l&#8217;usage excessif ou la surveillance constante des outils d&#8217;IA peut entraîner une fatigue mentale</strong>. Une seule étude, publiée par un acteur intéressé : c&#8217;est un indice, pas une preuve.</p>
<p>Enfin, je n&#8217;ai trouvé aucune étude relue par des pairs qui mesure comment la charge évolue avec le nombre d&#8217;agents qu&#8217;une même personne supervise.</p>
<h2>Trois questions avant d&#8217;automatiser</h2>
<p>On ne sait pas tout, mais on en sait assez pour poser de meilleures questions à un projet.</p>
<ol>
<li><strong>Que reste-t-il à vérifier une fois l&#8217;automatisation en place ?</strong> Pas seulement combien de temps : quelles décisions, et sont-elles plus difficiles qu&#8217;avant ?</li>
<li><strong>Qui le vérifie, et avec quel temps réellement alloué ?</strong> Une vérification sans temps alloué devient une signature, pas un contrôle.</li>
<li><strong>Que se passe-t-il le jour où l&#8217;automate se trompe ?</strong> Qui s&#8217;en aperçoit, et qui sait encore reprendre la main ?</li>
</ol>
<p>Le temps gagné reste un bon argument. Il ne dit simplement pas tout ce que l&#8217;automatisation coûte, ni à qui.</p>
<hr />
<p><em>Pour aller plus loin, le livre blanc gratuit</em> <a href="https://qognito.io/livres-blancs/la-facture-fantome-ia/" rel="nofollow" target="_blank"><em>La facture fantôme de l&#8217;IA</em></a> <em>part d&#8217;une question voisine, « qui vérifie le résultat ? », et montre comment faire apparaître la relecture, la correction et la supervision dans le coût complet d&#8217;un projet.</em></p>
<p><em>Les citations d&#8217;études publiées en anglais sont traduites par l&#8217;auteur.</em></p>
<hr />
<h2>Sources</h2>
<ul>
<li>Lisanne Bainbridge, « Ironies of automation », <em>Automatica</em>, 19(6), 1983, p. 775-779 : <a href="https://ckrybus.com/static/papers/Bainbridge_1983_Automatica.pdf" rel="nofollow" target="_blank">https://ckrybus.com/static/papers/Bainbridge_1983_Automatica.pdf</a></li>
<li>J. S. Warm, R. Parasuraman, G. Matthews, « Vigilance requires hard mental work and is stressful », <em>Human Factors</em>, 50(3), 2008, p. 433-441 : <a href="https://pubmed.ncbi.nlm.nih.gov/18689050/" rel="nofollow" target="_blank">https://pubmed.ncbi.nlm.nih.gov/18689050/</a></li>
<li>Hao-Ping (Hank) Lee et al., « The Impact of Generative AI on Critical Thinking: Self-Reported Reductions in Cognitive Effort and Confidence Effects From a Survey of Knowledge Workers », CHI 2025 : <a href="https://www.microsoft.com/en-us/research/publication/the-impact-of-generative-ai-on-critical-thinking-self-reported-reductions-in-cognitive-effort-and-confidence-effects-from-a-survey-of-knowledge-workers/" rel="nofollow" target="_blank">https://www.microsoft.com/en-us/research/publication/the-impact-of-generative-ai-on-critical-thinking-self-reported-reductions-in-cognitive-effort-and-confidence-effects-from-a-survey-of-knowledge-workers/</a></li>
<li>Berkeley Haas, « AI promised to free up workers&#8217; time. UC Berkeley Haas researchers found the opposite », 18 février 2026, sur les travaux en cours de Xingqi Maggie Ye et Aruna Ranganathan, présentés dans la <em>Harvard Business Review</em> : <a href="https://newsroom.haas.berkeley.edu/ai-promised-to-free-up-workers-time-uc-berkeley-haas-researchers-found-the-opposite/" rel="nofollow" target="_blank">https://newsroom.haas.berkeley.edu/ai-promised-to-free-up-workers-time-uc-berkeley-haas-researchers-found-the-opposite/</a></li>
<li>Joel Becker et al. (METR), « Measuring the Impact of Early-2025 AI on Experienced Open-Source Developer Productivity », 2025 : <a href="https://arxiv.org/abs/2507.09089" rel="nofollow" target="_blank">https://arxiv.org/abs/2507.09089</a></li>
<li>Linda Onnasch, Christopher D. Wickens, Huiyang Li, Dietrich Manzey, « Human Performance Consequences of Stages and Levels of Automation: An Integrated Meta-Analysis », <em>Human Factors</em>, 56(3), 2014, p. 476-488 : <a href="https://depositonce.tu-berlin.de/items/7df9879f-d77b-4f2b-8760-f0e3d174ca67/full" rel="nofollow" target="_blank">https://depositonce.tu-berlin.de/items/7df9879f-d77b-4f2b-8760-f0e3d174ca67/full</a></li>
<li>Thomas Dratsch et al., « Automation Bias in Mammography: The Impact of Artificial Intelligence BI-RADS Suggestions on Reader Performance », <em>Radiology</em>, 307(4), 2023, e222176 : <a href="https://pubmed.ncbi.nlm.nih.gov/37129490/" rel="nofollow" target="_blank">https://pubmed.ncbi.nlm.nih.gov/37129490/</a></li>
<li>D. Lyell, E. Coiera, « Automation bias and verification complexity: a systematic review », <em>JAMIA</em>, 24(2), 2017, p. 423-431 : <a href="https://pubmed.ncbi.nlm.nih.gov/27516495/" rel="nofollow" target="_blank">https://pubmed.ncbi.nlm.nih.gov/27516495/</a></li>
<li>Erik Brynjolfsson, Danielle Li, Lindsey Raymond, « Generative AI at Work », <em>Quarterly Journal of Economics</em>, 140(2), 2025, p. 889-942 : <a href="https://doi.org/10.1093/qje/qjae044" rel="nofollow" target="_blank">https://doi.org/10.1093/qje/qjae044</a></li>
<li>Kristine D. Olson et al., « Use of Ambient AI Scribes to Reduce Administrative Burden and Professional Burnout », <em>JAMA Network Open</em>, 8(10), 2025, e2534976 : <a href="https://pubmed.ncbi.nlm.nih.gov/41037268/" rel="nofollow" target="_blank">https://pubmed.ncbi.nlm.nih.gov/41037268/</a></li>
<li>BCG, « When Using AI Leads to “Brain Fry” », 5 mars 2026, présentation de l&#8217;article de Julie Bedard et al. dans la <em>Harvard Business Review</em> : <a href="https://www.bcg.com/news/5march2026-when-using-ai-leads-brain-fry" rel="nofollow" target="_blank">https://www.bcg.com/news/5march2026-when-using-ai-leads-brain-fry</a></li>
</ul>

<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/ia-travail-qui-reste"> 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/11/lia-travaille-il-vous-reste-le-plus-difficile/">L’IA travaille. Il vous reste le plus difficile.</a>]]></content:encoded>
					
		
		<enclosure url="https://cdn.hashnode.com/uploads/covers/61bc41016916c3593a6b04c3/9bfeb5f0-733c-4a7e-9c17-9e962423b7f2.jpg" length="0" type="image/jpeg" />

		<post-id xmlns="com-wordpress:feed-additions:1">404630</post-id>	</item>
		<item>
		<title>Stratified Modeling for Imbalanced Data</title>
		<link>https://www.r-bloggers.com/2026/11/stratified-modeling-for-imbalanced-data/</link>
		
		<dc:creator><![CDATA[Jason Bryer]]></dc:creator>
		<pubDate>Tue, 03 Nov 2026 05:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://bryer.org/posts/2026-11-03-Stratified_Modeling_for_Imbalanced_Data.html</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>NOTE: Any updates to this article will be maintained here: https://medley.bryer.org/articles/stratified_glm.html</p>
<p>Introduction<br />
Statistical modeling when there is substantial imbalance in the dependent variable poses unique challenges. In these si...</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/11/stratified-modeling-for-imbalanced-data/">Stratified Modeling for Imbalanced Data</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://bryer.org/posts/2026-11-03-Stratified_Modeling_for_Imbalanced_Data.html"> Jason Bryer</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>NOTE: Any updates to this article will be maintained here: <a href="https://medley.bryer.org/articles/stratified_glm.html" rel="nofollow" target="_blank">https://medley.bryer.org/articles/stratified_glm.html</a></p>
<section id="introduction" class="level2">
<h2 class="anchored" data-anchor-id="introduction">Introduction</h2>
<p>Statistical modeling when there is substantial imbalance in the dependent variable poses unique challenges. In these situations models often favor the majority class. Two common approaches are upsampling and downsampling. Upsampling is the process of increasing the sampling of the minority group to more closely match the size of the majority group. Conversely, downsampling involves sampling from the majority group to more closely match the size of the minority group. The latter is generally not preferred since it artificially increases the sample size (i.e. <em>n</em>) resulting in decreasing the standard error estimates.<sup>1</sup> This paper introduces an approach where multiple logistic regression models are estimated using the minority group and a unique subset of the majority group such that all observations are majority observations are used in exactly one model while the minority group is reused across models. The final model pools the models using Rubin’s Rules defined within the context of multiple imputation (Rubin, 2004).</p>
</section>
<section id="impact-on-fitted-values" class="level2">
<h2 class="anchored" data-anchor-id="impact-on-fitted-values">Impact on Fitted Values</h2>
<p>In the United States there are broadly two types of schools, public and private. Public schools are funded by property taxes whereas privates schools charge tuition to the families. The <a href="https://www.oecd.org/en/about/programmes/pisa.html" rel="nofollow" target="_blank">Programme for International Student Assessment</a> (PISA) is an international evaluation managed by the OECD. It measures how well 15-year-old students apply their knowledge and problem-solving skills in reading, mathematics, and science to real-world challenges. PISA samples students from both public and private schools within the United States. The <code>medley</code> package contains a subset of the PISA data for the United States containing 5233 observations of 44 variables. There are 4888 public school students and 345 private school students (6.5927766%)</p>
<p>To exemplify the problem with estimating logistic regression with imbalanced data, let’s consider several models where the ratio of public-to-private schools varies from 1-to-1 to 14-to-1. Figure @ref(fig:shrinking-values) represents the distributions of the fitted values from a logistic regression with varying ratios of public-to-private schools. That is, all private school students were used in every model. For the 1-to-1 model, the first 345 public school observations, then for the 2-to-1 model the first 690 observations, etcetera until the final model uses all observations. The vertical lines correspond to the median fitted values. With all else equal, as the ratio of public-to-private schools increases, the range of fitted values shrinks towards the majority group.<sup>2</sup></p>
<div class="cell">
<div class="cell-output-display">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://i0.wp.com/bryer.org/posts/2026-11-03-Stratified_Modeling_for_Imbalanced_Data_files/figure-html/shrinking-values-1.png?w=450&#038;ssl=1" class="img-fluid figure-img"  data-recalc-dims="1"></p>
<figcaption>Distributions for varying public-to-private ratios. Vertical lines corrospond to the median of the fitted values.</figcaption>
</figure>
</div>
</div>
</div>
</section>
<section id="stratified-modeling" class="level2">
<h2 class="anchored" data-anchor-id="stratified-modeling">Stratified Modeling</h2>
<p>The goal of stratified modeling is to address imbalance in the outcome variable while still leveraging all data. The first step is to separate the majority and minority classes. Observations in the minority class will be used in all model estimations. Observations in the majority class will be randomly assigned a model. The number of models is determined by the extent of the imbalance the specified ratio of majority-to-minority observations (the default is 1). For the PISA data here, there are approximately 14 times as many public school observations to private school observations. Using a ratio of one we will estimate 14 models. It should be noted that the public schools students will be used in exactly one model whereas the private school students will be used in all models. The <code>stratified_glm()</code> performs the modeling. To maximize flexibility this function can pass any parameter to the <code>stats::glm()</code> function. However, parameters that reference specific observations (e.g. <code>weights</code>, <code>subset</code>) should not be used since <code>stats::glm()</code> will be called with subsets of the data that will not match the order of the data. Similarly, the <code>family</code> parameter defaults to <code>binomial(link='logit')</code> since this method only works with classification problems with two classes.</p>
<div class="cell">
<pre>results &lt;- medley::stratified_glm(
    formula = Private ~ .,
    data = pisa,
    ratio = 1
)</pre>
</div>
<p>The results of all the models are combined using the <code>mitools::MIcombine</code> function (Lumley, 2026). Although this approach is designed for combining multiple imputed data sets, in this case it allows us to have a single coefficient and standard error that is not artificially smaller given a larger sample size for a model. The fitted values from all the individual models is available in the <code>fitted</code> element of the returned object. Additionally, the individual model results are available in the <code>model</code> element. The <code>summary()</code> function returns a data frame with the combined coefficients, standard error, and p-value. Below only coefficients where p < 0.1 is printed to save space.</p>
<div class="cell">
<pre>pisa_summary &lt;- summary(results)
pisa_summary |&gt; dplyr::filter(p &lt; 0.1)</pre>
<div class="cell-output cell-output-stdout">
<pre>                     coefficients   se     p
(Intercept)                 -5.21 2.63 0.051
ST04Q01Male                  0.84 0.28 0.004
ST08Q04TRUE                 -0.50 0.27 0.071
ST08Q05TRUE                  0.75 0.36 0.038
ST20Q06TRUE                  1.79 0.95 0.060
ST20Q07TRUE                  0.72 0.31 0.021
ST21Q05Three or more         0.93 0.45 0.042
ST22Q01201-500 books         1.18 0.59 0.050</pre>
</div>
</div>
<p>Figure @ref(fig:fitted-distributions) plots the density distributions from the full model (single model using all data) and the stratified models. The stratified sample shows a distribution that spans the entire range of fitted values resulting in fitted values that are more interpretable.</p>
<div class="cell">
<pre>plot(results)</pre>
<div class="cell-output-display">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://i2.wp.com/bryer.org/posts/2026-11-03-Stratified_Modeling_for_Imbalanced_Data_files/figure-html/fitted-distributions-1.png?w=450&#038;ssl=1" class="img-fluid figure-img"  data-recalc-dims="1"></p>
<figcaption>Distribution of fitted values for full and stratified models</figcaption>
</figure>
</div>
</div>
</div>
<p>Predictions for new data can be estimated using the <code>predict()</code> function.</p>
<div class="cell">
<pre>pisa_predicted &lt;- predict(results, newdata = pisa)
head(pisa_predicted)</pre>
<div class="cell-output cell-output-stdout">
<pre>   1    2    3    4    5    6 
0.79 0.16 0.30 0.24 0.22 0.46 </pre>
</div>
</div>
<p>Figure @ref(fig:fitted-distributions-3) shows how the stratified modeling is less effective when the ratio of public-to-private students increases to 3-to-1.</p>
<div class="cell">
<pre>medley::stratified_glm(
    formula = Private ~ .,
    data = pisa,
    ratio = 3
) |&gt; plot()</pre>
<div class="cell-output-display">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://i2.wp.com/bryer.org/posts/2026-11-03-Stratified_Modeling_for_Imbalanced_Data_files/figure-html/fitted-distributions-3-1.png?w=450&#038;ssl=1" class="img-fluid figure-img"  data-recalc-dims="1"></p>
<figcaption>Distribution of fitted values when a ratio of three is used for the stratified model</figcaption>
</figure>
</div>
</div>
</div>
</section>
<section id="impact-for-predictive-modeling" class="level2">
<h2 class="anchored" data-anchor-id="impact-for-predictive-modeling">Impact for Predictive Modeling</h2>
<p>Stratified modeling clearly provides a strategy for addressing the issue of shrinking fitted values from logistic regression as imbalance in the outcome increases. In this section we will explore what impact, if any, does this approach have when modeling for prediction. To begin, we will split our data into a training and validation data set with 80% for training and 20% for validation.</p>
<div class="cell">
<pre>set.seed(2112); split &lt;- medley::stratified_split(pisa, 'Private', 0.8)
pisa_train &lt;- split$train
pisa_valid &lt;- split$validation
pisa_train$Private |&gt; table() |&gt; prop.table()</pre>
<div class="cell-output cell-output-stdout">
<pre>
FALSE  TRUE 
0.934 0.066 </pre>
</div>
<pre>pisa_valid$Private |&gt; table() |&gt; prop.table()</pre>
<div class="cell-output cell-output-stdout">
<pre>
FALSE  TRUE 
0.934 0.066 </pre>
</div>
</div>
<section id="full-model" class="level3">
<h3 class="anchored" data-anchor-id="full-model">Full Model</h3>
<p>First, we will estimate the logistic regression model using the full data set.</p>
<div class="cell">
<pre>full_model &lt;- glm(Private ~ ., data = pisa_train, family = binomial(link = 'logit'))</pre>
</div>
<p>Get the predictions for the validation data set.</p>
<div class="cell">
<pre>full_new_predictions &lt;- predict(full_model, newdata = pisa_valid, type = 'response')</pre>
</div>
<p>We calculate the metrics for a receiver operating characteristic below. The summary function provides us with some thresholds that maximize accuracy, minimize cost<sup>3</sup>, or where the specificity and sensitivity curves intersect.</p>
<div class="cell">
<pre>full_roc &lt;- medley::calculate_roc(
    predictions = full_new_predictions,
    observed = pisa_valid$Private
)
summary(full_roc)</pre>
<div class="cell-output cell-output-stdout">
<pre>AUC = 0.747
Cost of false-positive = 1
Cost of false-negative = 1
Threshold with minimum cost = 0.465 (accuracy = 93.4%)
Threshold with maximum accuracy = 0.465 (accuracy = 93.4%)
Sensitivity and specificity intersection = 0.059 (accuracy = 66.4%)</pre>
</div>
</div>
<p>Figure @ref(fig:full-roc) represents the receiver operating characteristic plot for the validation data using the full model.</p>
<div class="cell">
<pre>plot(full_roc)</pre>
<div class="cell-output-display">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://i1.wp.com/bryer.org/posts/2026-11-03-Stratified_Modeling_for_Imbalanced_Data_files/figure-html/full-roc-1.png?w=450&#038;ssl=1" class="img-fluid figure-img"  data-recalc-dims="1"></p>
<figcaption>Receiver operating characteristic (ROC) and accuracy curves for the full model</figcaption>
</figure>
</div>
</div>
</div>
<p>In order to balance sensitivity and specificity we provide a confusion matrix using the intersection of these two curves as the cut point for determing the outcome class.</p>
<div class="cell">
<pre>medley::confusion_matrix(
    observed = pisa_valid$Private,
    predicted = full_new_predictions &gt; attr(full_roc, 'intersection')
)</pre>
<div class="cell-output cell-output-stdout">
<pre>              predicted             
  observed        FALSE         TRUE
     FALSE 649 (61.99%) 329 (31.42%)
      TRUE   23 (2.20%)   46 (4.39%)
Accuracy: 66.38%
Sensitivity: 66.36%
Specificity: 66.67%</pre>
</div>
</div>
</section>
<section id="stratified-model" class="level3">
<h3 class="anchored" data-anchor-id="stratified-model">Stratified Model</h3>
<p>Now let’s estimate the stratified model.</p>
<div class="cell">
<pre>stratified_model &lt;- medley::stratified_glm(
    formula = Private ~ .,
    data = pisa_train,
    ratio = 1
)
stratified_new_predictions &lt;- predict(stratified_model, newdata = pisa_valid)</pre>
</div>
<p>Examine the receiver operating characteristics to determine the optimal cut point. Figure @ref(fig:stratified-roc) plots the results.</p>
<div class="cell">
<pre>stratified_roc &lt;- medley::calculate_roc(
    predictions = stratified_new_predictions,
    observed = pisa_valid$Private
)
summary(stratified_roc)</pre>
<div class="cell-output cell-output-stdout">
<pre>AUC = 0.745
Cost of false-positive = 1
Cost of false-negative = 1
Threshold with minimum cost = 1 (accuracy = 93.4%)
Threshold with maximum accuracy = 1 (accuracy = 93.4%)
Sensitivity and specificity intersection = 0.509 (accuracy = 68.6%)</pre>
</div>
</div>
<div class="cell">
<pre>plot(stratified_roc)</pre>
<div class="cell-output-display">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://i2.wp.com/bryer.org/posts/2026-11-03-Stratified_Modeling_for_Imbalanced_Data_files/figure-html/stratified-roc-1.png?w=450&#038;ssl=1" class="img-fluid figure-img"  data-recalc-dims="1"></p>
<figcaption>Receiver operating characteristic (ROC) and accuracy curves for the stratified model</figcaption>
</figure>
</div>
</div>
</div>
<p>Finally, calculate the confusion matrix using the intersection of the sensitivity and specificity curves as the cut point for determing predicted class.</p>
<div class="cell">
<pre>medley::confusion_matrix(
    observed = pisa_valid$Private,
    predicted = full_new_predictions &gt; attr(stratified_roc, 'intersection')
)</pre>
<div class="cell-output cell-output-stdout">
<pre>              predicted          
  observed        FALSE      TRUE
     FALSE 975 (93.12%) 3 (0.29%)
      TRUE   68 (6.49%) 1 (0.10%)
Accuracy: 93.22%
Sensitivity: 99.69%
Specificity: 1.45%</pre>
</div>
</div>
<p>Often introductory texts for predictive modeling and logistic regression suggest using a cut point of 0.5 for determining the class. As show above, this is not the optimal cut point when there is significant imbalance. The stratified modeling approach introduced gets us closer to that intuitive cut point in the middle. It should be noted that this approach is no better or worse for making predictions. However, it does address the issue of shrinking fitted values and makes the fitted and predicted values more interpretable.</p>
</section>
</section>
<section id="conclusion" class="level2">
<h2 class="anchored" data-anchor-id="conclusion">Conclusion</h2>
<p>The stratified modeling approach provides a modeling approach for logistic regression where interpretability of the fitted or predicted values are meaningful. For predictive modeling adjusting the cut point for making a class prediction will provide a similar result. However, this issue was initially encountered when conducting propensity score analysis (PSA). Logistic regression is often used to estimate the propensity scores, which are defined as the conditional probability of being in the treatment (Rosenbaum & Rubin, 1983). When using logistic regression, the propensity scores are simply the fitted values from the model. In this context having fitted values that span the entire range where the distribution of the treatment group (private schools in this example) is skewed to the right and the control group (public schools in this example) is skewed to the left. For these situations the stratified modeling approach may provide a reasonable approach to addressing the issue of shrinking fitted values from logistic regression with significant imbalance.</p>
</section>
<section id="references" class="level2">
<h2 class="anchored" data-anchor-id="references">References</h2>
<p>Bryer, Jason M., “A national study comparing charter and traditional public schools using propensity score analysis” (2014). <em>Legacy Theses & Dissertations (2009 - 2024).</em> 1090. https://doi.org/10.54014/H2BR-2XKN</p>
<p>Lumley, T (2026). mitools: Tools for Multiple Imputation of Missing Data_. https://doi.org/10.32614/CRAN.package.mitools. R package version 2.7, <a href="https://cran.r-project.org/package=mitools" class="uri" rel="nofollow" target="_blank">https://CRAN.R-project.org/package=mitools</a>.</p>
<p>Rosenbaum, P.R., & Rubin, D.B. (1983). The central role of the propensity score in observational studies for causal effects. <em>Biometrika, 70</em>(1), 41–55, https://doi.org/10.1093/biomet/70.1.41</p>
<p>Rubin DB. <em>Multiple Imputation for Nonresponse in Surveys.</em> New York: John Wiley and Sons; 2004.</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>The <img src="https://latex.codecogs.com/png.latex?%5Clim_%7Bx%5Cto%5Cinfty%7D%5Cfrac%7Bn%7D%7Bx%7D=0"> for all fixed <img src="https://latex.codecogs.com/png.latex?n">.<img src="https://s.w.org/images/core/emoji/13.0.0/72x72/21a9.png" alt="↩" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎</p></li>
<li id="fn2"><p>See https://psa.bryer.org/appendix-psranges.html for a simulation showing the same result.<img src="https://s.w.org/images/core/emoji/13.0.0/72x72/21a9.png" alt="↩" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎</p></li>
<li id="fn3"><p>Since the costs of false-positives and false-negatives are the same this will be the same threshold as maximizing accuracy<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://bryer.org/posts/2026-11-03-Stratified_Modeling_for_Imbalanced_Data.html"> Jason Bryer</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/11/stratified-modeling-for-imbalanced-data/">Stratified Modeling for Imbalanced Data</a>]]></content:encoded>
					
		
		<enclosure url="https://bryer.org/posts/2026-11-03-banner.png" length="0" type="image/png" />

		<post-id xmlns="com-wordpress:feed-additions:1">404655</post-id>	</item>
		<item>
		<title>Introducing tuiksdmx: A Metadata-Driven R Client for Turkish Official Statistics</title>
		<link>https://www.r-bloggers.com/2026/11/introducing-tuiksdmx-a-metadata-driven-r-client-for-turkish-official-statistics/</link>
		
		<dc:creator><![CDATA[M. Fatih Tüzen]]></dc:creator>
		<pubDate>Tue, 03 Nov 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://mfatihtuzen.github.io/posts/2026-11-03_tuiksdmx/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>The tuiksdmx R package logo</p>
<p>1 Introduction<br />
Accessing official statistics programmatically should be straightforward. In practice, however, finding the right dataset is only part of the problem. An analyst also needs to understand its dime...</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/11/introducing-tuiksdmx-a-metadata-driven-r-client-for-turkish-official-statistics/">Introducing tuiksdmx: A Metadata-Driven R Client for Turkish Official Statistics</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://mfatihtuzen.github.io/posts/2026-11-03_tuiksdmx/"> A Statistician&#039;s R Notebook</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/mfatihtuzen.github.io/posts/2026-11-03_tuiksdmx/logo.png?w=210&#038;ssl=1" class="img-fluid figure-img"  data-recalc-dims="1"></p>
<figcaption>The <code>tuiksdmx</code> R package logo</figcaption>
</figure>
</div>
<section id="introduction" class="level2" data-number="1">
<h2 data-number="1" class="anchored" data-anchor-id="introduction"><span class="header-section-number">1</span> Introduction</h2>
<p>Accessing official statistics programmatically should be straightforward. In practice, however, finding the right dataset is only part of the problem. An analyst also needs to understand its dimensions, available classifications, valid codes, and the structure of the request accepted by the data provider.</p>
<p>This is particularly relevant for <a href="https://sdmx.org/" rel="nofollow" target="_blank">SDMX</a> (Statistical Data and Metadata eXchange), the international standard widely used to exchange statistical data and metadata. SDMX provides a consistent framework for describing multidimensional statistical datasets, but it does not eliminate the need to understand their structures.</p>
<p>I developed <strong><code>tuiksdmx</code></strong>, an open-source R package that provides an <strong>authenticated, metadata-driven interface</strong> to the Turkish Statistical Institute’s (TÜİK, or TurkStat) SDMX REST service. The goal is not simply to download data. It is to make the steps leading to a valid and reproducible request easier to discover, understand, and verify.</p>
<p>The project is available on <a href="https://github.com/MFatihTuzen/tuiksdmx" rel="nofollow" target="_blank">GitHub</a>, with detailed guides and function documentation on its <a href="https://mfatihtuzen.github.io/tuiksdmx/" rel="nofollow" target="_blank">dedicated website</a>.</p>
</section>
<section id="why-a-metadata-driven-client" class="level2" data-number="2">
<h2 data-number="2" class="anchored" data-anchor-id="why-a-metadata-driven-client"><span class="header-section-number">2</span> Why a Metadata-Driven Client?</h2>
<p>An SDMX dataset is not necessarily a conventional rectangular table waiting to be downloaded. A particular <em>Dataflow</em> is associated with a <em>Data Structure Definition</em> (DSD), which specifies the dimensions used to identify a statistical series. These dimensions may reference <em>Codelists</em> defining permitted values, while <em>Concepts</em> describe what the dimensions represent.</p>
<p>A typical workflow therefore involves several questions:</p>
<ol type="1">
<li>Which Dataflow contains the statistics I need?</li>
<li>What dimensions and classifications define that Dataflow?</li>
<li>Which codes are valid for the relevant dimensions?</li>
<li>How can I request observations without assembling a raw SDMX key by hand?</li>
</ol>
<p><code>tuiksdmx</code> brings these steps together. It supports Dataflow discovery, DSD and Codelist inspection, metadata-based filter validation, structure-aware key construction, and observation retrieval. It also helps distinguish between the codes defined in metadata and those actually present in returned data.</p>
<p>The distinction matters: <strong>a code being valid does not mean that observations exist for it in every period.</strong></p>
</section>
<section id="getting-started" class="level2" data-number="3">
<h2 data-number="3" class="anchored" data-anchor-id="getting-started"><span class="header-section-number">3</span> Getting Started</h2>
<section id="install-the-package" class="level3" data-number="3.1">
<h3 data-number="3.1" class="anchored" data-anchor-id="install-the-package"><span class="header-section-number">3.1</span> Install the package</h3>
<p>Version <strong>0.1.0</strong> is distributed through GitHub; it is <strong>not currently on CRAN</strong>. Install it with <a href="https://pak.r-lib.org/" rel="nofollow" target="_blank"><code>pak</code></a>:</p>
<div class="cell">
<pre>install.packages(&quot;pak&quot;)  # If needed
pak::pak(&quot;MFatihTuzen/tuiksdmx&quot;)

library(tuiksdmx)</pre>
</div>
</section>
<section id="configure-authentication" class="level3" data-number="3.2">
<h3 data-number="3.2" class="anchored" data-anchor-id="configure-authentication"><span class="header-section-number">3.2</span> Configure authentication</h3>
<p>The TÜİK SDMX service currently requires authentication for its data endpoints. <code>tuiksdmx</code> accepts an API key and handles the authentication workflow internally. For regular use, I recommend storing the key in your personal <code>.Renviron</code> file, rather than embedding credentials in an R script.</p>
<pre>TUIK_SDMX_API_KEY=YOUR_OWN_API_KEY</pre>
<p>You can open that file using <code>usethis::edit_r_environ()</code> if you have <strong>usethis</strong> installed. Restart R after saving it. The package will then read the environment variable automatically.</p>
<p><strong>Never publish your API key or access tokens in a script, blog post, repository, or issue report.</strong> You must obtain and configure your own credentials; installing the package does not provide an API key.</p>
</section>
<section id="find-a-dataflow" class="level3" data-number="3.3">
<h3 data-number="3.3" class="anchored" data-anchor-id="find-a-dataflow"><span class="header-section-number">3.3</span> Find a Dataflow</h3>
<p>Instead of starting with a hard-coded endpoint, search the available Dataflows by subject:</p>
<div class="cell">
<pre># Search by an English description
flows &lt;- tuik_search_dataflows(
  &quot;consumer price&quot;,
  language = &quot;en&quot;,
  limit = 10
)

flows</pre>
</div>
<p>You can also search in Turkish, for example with <code>tuik_search_dataflows(&quot;tüketici fiyat&quot;)</code>. This makes discovery possible without knowing the Dataflow identifier in advance.</p>
</section>
<section id="explore-dimensions-before-retrieving-data" class="level3" data-number="3.4">
<h3 data-number="3.4" class="anchored" data-anchor-id="explore-dimensions-before-retrieving-data"><span class="header-section-number">3.4</span> Explore dimensions before retrieving data</h3>
<p>The following example uses the consumer price Dataflow <code>DF_TUFE_SDMX_TT01</code>, inspected during package development. Since provider catalogues can evolve, always check the current metadata before building a production workflow.</p>
<div class="cell">
<pre># Explore the structure without retrieving observations
tuik_explore(&quot;DF_TUFE_SDMX_TT01&quot;)

# Inspect the codes associated with one dimension
tuik_explore(
  &quot;DF_TUFE_SDMX_TT01&quot;,
  dimension = &quot;DEGISIM&quot;
)</pre>
</div>
<p>This is one of the central design decisions behind the package: <strong>understand the data structure first, then request observations.</strong> It reduces the need to guess dimension identifiers or construct query keys manually.</p>
</section>
<section id="retrieve-observations" class="level3" data-number="3.5">
<h3 data-number="3.5" class="anchored" data-anchor-id="retrieve-observations"><span class="header-section-number">3.5</span> Retrieve observations</h3>
<p>Once the relevant Dataflow and period are known, retrieving data can be concise:</p>
<div class="cell">
<pre>cpi &lt;- tuik_get_data(
  &quot;DF_TUFE_SDMX_TT01&quot;,
  start_period = &quot;2026-01&quot;,
  end_period   = &quot;2026-01&quot;
)

head(cpi)</pre>
</div>
<p>There is an important qualification: <strong>this request retrieves all series for the selected observation period</strong>, not just one CPI indicator. Restricting the time range alone does not guarantee a small response.</p>
<p>For a more focused query, the package accepts named, metadata-validated dimension filters:</p>
<div class="cell">
<pre>selected &lt;- list(
  REF_AREA = &quot;TR&quot;,
  FREQ = &quot;M&quot;,
  DEGISIM = &quot;1&quot;,
  SINIFLAMA_DUZEYI = &quot;2&quot;
)

# Validate the chosen values against the Dataflow structure
tuik_explore(
  &quot;DF_TUFE_SDMX_TT01&quot;,
  filters = selected
)</pre>
</div>
<p>A practical limitation is worth highlighting. <strong>Complete series-key filters</strong> allow the package to construct a targeted server-side SDMX request. <strong>Partial filters</strong>, on the other hand, require an explicit <code>allow_all = TRUE</code> opt-in and may lead to a broader download followed by filtering in R. The package does not present this as an efficient server-side partial-key request, nor does <code>allow_all</code> impose a data-size limit. The <a href="https://mfatihtuzen.github.io/tuiksdmx/articles/filtering-and-performance.html" rel="nofollow" target="_blank">filtering and performance guide</a> explains the distinction in detail.</p>
</section>
</section>
<section id="a-subtle-but-important-question-which-codes-actually-have-data" class="level2" data-number="4">
<h2 data-number="4" class="anchored" data-anchor-id="a-subtle-but-important-question-which-codes-actually-have-data"><span class="header-section-number">4</span> A Subtle but Important Question: Which Codes Actually Have Data?</h2>
<p>Metadata and observed data answer different questions.</p>
<p>A <strong>Codelist</strong> defines the theoretical set of codes for a dimension. But some of those codes may not appear in the observations returned for a particular period and set of filters.</p>
<p><code>tuiksdmx</code> addresses this distinction through two complementary operations:</p>
<div class="cell">
<pre>profile &lt;- tuik_flow_profile(&quot;DF_TUFE_SDMX_TT01&quot;)

# Permitted codes in the metadata
tuik_dimension_codes(profile, dimension = &quot;DEGISIM&quot;)

# Codes observed in the data for a bounded request
observed &lt;- tuik_available_codes(
  profile,
  dimension = &quot;COICOP_2018&quot;,
  start_period = &quot;2026-01&quot;,
  end_period = &quot;2026-01&quot;,
  allow_all = TRUE
)

observed</pre>
</div>
<p>The second operation inspects <strong>retrieved observations</strong>, so the result is conditional on the request. It is <strong>not</strong> an implementation of SDMX <em>ContentConstraint</em> queries. Moreover, the example deliberately permits a potentially broad download and should be used with attention to response size.</p>
<p>For statistical work, this separation helps prevent a common misunderstanding: the universe of valid categories is not necessarily the same as the categories populated by the data.</p>
</section>
<section id="documentation-and-current-scope" class="level2" data-number="5">
<h2 data-number="5" class="anchored" data-anchor-id="documentation-and-current-scope"><span class="header-section-number">5</span> Documentation and Current Scope</h2>
<p>The package includes documentation on authentication, Dataflow discovery, DSD metadata, filtering, observed availability, and code-label decoding. Rather than repeating those guides here, I recommend starting with the <a href="https://mfatihtuzen.github.io/tuiksdmx/articles/getting-started.html" rel="nofollow" target="_blank">Getting Started guide</a> and the <a href="https://mfatihtuzen.github.io/tuiksdmx/reference/" rel="nofollow" target="_blank">function reference</a>.</p>
<p>The initial version is deliberately focused. <code>tuiksdmx</code> is an SDMX client, not a general-purpose interface to every resource on the TÜİK portal. It does not claim to support every SDMX service feature or every Dataflow without further testing. Provider structures and authentication behaviour can change, so reproducible work still requires validation and sensible request boundaries.</p>
</section>
<section id="final-thoughts" class="level2" data-number="6">
<h2 data-number="6" class="anchored" data-anchor-id="final-thoughts"><span class="header-section-number">6</span> Final Thoughts</h2>
<p>When working with an API, it is tempting to judge a client only by how quickly it returns a table. For structured statistical data, I think a more useful measure is whether we can <strong>identify what we requested, validate the choices we made, and reproduce the result later</strong>.</p>
<p>That is the idea behind <code>tuiksdmx</code>: bring discovery, statistical metadata, validation, and data retrieval into the same R workflow while keeping the raw SDMX machinery largely out of the way.</p>
<p>I welcome feedback, reproducible bug reports, and contributions through the project repository. The package is an <strong>independently developed open-source project</strong>, not an official TÜİK software product.</p>
<p><strong>Project links:</strong> <a href="https://github.com/MFatihTuzen/tuiksdmx" rel="nofollow" target="_blank">GitHub repository</a> · <a href="https://mfatihtuzen.github.io/tuiksdmx/" rel="nofollow" target="_blank">Package website</a> · <a href="https://mfatihtuzen.github.io/tuiksdmx/articles/" rel="nofollow" target="_blank">Guides</a> · <a href="https://github.com/MFatihTuzen/tuiksdmx/issues" rel="nofollow" target="_blank">Issues and suggestions</a></p>
</section>
<section id="references" class="level2" data-number="7">
<h2 data-number="7" class="anchored" data-anchor-id="references"><span class="header-section-number">7</span> References</h2>
<ul>
<li>SDMX. <em>Statistical Data and Metadata eXchange</em>. <a href="https://sdmx.org/" class="uri" rel="nofollow" target="_blank">https://sdmx.org/</a>.</li>
<li>Turkish Statistical Institute (TÜİK). <em>Official website</em>. <a href="https://www.tuik.gov.tr/" class="uri" rel="nofollow" target="_blank">https://www.tuik.gov.tr/</a>.</li>
<li>Tüzen, M. F. (2026). <em>tuiksdmx: Access the Turkish Statistical Institute SDMX REST API</em> (version 0.1.0). GitHub. <a href="https://github.com/MFatihTuzen/tuiksdmx" class="uri" rel="nofollow" target="_blank">https://github.com/MFatihTuzen/tuiksdmx</a>.</li>
<li>Tüzen, M. F. <em>tuiksdmx package documentation</em>. <a href="https://mfatihtuzen.github.io/tuiksdmx/" class="uri" rel="nofollow" target="_blank">https://mfatihtuzen.github.io/tuiksdmx/</a>.</li>
</ul>


<!-- -->

</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://mfatihtuzen.github.io/posts/2026-11-03_tuiksdmx/"> A Statistician&#039;s R Notebook</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/11/introducing-tuiksdmx-a-metadata-driven-r-client-for-turkish-official-statistics/">Introducing tuiksdmx: A Metadata-Driven R Client for Turkish Official Statistics</a>]]></content:encoded>
					
		
		<enclosure url="https://mfatihtuzen.github.io/posts/2026-11-03_tuiksdmx/logo.png" length="0" type="image/png" />

		<post-id xmlns="com-wordpress:feed-additions:1">404653</post-id>	</item>
		<item>
		<title>rplatypus rebuilt: deep learning for medical images in R</title>
		<link>https://www.r-bloggers.com/2026/11/rplatypus-rebuilt-deep-learning-for-medical-images-in-r/</link>
		
		<dc:creator><![CDATA[Michał Maj]]></dc:creator>
		<pubDate>Tue, 03 Nov 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://datascienceguts.com/posts/2026-11-03-rplatypus/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>In October 2020 I wrote about platypus, an R package for object detection and image segmentation: YOLOv3 and U-Net in a few lines of R, on top of the keras and tensorflow packages. People used it. It collected 134 stars and a queue of issues - ...</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/11/rplatypus-rebuilt-deep-learning-for-medical-images-in-r/">rplatypus rebuilt: deep learning for medical images in 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://datascienceguts.com/posts/2026-11-03-rplatypus/"> Data Science Guts</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 October 2020 I wrote about <a href="https://github.com/maju116/rplatypus/tree/master" rel="nofollow" target="_blank">platypus</a>, an R package for object detection and image segmentation: YOLOv3 and U-Net in a few lines of R, on top of the <code>keras</code> and <code>tensorflow</code> packages. People used it. It collected 134 stars and a queue of issues &#8211; and then it quietly stopped working, and I stopped answering them.</p>
<p>This year I went back to it. Computer vision is the part of data science I enjoy most, and this package has always been my passion project &#8211; the place where I get to learn things my day job does not ask of me. What came out is not an update but a different package, with a new name, a different backend and a narrower purpose: <strong>rplatypus</strong>, deep learning for medical images in R. It has been submitted to CRAN and is waiting for review; until it is accepted, install it from GitHub:</p>
<div class="cell">
<pre># install.packages(&quot;remotes&quot;)
remotes::install_github(&quot;maju116/rplatypus&quot;)</pre>
</div>
<p>This post is about why it had to be rewritten rather than fixed, what is in it now, and where the series of posts that follows is going.</p>
<section id="why-the-old-one-broke" class="level2">
<h2 class="anchored" data-anchor-id="why-the-old-one-broke">Why the old one broke</h2>
<p>R’s <code>keras</code> and <code>tensorflow</code> packages are not bindings to a C++ library. They are bridges, through <code>reticulate</code>, into a Python installation of TensorFlow &#8211; and in 2020 that was <em>the</em> Python installation, the one shared with everything else on the machine. Upgrade numpy for an unrelated project and an R session using platypus could stop loading. The failure never appeared in my package’s code, which made it miserable to support: the bug reports were about other people’s Python.</p>
<p>The second problem is the one that actually made me stop. platypus sat on two layers I did not control: the R <code>keras</code> and <code>tensorflow</code> packages on top of TensorFlow itself. When TensorFlow changed in Python, the R packages had to catch up before platypus could, and that often took a very long time. Until they did, my package was stuck, with issues I could not fix from my side. After enough of that, I lost the will to keep working on it.</p>
<p>Two things have changed since, and together they remove both problems at the root rather than patching around them.</p>
<p><strong>reticulate learned to isolate.</strong> Since version 1.41, a package declares its Python requirements with <code>py_require()</code>, and reticulate builds a private environment for them with <a href="https://docs.astral.sh/uv/" rel="nofollow" target="_blank">uv</a>, cached and shared with nobody. There is no common Python left for two packages to fight over. rplatypus never touches a Python anyone else uses &#8211; and the person installing it never chooses an interpreter, activates anything or matches a version. The first call that needs the engine builds the environment; every call after that reuses it. And there is no R wrapper in between any more: rplatypus calls its own engine, which I release myself, so nothing waits for anybody else to catch up.</p>
<p><strong>The medical imaging world settled on PyTorch.</strong> MONAI, nnU-Net, TorchIO, timm, the pretrained encoders and the foundation models &#8211; all of it is PyTorch. TensorFlow for medical images is effectively over, and the TensorFlow 2.9 the old engine was pinned to does not even install on Python 3.12. Staying would have meant rewriting anyway, onto a backend with no future in the field.</p>
<p>A third thing changed too. It does not fix the old failure, but it is the reason the rewrite happened at all instead of staying a good idea: <strong>writing a package got much cheaper.</strong> In 2020 I wrote every line of platypus myself, and the parts that make a package trustworthy &#8211; tests, documentation, error messages that say what went wrong &#8211; were the parts I never had time left for. This time I worked with an AI coding agent, <a href="https://claude.com/claude-code" rel="nofollow" target="_blank">Claude Code</a>.</p>
<p>I want to be precise about what that means, because “AI wrote my package” is not it. The decisions were mine: what the package is for, which datasets to measure it on, what it should refuse, and what a result has to show before it goes into the documentation &#8211; every number in this series was measured, not generated. What the agent changed was the cost of everything around those decisions: writing the tests, keeping two packages in two languages in step, chasing a failing check through reticulate, uv and CUDA. That is exactly the work that used to end side projects like this one. The rewrite took around a month.</p>
<p>So the computation moved to <a href="https://pypi.org/project/pyplatypus/" rel="nofollow" target="_blank">pyplatypus</a>, a Python engine on PyTorch, and rplatypus is the R package on top of it. The split is deliberate:</p>
<ul>
<li><strong>The engine is in Python</strong>, because that is where the models, the weights and the medical libraries already are. One codebase to maintain, and everything PyTorch has is within reach.</li>
<li><strong>The product is in R</strong>, because that is where the gap is. Reading and combining masks, plotting, comparing models, the analysis around the network &#8211; that part is plain R and needs no Python at all.</li>
</ul>
<p>I did consider native R <code>torch</code>, which binds libtorch directly and needs no Python. For one person maintaining this in their evenings it would have meant writing every architecture, loss and data pipeline twice, and giving up every pretrained weight in the Python ecosystem.</p>
</section>
<section id="pyplatypus-the-same-engine-from-python" class="level2">
<h2 class="anchored" data-anchor-id="pyplatypus-the-same-engine-from-python">pyplatypus: the same engine, from Python</h2>
<p>The engine is not hidden plumbing. <a href="https://github.com/maju116/pyplatypus" rel="nofollow" target="_blank">pyplatypus</a> is a package on PyPI in its own right &#8211; it too started life in 2022 on TensorFlow and was rewritten alongside &#8211; and if you work in Python you can use it directly:</p>
<div class="cell">
<pre>pip install &quot;pyplatypus[hub]&quot;</pre>
</div>
<p>The example below runs from both languages, out of one file.</p>
<p>The two are developed together, and in a fixed order. A feature lands in the engine first, the engine is published to PyPI, and only then does rplatypus move to it &#8211; each release of the R package pins one exact version of pyplatypus, so it cannot break because the engine changed underneath it. They also share one <a href="https://maju116.github.io/pyplatypus/config.html" rel="nofollow" target="_blank">configuration format</a>: the same YAML file describes the same experiment whichever language reads it, so an R user and a Python user can hand each other a run.</p>
<p>That is also how this series is written. The examples are in R, because that is where the gap is, and each post shows what the same step looks like from Python.</p>
</section>
<section id="the-gap" class="level2">
<h2 class="anchored" data-anchor-id="the-gap">The gap</h2>
<p>Here is the part that convinced me this was worth doing. R has excellent tools for <em>reading</em> medical images &#8211; RNifti, oro.dicom, divest &#8211; and a healthy deep learning stack. Almost nothing joins them. When I checked in September, the CRAN Task View for <a href="https://cran.r-project.org/view=MedicalImaging" rel="nofollow" target="_blank">Medical Image Analysis</a> (last updated August 2025) listed no deep learning packages at all.</p>
<p>So rplatypus is meant to be that missing piece: not a wrapper around a Python library you then have to learn, but an R package that happens to compute in Python.</p>
</section>
<section id="a-mask-without-training-anything" class="level2">
<h2 class="anchored" data-anchor-id="a-mask-without-training-anything">A mask, without training anything</h2>
<p>The quickest way to see whether a package does anything useful is to borrow a model someone already trained. rplatypus publishes weights by name &#8211; here a U-Net trained on the 2018 Data Science Bowl, the same nuclei dataset I used on this blog in 2020.</p>
<p>The folder <code>nuclei/</code> holds four microscopy images in the dataset’s own layout &#8211; one directory per image, with <code>images/</code> and <code>masks/</code> inside.</p>
<div class="cell">
<pre>library(rplatypus)

spec &lt;- platypus_spec(
  data = segmentation_data(&quot;nuclei&quot;, &quot;nuclei&quot;, colormap = binary_colormap),
  models = list(u_net(&quot;nuclei&quot;, input_shape = c(256, 256), blocks = 4, filters = 16,
                      weights = &quot;dsbowl-unet&quot;, fit = FALSE))
)
fit &lt;- platypus_fit(spec, verbose = FALSE)
masks &lt;- predict(fit, split = &quot;validation&quot;)
dim(masks)
#&gt; [1]   4 256 256</pre>
</div>
<p>Four images, 256 by 256 pixels, one class label per pixel. This ran on the processor, with no GPU; <code>platypus_device()</code> tells you whether yours will use one, and why not if it can’t.</p>
<div class="cell">
<pre>images &lt;- list.files(&quot;nuclei&quot;, pattern = &quot;[.]png$&quot;, recursive = TRUE, full.names = TRUE)
images &lt;- read_images(grep(&quot;/images/&quot;, images, value = TRUE), size = c(256, 256))
plot_masks(images, prediction = masks, colormap = binary_colormap)</pre>
<div class="cell-output-display">
<div id="fig-nuclei" class="quarto-float quarto-figure quarto-figure-center anchored">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-nuclei-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<img src="https://i1.wp.com/datascienceguts.com/posts/2026-11-03-rplatypus/index_files/figure-html/fig-nuclei-1.png?w=450&#038;ssl=1" class="img-fluid figure-img"  data-recalc-dims="1">
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig" id="fig-nuclei-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure 1: Nuclei found by the published <code>dsbowl-unet</code> weights, on four images of different kinds.
</figcaption>
</figure>
</div>
</div>
</div>
<p>The first three are fluorescence images, and the nuclei come out as clean blobs. The fourth is a brightfield image of stained cells, and the model finds specks - the small dark nuclei inside the cells. That is the hard kind of image in this dataset, and the next post looks at it closely.</p>
<p>There is no score here, and that is on purpose. These four images come from the same collection the weights were trained on, so any number measured on them would flatter the model. The numbers that mean something were measured on a held-out split when the weights were published, and <code>available_weights()</code> says what they are and what the model cannot do - for instance, that it is a <em>semantic</em> model, so touching nuclei come back as one region and a count taken from them would be wrong.</p>
</section>
<section id="the-same-thing-from-a-file" class="level2">
<h2 class="anchored" data-anchor-id="the-same-thing-from-a-file">The same thing from a file</h2>
<p>The same specification can live in a file. This one started as <code>save_spec(spec, &quot;nuclei.yaml&quot;)</code>, with the lists tidied by hand:</p>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre>nuclei.yaml</pre>
</div>
<pre>task: semantic_segmentation
data:
  train_path: nuclei
  validation_path: nuclei
  colormap: [[0, 0, 0], [255, 255, 255]]
models:
- name: nuclei
  architecture: u_net
  input_shape: [256, 256]
  blocks: 4
  filters: 16
  weights: dsbowl-unet
  fit: false</pre>
</div>
<p>Reading it back gives exactly the object built from arguments above:</p>
<div class="cell">
<pre>identical(as.list(platypus_spec(&quot;nuclei.yaml&quot;)), as.list(spec))
#&gt; [1] TRUE</pre>
</div>
<p>So <code>platypus_fit(platypus_spec(&quot;nuclei.yaml&quot;))</code> does everything the R code did, and nothing downstream can tell which way the specification was written. The Python engine reads the very same file:</p>
<div class="cell">
<pre>from pyplatypus import build_engine, from_yaml

engine = build_engine(from_yaml(&quot;nuclei.yaml&quot;))
engine.fit()  # with `fit: false` this loads the published weights instead of training
masks = engine.predict(&quot;nuclei&quot;, split=&quot;validation&quot;)</pre>
</div>
<p>A configuration file and an R script describe the same experiment, and either can be handed to a colleague - whichever language they work in.</p>
</section>
<section id="what-is-in-it" class="level2">
<h2 class="anchored" data-anchor-id="what-is-in-it">What is in it</h2>
<p>Briefly, because most of these get their own post:</p>
<ul>
<li><strong>Segmentation</strong>, 2D and 3D: U-Net, U-Net++, Res-U-Net and LinkNet, eleven losses, five metrics that can leave the background out - which matters when the background is most of the image.</li>
<li><strong>Classification</strong> - one label for a whole image, scored by AUC rather than accuracy, because a classifier that answers “no finding” where 3% have one scores 0.97 and has learnt nothing.</li>
<li><strong>Object detection</strong> with YOLOv3, with anchors fitted to your boxes rather than borrowed from COCO.</li>
<li><strong>Instance segmentation</strong> - a count of objects instead of an area.</li>
<li><strong>Volumes</strong>: NIfTI and DICOM in, with voxel spacing handled, a split by patient rather than by slice, and results in millilitres rather than voxels.</li>
<li><strong>A score per case, not one per dataset</strong>, so the worst image has a name.</li>
<li><strong>Refusals before a wasted day</strong>: a colormap that matches none of your masks, channels in an order you did not intend, a DICOM series with a missing slice - each stopped before training, by name.</li>
<li><strong>One specification</strong> - from R arguments or from a YAML file, read the same way by the Python engine.</li>
</ul>
<p>The full reference and seven vignettes, six of them precomputed from real runs, are at <a href="https://maju116.github.io/rplatypus/" rel="nofollow" target="_blank">maju116.github.io/rplatypus</a>.</p>
<p>This is where the package starts, not where it ends. The U-Net family, YOLOv3 and the classifier are the first models in it, not the only ones that will be: more architectures are coming, along with what is still missing - multi-label classification, ensembling, boxes in three dimensions. Which of those comes first depends partly on what people actually need, which is one more reason for the question at the end of this post.</p>
<p>Development is moving fast right now, and CRAN asks for new versions no more often than every month or two, so once the package is on CRAN, <code>install.packages(&quot;rplatypus&quot;)</code> will usually be a step behind. The GitHub version above will always be the newest.</p>
</section>
<section id="what-comes-next" class="level2">
<h2 class="anchored" data-anchor-id="what-comes-next">What comes next</h2>
<p>This is the first post of a series. Each of the next ones takes one dataset and one thing that measurement on it taught me:</p>
<ol type="1">
<li><strong>Nuclei, five years later</strong> - a model with a mean Dice of 0.855 that scores 0.006 on three images, and why the mean could not tell you.</li>
<li><strong>Classification</strong> - a classifier with 0.97 accuracy that has learnt nothing, and why balanced accuracy does not rescue it.</li>
<li><strong>Blood cells and YOLOv3</strong> - anchors, and why a point of mAP across five seeds is noise.</li>
<li><strong>CT volumes</strong> - a Dice of 0.88 and volumes 12-23% too large, which is not a contradiction.</li>
<li><strong>Retinal vessels</strong> - why you cannot resize a vessel nine pixels wide.</li>
<li><strong>Counting touching nuclei</strong> - 245 objects or 180, from the same forward pass.</li>
<li><strong>Pretrained encoders</strong> - measured on three modalities, where a 1.9M-parameter encoder beats ResNet-34 on two of them.</li>
</ol>
</section>
<section id="one-question-for-you" class="level2">
<h2 class="anchored" data-anchor-id="one-question-for-you">One question for you</h2>
<p>Everything above rests on an assumption I have not been able to test: that people do medical image analysis in R and would rather stay in R to do deep learning on it. If that is you - or if it is your lab, and you went to Python because there was nothing here - I would really like to hear about it. <a href="https://github.com/maju116/rplatypus/issues" rel="nofollow" target="_blank">Open an issue</a>, write to me at <a href="mailto:michalmaj116@gmail.com" rel="nofollow" target="_blank">michalmaj116@gmail.com</a>, or find me on <a href="https://www.linkedin.com/in/michal-maj116/" rel="nofollow" target="_blank">LinkedIn</a>.</p>
<p>And one more thing, since this is a good place to say it: I am looking for my next role. If your team works on computer vision, medical imaging, or any problem where a model has to be measured honestly before anyone trusts it, I would be glad to talk - the same email works.</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://datascienceguts.com/posts/2026-11-03-rplatypus/"> Data Science Guts</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/11/rplatypus-rebuilt-deep-learning-for-medical-images-in-r/">rplatypus rebuilt: deep learning for medical images in R</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404628</post-id>	</item>
		<item>
		<title>Roche Open-Source TLG Packages Move to pharmaverse</title>
		<link>https://www.r-bloggers.com/2026/11/roche-open-source-tlg-packages-move-to-pharmaverse/</link>
		
		<dc:creator><![CDATA[Joe Zhu]]></dc:creator>
		<pubDate>Tue, 03 Nov 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://pharmaverse.github.io/blog/posts/2026-11-03-roche-open-source-t/roche-open-source-tlg-packages-move-to-pharmaverse.html</guid>

					<description><![CDATA[<p>Written on behalf of the NEST team.<br />
Over the past few months, we have reached an important milestone in our open-source journey: transitioning key clinical reporting and exploratory analysis R packages from the insightsengineering GitHub organ...</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/11/roche-open-source-tlg-packages-move-to-pharmaverse/">Roche Open-Source TLG Packages Move to pharmaverse</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://pharmaverse.github.io/blog/posts/2026-11-03-roche-open-source-t/roche-open-source-tlg-packages-move-to-pharmaverse.html"> pharmaverse 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>
 





<!--------------- typical setup ----------------->
<!--------------- post begins here ----------------->
<p><em>Written on behalf of the NEST team.</em></p>
<p>Over the past few months, we have reached an important milestone in our open-source journey: transitioning key clinical reporting and exploratory analysis R packages from the <a href="https://github.com/insightsengineering" rel="nofollow" target="_blank">insightsengineering</a> GitHub organization into the <a href="https://github.com/pharmaverse" rel="nofollow" target="_blank"><strong>pharmaverse</strong></a>.</p>
<p>The recent migration of the {teal} framework marks another step in this transition, bringing these framework packages under pharmaverse governance.</p>
<section id="why-the-move-to-pharmaverse" class="level2">
<h2 class="anchored" data-anchor-id="why-the-move-to-pharmaverse">Why the Move to pharmaverse?</h2>
<p>When we initially incubated these packages under insightsengineering, our primary goal was to modernize clinical reporting workflows and pioneer reproducible, interactive data exploration. However, open source in the pharmaceutical industry is at its most powerful when built <strong>with</strong> the community, not just for it.</p>
<p>Moving these packages directly under the pharmaverse organization:</p>
<ol type="1">
<li><strong>Breaks Down Barriers to Collaboration:</strong> Moving beyond decision-making led by a single organization allows other companies and community contributors to help shape priorities, roadmaps, and development.</li>
<li><strong>Harmonizes the Ecosystem:</strong> Consolidating standard clinical data manipulation, table creation, and exploration tools into a unified organization makes it easier for teams across industry sponsors and CROs to adopt, contribute to, and integrate tools.</li>
<li><strong>Fosters Long-Term Sustainability:</strong> It paves the way for joint ownership, core co-maintainership, and broader industry governance across life-science data science stacks.</li>
</ol>
</section>
<section id="what-packages-have-migrated" class="level2">
<h2 class="anchored" data-anchor-id="what-packages-have-migrated">What Packages Have Migrated?</h2>
<p>The transition has taken place gradually over the past several months and includes foundational packages spanning tabular reporting, clinical analysis modules, and interactive exploration:</p>
<ul>
<li><strong>Reporting &#038; Tabulation Foundation:</strong>
<ul>
<li><a href="https://pharmaverse.github.io/formatters/" rel="nofollow" target="_blank">formatters</a> — Core rendering engine for ASCII and rich-text table layouts.</li>
<li><a href="https://pharmaverse.github.io/rtables/" rel="nofollow" target="_blank">rtables</a> — Tabular summaries and complex multi-level layout generation.</li>
<li><a href="https://pharmaverse.github.io/rlistings/" rel="nofollow" target="_blank">rlistings</a> — Clinical listings supporting pagination and display formatting.</li>
<li><a href="https://pharmaverse.github.io/cards/" rel="nofollow" target="_blank">cards</a> &#038; <a href="https://pharmaverse.github.io/cardx/" rel="nofollow" target="_blank">cardx</a> — Analysis results datasets (ARDs) creation and statistical helper utilities.</li>
</ul></li>
<li><strong>Analysis &#038; Automation Modules:</strong>
<ul>
<li><a href="https://pharmaverse.github.io/tern/" rel="nofollow" target="_blank">tern</a> — Standard clinical trial summary tables, figures, and listings (TLGs).</li>
<li><a href="https://pharmaverse.github.io/autoslider.core/" rel="nofollow" target="_blank">autoslider.core</a> — Automated presentation/slide generation engines.</li>
</ul></li>
<li><strong>Interactive Exploration ({teal} ecosystem):</strong>
<ul>
<li>The entire suite of <strong>{teal} framework core packages</strong> (including <a href="https://pharmaverse.github.io/teal/" rel="nofollow" target="_blank">teal</a>, <a href="https://pharmaverse.github.io/teal.code/" rel="nofollow" target="_blank">teal.code</a>, <a href="https://pharmaverse.github.io/teal.data/" rel="nofollow" target="_blank">teal.data</a>, <a href="https://pharmaverse.github.io/teal.widgets/" rel="nofollow" target="_blank">teal.widgets</a>, <a href="https://pharmaverse.github.io/teal.reporter/" rel="nofollow" target="_blank">teal.reporter</a>, <a href="https://pharmaverse.github.io/teal.slice/" rel="nofollow" target="_blank">teal.slice</a>, <a href="https://pharmaverse.github.io/teal.logger/" rel="nofollow" target="_blank">teal.logger</a>, <a href="https://pharmaverse.github.io/teal.picks/" rel="nofollow" target="_blank">teal.picks</a>).</li>
</ul></li>
</ul>
</section>
<section id="what-changes-for-users-and-developers" class="level2">
<h2 class="anchored" data-anchor-id="what-changes-for-users-and-developers">What Changes for Users and Developers?</h2>
<ul>
<li><strong>CRAN / Release Users:</strong> <strong>Zero breaking changes.</strong> The repository move does not change existing released package versions. If you install these versions via CRAN or an internal mirror, no changes to your R code are required as a result of the move.</li>
<li><strong>GitHub Remotes &#038; Dev Versions:</strong> GitHub automatically handles redirection for repository moves, but we recommend updating your development remotes and <code>remotes::install_github()</code> / <code>pak::pkg_install()</code> references from <code>insightsengineering/&lt;pkg&gt;</code> to <code>pharmaverse/&lt;pkg&gt;</code>:</li>
</ul>
<div class="cell">
<pre>pak::pkg_install(&quot;pharmaverse/teal&quot;)
pak::pkg_install(&quot;pharmaverse/rtables&quot;)</pre>
</div>
<ul>
<li><strong>Contributing &#038; Issue Tracking:</strong> For the migrated packages, all active issue trackers, feature requests, and discussions are now centralized under <code>pharmaverse/&lt;repo&gt;</code>. You are welcome to submit PRs, suggest features, or report bugs directly there.</li>
</ul>
</section>
<section id="looking-ahead" class="level2">
<h2 class="anchored" data-anchor-id="looking-ahead">Looking Ahead</h2>
<p>As these packages become part of pharmaverse, we look forward to broader participation and shared stewardship across the community. We welcome contributions from companies and individuals to help shape their direction and support their continued development.</p>
<!--------------- appendices go here ----------------->
</section>
<div class="cell">
<div class="cell-output-display">


</div>
</div>



<div id="quarto-appendix" class="default"><section id="last-updated" class="level2 appendix"><h2 class="anchored quarto-appendix-heading">Last updated</h2><div class="quarto-appendix-contents">

<p>2026-10-29 15:41:41.215425</p>
</div></section><section id="details" class="level2 appendix"><h2 class="anchored quarto-appendix-heading">Details</h2><div class="quarto-appendix-contents">

<p><a href="https://github.com/pharmaverse/blog/tree/main/posts/2026-11-03-roche-open-source-t/roche-open-source-tlg-packages-move-to-pharmaverse.qmd" rel="nofollow" target="_blank">Source</a>, <a href="https://pharmaverse.github.io/blog/session_info.html" rel="nofollow" target="_blank">Session info</a></p>
</div></section><section class="quarto-appendix-contents" id="quarto-reuse"><h2 class="anchored quarto-appendix-heading">Reuse</h2><div class="quarto-appendix-contents"><div><a rel="nofollow" href="https://creativecommons.org/licenses/by/4.0/" target="_blank">CC BY 4.0</a></div></div></section><section class="quarto-appendix-contents" id="quarto-citation"><h2 class="anchored quarto-appendix-heading">Citation</h2><div><div class="quarto-appendix-secondary-label">BibTeX citation:</div><pre>@online{zhu2026,
  author = {Zhu, Joe and Unardi, Dony},
  title = {Roche {Open-Source} {TLG} {Packages} {Move} to Pharmaverse},
  date = {2026-11-03},
  url = {https://pharmaverse.github.io/blog/posts/2026-11-03-roche-open-source-t/roche-open-source-tlg-packages-move-to-pharmaverse.html},
  langid = {en}
}
</pre><div class="quarto-appendix-secondary-label">For attribution, please cite this work as:</div><div id="ref-zhu2026" class="csl-entry quarto-appendix-citeas">
Zhu, Joe, and Dony Unardi. 2026. <span>“Roche Open-Source TLG Packages
Move to Pharmaverse.”</span> November 3, 2026. <a href="https://pharmaverse.github.io/blog/posts/2026-11-03-roche-open-source-t/roche-open-source-tlg-packages-move-to-pharmaverse.html" rel="nofollow" target="_blank">https://pharmaverse.github.io/blog/posts/2026-11-03-roche-open-source-t/roche-open-source-tlg-packages-move-to-pharmaverse.html</a>.
</div></div></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://pharmaverse.github.io/blog/posts/2026-11-03-roche-open-source-t/roche-open-source-tlg-packages-move-to-pharmaverse.html"> pharmaverse 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/11/roche-open-source-tlg-packages-move-to-pharmaverse/">Roche Open-Source TLG Packages Move to pharmaverse</a>]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">404514</post-id>	</item>
		<item>
		<title>Agents in Production: Build Your First AI Agent in R with {ellmer}</title>
		<link>https://www.r-bloggers.com/2026/11/agents-in-production-build-your-first-ai-agent-in-r-with-ellmer/</link>
		
		<dc:creator><![CDATA[The Jumping Rivers Blog]]></dc:creator>
		<pubDate>Mon, 02 Nov 2026 23:59:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://www.jumpingrivers.com/blog/agents-in-production-first-agent/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>This is part one of a five-part series on putting AI agents into production with R.<br />
Each part builds on the same app, a clinical trial safety assistant. The parts are:</p>
<p>From Chatbot to Agent (this post)<br />
Putting the Agent in a Shiny App<br />
Can You Tr...</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/11/agents-in-production-build-your-first-ai-agent-in-r-with-ellmer/">Agents in Production: Build Your First AI Agent in R with {ellmer}</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/agents-in-production-first-agent/"> 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/agents-in-production-first-agent/">
<img src="https://www.jumpingrivers.com/blog/agents-in-production-first-agent/" width="400" style="width:400px" class="image-center" style="display: block; margin: auto;" />
</a>
</p>
<p>This is part one of a five-part series on putting AI agents into production with R.
Each part builds on the same app, a clinical trial safety assistant. The parts are:</p>
<ul>
<li><a href="https://www.jumpingrivers.com/blog/agents-in-production-first-agent/" rel="nofollow" target="_blank">From Chatbot to Agent</a> (this post)</li>
<li>Putting the Agent in a Shiny App</li>
<li>Can You Trust It?</li>
<li>Audit Trails, Costs and Models</li>
<li>Sharing Your Tools with MCP</li>
</ul>
<p>We asked Gemini how many people on the high dose of a drug had itching in a well-known clinical trial. It said 21, and sounded sure of it. We asked again and got 25, with different numbers for the other groups too. Both answers cited the trial’s data files, but the model hadn’t been given them. It was answering from memory, so neither answer could be checked.</p>
<p>In this series we build an assistant whose numbers you can check. The model picks an R function, and R does the counting. We tell the model never to do the maths itself, and part 3 checks that it doesn’t. It can still ask R the wrong question, so every result says what was counted.</p>
<p>No pharma background is needed, since the pattern works for any data your team asks about. Later parts add a Shiny app, tests, limits on who sees patient records, an audit trail, and a way to share the tools with other AI agents.</p>
<h2 id="what-is-an-ai-agent">What is an AI agent?</h2>
<p>A chatbot answers from what it learned in training. An AI agent can also ask your code to do things, which is called <strong>tool calling</strong> (or function calling).</p>
<p>You give the model a list of tools, each with a name, a description and its arguments. When a question needs a tool, the model replies with a request instead of an answer, such as “run <code>count_subjects()</code> with <code>arm = &quot;Placebo&quot;</code>”.</p>
<p>Your code runs the function and sends back the result. This repeats until the model has enough to answer, and that cycle is the agent loop.</p>
<p><strong>The model never runs your code.</strong> It can only ask. Your R session decides what runs and what goes back.</p>
<figure class="w600">
<figcaption>The agent loop: the model asks for tools, R runs them, and the model answers from the results.</figcaption>
<a href="https://www.jumpingrivers.com/blog/agents-in-production-first-agent/assets/agent-loop.svg" rel="nofollow" target="_blank"><img src="https://www.jumpingrivers.com/blog/agents-in-production-first-agent/assets/agent-loop.svg" alt="Diagram of the agent loop. Your question goes to the model. The model sends a tool request, such as count_subjects with arm Placebo and term PRURITUS, to your R session. R runs the function on the data and sends back a tool result, such as 8 subjects with the event. This repeats until the model can answer, and it is told to answer from the tool results only."></a>
</figure>
<p>{ellmer} runs this loop for you. You write ordinary R functions, describe them and register them with a chat. New to {ellmer}? Our <a href="https://www.jumpingrivers.com/blog/programming-llms-r-python/" rel="nofollow" target="_blank">LLMs in R &#038; Python webinar</a> is a good start. As the <a href="https://ellmer.tidyverse.org/articles/tool-calling.html" rel="nofollow" target="_blank">{ellmer} tool-calling guide</a> recommends, all exact computation happens inside the tools.</p>
<h2 id="what-you-need">What you need</h2>
<p>Get this part’s folder from <a href="https://github.com/jumpingrivers/blog/tree/main/blogs/agents-in-production/part-1" rel="nofollow" target="_blank">our blog repository</a>, by cloning the repository or downloading it as a ZIP file, and open it as a project in R 4.5. The project uses {renv} to pin every package version, so run this once:</p>
<pre>install.packages(&quot;renv&quot;)
renv::restore()
</pre><p>If you’d rather not use {renv}, install the packages yourself, though you’ll get newer versions than we tested:</p>
<pre>install.packages(c(&quot;ellmer&quot;, &quot;pharmaverseadam&quot;, &quot;dplyr&quot;, &quot;jsonlite&quot;))
</pre><p>You’ll also need a Gemini API key, and our <a href="https://www.jumpingrivers.com/blog/ellmer-dynamic-alt-text/" rel="nofollow" target="_blank">alt-text post</a> shows how to get one. To store it, run <code>file.edit(&quot;.Renviron&quot;)</code> in the project, add the line <code>GEMINI_API_KEY=your-key</code>, save the file and restart R. The code’s README has more setup tips, for Windows in particular.</p>
<p>The free tier is fine here, because this trial’s data is public, but not for your own patient or confidential data. Google’s terms also require the paid service for any app offered to users in the UK, the EEA or Switzerland (more on this in part 4).</p>
<p>We tested everything with R 4.5.1, {ellmer} 0.5.0 and {pharmaverseadam} 1.3.0 in September 2026. {ellmer} changes quickly, so if something behaves differently, check your versions first.</p>
<h2 id="the-trial-data">The trial data</h2>
<p>Our example is CDISC pilot study 01, a worked example published by CDISC, the body that sets clinical trial data standards. It tested a skin patch of an experimental drug, xanomeline, in people with mild to moderate Alzheimer’s disease. Participants, called <em>subjects</em> in trial data, wore a placebo, low-dose or high-dose patch for 26 weeks.</p>
<p>The {pharmaverseadam} package rebuilds the study’s analysis datasets in CDISC’s ADaM format, and we need two of them: <code>adsl</code>, with one row per subject, and <code>adae</code>, with one row per adverse event. An adverse event is anything medically unwanted that happened to a subject during the trial, whether or not the drug caused it.</p>
<aside>
<h3 id="trial-terms-in-plain-english">Trial terms, in plain English</h3>
<ul>
<li><strong>Arm:</strong> a treatment group, here placebo, low dose (54 mg) or high dose (81 mg, after two weeks at 54 mg).</li>
<li><strong>Screen failure:</strong> someone assessed who didn’t join the trial.</li>
<li><strong>Safety population:</strong> everyone who had at least one dose. Safety results count this group.</li>
<li><strong>Treatment-emergent:</strong> an adverse event that started, or got worse, after the first dose and no later than a set window after the last one (14 days, in this data).</li>
<li><strong>Serious adverse event:</strong> one with a serious outcome, such as death, a threat to life, a hospital stay or lasting disability. Intensity is recorded separately, as <em>severity</em> (mild, moderate or severe), so a severe headache need not be serious. In the data, the <code>AESER</code> flag marks serious events, and our tools count that flag.</li>
<li><strong>Preferred term</strong> and <strong>body system</strong> (or <em>system organ class</em>): each event is coded with a standard medical dictionary, MedDRA. “Itchy skin” becomes the preferred term PRURITUS, in the body system “skin and subcutaneous tissue disorders”.</li>
<li><strong>Application site:</strong> where the patch was worn. <em>Erythema</em> means redness.</li>
</ul>
</aside>
<p>Our tools count the way a safety table usually does, and two of their choices are easy to miss. Both change every count:</p>
<ul>
<li><strong>Not everyone in <code>adsl</code> joined the trial.</strong> Of its 306 people, 52 are screen failures, so the safety population (<code>SAFFL == &quot;Y&quot;</code>) is 254 subjects.</li>
<li><strong>Planned and received treatment can differ.</strong> Each subject has a planned arm (<code>ARM</code>) and the treatment they received (<code>ACTARM</code>). Twelve subjects randomised to the high dose stopped within two weeks, before their dose went up to 81 mg, so here they count as low dose.</li>
</ul>
<figure class="w600">
<figcaption>Planned arms against treatment received: 86, 84 and 84 subjects become 86, 96 and 72.</figcaption>
<a href="https://www.jumpingrivers.com/blog/agents-in-production-first-agent/assets/who-gets-counted.svg" rel="nofollow" target="_blank"><img src="https://www.jumpingrivers.com/blog/agents-in-production-first-agent/assets/who-gets-counted.svg" alt="Diagram. Everyone in adsl, 306 people, minus 52 screen failures, leaves a safety population of 254 subjects. Split by planned arm, that is 86 placebo, 84 low dose and 84 high dose. Split by treatment received, as {pharmaverseadam} records it and our tools count it, it is 86 placebo, 96 low dose and 72 high dose. An arrow shows 12 subjects moving from the planned high dose to the low dose, because they stopped within two weeks, before their dose went up to 81 mg. A note says this is how {pharmaverseadam} records them, and that the pilot's own analysis kept them on the high dose."></a>
</figure>
<p>If you work with trial data, the <a href="https://www.jumpingrivers.com/blog/agents-in-production-first-agent/#a-note-for-pharma-readers" rel="nofollow" target="_blank">note for pharma readers</a> near the end covers a few choices a formal analysis plan might make differently.</p>
<p>The tools also keep only the treatment-emergent events (<code>TRTEMFL == &quot;Y&quot;</code>), which leaves 1,122 of the 1,191 event rows. One function, in <code>R/data.R</code>, does all of this:</p>
<!-- source: code/R/data.R -->
<pre>ARMS &lt;- c(&quot;Placebo&quot;, &quot;Xanomeline Low Dose&quot;, &quot;Xanomeline High Dose&quot;)

load_trial_data &lt;- function() {
 subjects &lt;- pharmaverseadam::adsl |&gt;
 dplyr::filter(SAFFL == &quot;Y&quot;) |&gt;
 dplyr::select(USUBJID, ACTARM, AGE, SEX, RACE)
 events &lt;- pharmaverseadam::adae |&gt;
 dplyr::filter(TRTEMFL %in% &quot;Y&quot;, USUBJID %in% subjects$USUBJID) |&gt;
 dplyr::select(USUBJID, ACTARM, AEDECOD, AETERM, AEBODSYS, AESER, AESEV, ASTDT)
 list(
 subjects = subjects,
 events = events,
 # Every term and body system recorded in the trial, including the few
 # never treatment-emergent, so asking about them gives a real zero.
 terms = sort(unique(pharmaverseadam::adae$AEDECOD)),
 body_systems = sort(unique(pharmaverseadam::adae$AEBODSYS))
 )
}
</pre>
<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="asking-without-tools">Asking without tools</h2>
<p>We started by asking <code>gemini-3.7-flash</code> the opening question twice, with no tools:</p>
<!-- source: code/demo/part-1.R, lines 4-6 -->
<pre># Without tools, the model can only guess.
bare &lt;- ellmer::chat_google_gemini()
bare$chat(&quot;In CDISC pilot study 01, how many subjects on high-dose xanomeline had pruritus?&quot;, echo = &quot;output&quot;)
</pre><p>Pruritus is itching. Both answers are pasted as they came back, Markdown and all, except that their headings are shown in bold.</p>
<div class="conversation-turn conversation-turn--pedro conversation-turn--right" data-speaker="Pedro">
<div class="conversation-speaker">Pedro</div>
<div class="conversation-body">In CDISC pilot study 01, how many subjects on high-dose xanomeline had pruritus?</div>
</div>
<div class="conversation-turn conversation-turn--gemini, first try conversation-turn--left" data-speaker="Gemini, first try">
<div class="conversation-speaker">Gemini, first try</div>
<div class="conversation-body"><p>In the <strong>CDISC Pilot Study 01</strong> (Safety Pilot), <strong>21 subjects</strong> (or <strong>25.0%</strong>)
in the Xanomeline High-Dose group had the adverse event <strong>pruritus</strong>.</p>
<p><strong>Summary Breakdown for Pruritus (Treatment-Emergent AEs):</strong></p>
<ul>
<li><strong>Placebo</strong> ($N = 86$): 2 subjects (2.3%)</li>
<li><strong>Xanomeline Low Dose (54 mg)</strong> ($N = 84$): 11 subjects (13.1%)</li>
<li><strong>Xanomeline High Dose (81 mg)</strong> ($N = 84$): <strong>21 subjects (25.0%)</strong></li>
</ul>
<p><em>(Source: CDISC Pilot 01 Table 14-4.01 / <code>ADAE</code> dataset, Preferred Term:
<code>Pruritus</code> under the System Organ Class “Skin and subcutaneous tissue
disorders”)</em></p>
</div>
</div>
<div class="conversation-turn conversation-turn--gemini, second try conversation-turn--left" data-speaker="Gemini, second try">
<div class="conversation-speaker">Gemini, second try</div>
<div class="conversation-body"><p>In the <strong>CDISC Pilot Study 01</strong> (evaluating Xanomeline vs. Placebo in
Alzheimer’s Disease), <strong>25 subjects</strong> in the <strong>Xanomeline High Dose</strong> treatment
group experienced <strong>Pruritus</strong> (MedDRA Preferred Term: <code>PRURITUS</code>).</p>
<p><strong>Context & Breakdown across Treatment Arms:</strong></p>
<ul>
<li><strong>Placebo</strong> ($N = 86$): 5 subjects (5.8%)</li>
<li><strong>Xanomeline Low Dose</strong> ($N = 84$): 18 subjects (21.4%)</li>
<li><strong>Xanomeline High Dose</strong> ($N = 84$): <strong>25 subjects (29.8%)</strong></li>
</ul>
<p><em>(Source: CDISC SDTM/ADaM Pilot Project 01 Adverse Events Analysis Dataset,
<code>ADAE</code>)</em></p>
</div>
</div>
<p>Neither answer matches the data we hold, but each high-dose figure is a correct number for something else:</p>
<table>
<thead>
<tr>
<th></th>
<th>High-dose pruritus</th>
<th>Where the number comes from</th>
</tr>
</thead>
<tbody>
<tr>
<td>First answer</td>
<td>21 of 84 (25.0%)</td>
<td>The low dose’s count, over the planned arm size</td>
</tr>
<tr>
<td>Second answer</td>
<td>25 of 84 (29.8%)</td>
<td>The right count, over the planned arm size</td>
</tr>
<tr>
<td>The data</td>
<td>25 of 72 (34.7%)</td>
<td>Treatment-emergent, by treatment received</td>
</tr>
</tbody>
</table>
<p>On placebo, the data says 8 of 86 (9.3%), and the other counts in both answers match nothing.</p>
<p>The second answer is the more dangerous one. Its count is right, and only the arm size differs from ours, so it survives a quick check. And nothing in either answer says which data, which population or which events it counted.</p>
<h2 id="writing-the-tools">Writing the tools</h2>
<p>The model never sees a tool’s code, only its description, so the description is what tells it when to use the tool. Most of the work happens in <code>count_subjects()</code>, in <code>R/tools.R</code>:</p>
<!-- source: code/R/tools.R, count_subjects() -->
<pre>count_subjects &lt;- function(data, arm = NULL, serious_only = FALSE, term = NULL, body_system = NULL) {
 arm &lt;- check_arm(arm)
 serious_only &lt;- check_flag(serious_only, &quot;serious_only&quot;)
 term &lt;- check_known(
 check_string(term, &quot;term&quot;), data$terms, &quot;preferred term&quot;,
 &quot;Call ae_summary to see the terms used in this trial.&quot;
 )
 body_system &lt;- check_known(
 check_string(body_system, &quot;body_system&quot;), data$body_systems, &quot;body system&quot;,
 &quot;Call ae_summary with by = &#39;body_system&#39; to see them.&quot;
 )
 data &lt;- filter_arm(data, arm)
 events &lt;- data$events
 if (serious_only) events &lt;- dplyr::filter(events, AESER == &quot;Y&quot;)
 if (!is.null(term)) events &lt;- dplyr::filter(events, AEDECOD == term)
 if (!is.null(body_system)) events &lt;- dplyr::filter(events, AEBODSYS == body_system)
 n &lt;- dplyr::n_distinct(events$USUBJID)
 total &lt;- nrow(data$subjects)
 jsonlite::toJSON(list(
 arm = arm %||% &quot;All arms&quot;,
 # Say what was counted, so a wrong reading of the question shows.
 filters = list(
 serious_only = serious_only,
 term = term %||% &quot;any&quot;,
 body_system = body_system %||% &quot;any&quot;
 ),
 subjects_with_event = n,
 subjects_in_arm = total,
 percent = round(100 * n / total, 1)
 ), auto_unbox = TRUE)
}
</pre><p>Four choices matter most:</p>
<ul>
<li><strong>It returns JSON.</strong> The model reads text, and {ellmer} 0.5.0 deprecates returning data frames from tools.</li>
<li><strong>It says what it counted.</strong> The result repeats the filters it applied, so if the model misread the question, you can see it.</li>
<li><strong>Unknown input is an error, never a zero.</strong> Ask for a term the trial doesn’t have and the tool fails, with a message telling the model how to recover. A zero that means “I didn’t understand” is worse than no answer.</li>
<li><strong>Every argument is checked.</strong> Models don’t always send the types they’re asked for, and in part 5 other programs call these tools over MCP, a standard way for AI apps to share tools.</li>
</ul>
<p>We added that last check after a problem in testing. Passing <code>serious_only = &quot;true&quot;</code>, as text rather than <code>TRUE</code>, made the tool quietly count every subject with any adverse event. It answered 217 instead of 3.</p>
<p>Because it’s an ordinary R function, you can call it without a model. Source the files in <code>R/</code> first:</p>
<pre>for (file in list.files(&quot;R&quot;, pattern = &quot;[.]R$&quot;, full.names = TRUE)) source(file)
count_subjects(load_trial_data(), arm = &quot;Xanomeline High Dose&quot;, term = &quot;PRURITUS&quot;)
#&gt; {&quot;arm&quot;:&quot;Xanomeline High Dose&quot;,&quot;filters&quot;:{&quot;serious_only&quot;:false,&quot;term&quot;:&quot;PRURITUS&quot;,&quot;body_system&quot;:&quot;any&quot;},&quot;subjects_with_event&quot;:25,&quot;subjects_in_arm&quot;:72,&quot;percent&quot;:34.7}
</pre><p>The tool definition is what the model sees:</p>
<!-- source: code/R/tools.R, the count_subjects tool in trial_tools() -->
<pre>ellmer::tool(
 function(arm = NULL, serious_only = FALSE, term = NULL, body_system = NULL) {
 count_subjects(data, arm, serious_only, term, body_system)
 },
 name = &quot;count_subjects&quot;,
 description = paste(
 &quot;Count subjects in the safety population with at least one treatment-emergent&quot;,
 &quot;adverse event matching the filters. Returns the count, the number of subjects&quot;,
 &quot;in the arm and the percentage.&quot;
 ),
 arguments = list(
 arm = arm_type,
 serious_only = ellmer::type_boolean(&quot;Only count serious adverse events.&quot;, required = FALSE),
 term = ellmer::type_string(
 &quot;MedDRA preferred term, e.g. &#39;PRURITUS&#39;. Leave out to count any event.&quot;,
 required = FALSE
 ),
 body_system = ellmer::type_string(
 &quot;MedDRA body system, e.g. &#39;NERVOUS SYSTEM DISORDERS&#39;. Leave out for any.&quot;,
 required = FALSE
 )
 ),
 annotations = ellmer::tool_annotations(title = &quot;Counting subjects&quot;, read_only_hint = TRUE)
)
</pre><p>The annotations give the tool a title, which part 2’s app uses to label each tool call. The arm is an enum, a fixed list of allowed values, so the model sees the three valid arms:</p>
<!-- source: code/R/tools.R, trial_tools() -->
<pre>arm_type &lt;- ellmer::type_enum(ARMS, &quot;Treatment arm. Leave out to cover all arms.&quot;, required = FALSE)
</pre><p><code>check_arm()</code> still rejects anything else, for callers that ignore the list. In all, <code>trial_tools()</code> builds four tools around one copy of the data: <code>list_arms()</code> for arm sizes, <code>ae_summary()</code> for the most common events, <code>count_subjects()</code>, and <code>subject_profile()</code> for one patient’s record.</p>
<h2 id="running-the-loop">Running the loop</h2>
<p>One small function in <code>R/agent.R</code> puts the agent together, creating a Gemini chat with the system prompt (next section) and registering the tools:</p>
<!-- source: code/R/agent.R -->
<pre>make_agent &lt;- function(tools = trial_tools(), prompt_path = &quot;prompts/system.md&quot;,
 model = NULL, chat_fun = ellmer::chat_google_gemini) {
 chat &lt;- chat_fun(system_prompt = read_prompt(prompt_path), model = model)
 chat$register_tools(tools)
 chat
}
</pre><p>With <code>model = NULL</code> you get {ellmer} 0.5.0’s default, <code>gemini-3.7-flash</code>. That default changes between releases, so part 4 pins the model by name.</p>
<p>Switching provider is usually one argument, such as <code>make_agent(chat_fun = ellmer::chat_anthropic)</code>, though local models through <code>chat_ollama()</code> have a catch that part 4 covers.</p>
<p>We asked two questions with <code>echo = &quot;all&quot;</code>, which prints the question, every tool request and every result, as well as the answer. Below are the tool requests and some of the results. We’ve kept the high dose’s <code>ae_summary()</code> result in full, so you can match it to the answer.</p>
<!-- source: code/demo/part-1.R, lines 9-11 -->
<pre>chat &lt;- make_agent()
chat$chat(&quot;Which adverse events were most common on the high dose, compared with placebo?&quot;, echo = &quot;all&quot;)
chat$chat(&quot;How many subjects had a serious adverse event, and on which arms?&quot;, echo = &quot;all&quot;)
</pre>
<div class="conversation-turn conversation-turn--pedro conversation-turn--right" data-speaker="Pedro">
<div class="conversation-speaker">Pedro</div>
<div class="conversation-body">Which adverse events were most common on the high dose, compared with placebo?</div>
</div>
<!-- source: code/demo/output/part-1-gemini-3.7-flash.txt, lines 3-7 and 25-32 -->
<pre>&lt; [tool request (ae_summary)]: ae_summary(arm = &quot;Xanomeline High Dose&quot;, by =
&lt; &quot;term&quot;, top_n = 10L)
&lt; [tool request (ae_summary)]: ae_summary(arm = &quot;Placebo&quot;, by = &quot;term&quot;, top_n =
&lt; 10L)
[tool result (ae_summary)]: {&quot;arm&quot;:&quot;Xanomeline High Dose&quot;,&quot;serious_only&quot;:false,&quot;subjects_in_arm&quot;:72,&quot;by&quot;:&quot;term&quot;,&quot;rows&quot;:[{&quot;event&quot;:&quot;PRURITUS&quot;,&quot;subjects&quot;:25,&quot;percent&quot;:34.7},{&quot;event&quot;:&quot;APPLICATION SITE PRURITUS&quot;,&quot;subjects&quot;:21,&quot;percent&quot;:29.2},{&quot;event&quot;:&quot;APPLICATION SITE ERYTHEMA&quot;,&quot;subjects&quot;:14,&quot;percent&quot;:19.4},{&quot;event&quot;:&quot;ERYTHEMA&quot;,&quot;subjects&quot;:14,&quot;percent&quot;:19.4},{&quot;event&quot;:&quot;DIZZINESS&quot;,&quot;subjects&quot;:10,&quot;percent&quot;:13.9},{&quot;event&quot;:&quot;APPLICATION SITE IRRITATION&quot;,&quot;subjects&quot;:9,&quot;percent&quot;:12.5},{&quot;event&quot;:&quot;HYPERHIDROSIS&quot;,&quot;subjects&quot;:8,&quot;percent&quot;:11.1},{&quot;event&quot;:&quot;RASH&quot;,&quot;subjects&quot;:8,&quot;percent&quot;:11.1},{&quot;event&quot;:&quot;SINUS BRADYCARDIA&quot;,&quot;subjects&quot;:8,&quot;percent&quot;:11.1},{&quot;event&quot;:&quot;APPLICATION SITE DERMATITIS&quot;,&quot;subjects&quot;:7,&quot;percent&quot;:9.7}]}
&lt; [tool request (count_subjects)]: count_subjects(arm = &quot;Placebo&quot;, term =
&lt; &quot;DIZZINESS&quot;)
&lt; [tool request (count_subjects)]: count_subjects(arm = &quot;Placebo&quot;, term =
&lt; &quot;APPLICATION SITE IRRITATION&quot;)
&lt; [tool request (count_subjects)]: count_subjects(arm = &quot;Placebo&quot;, term =
&lt; &quot;HYPERHIDROSIS&quot;)
&lt; [tool request (count_subjects)]: count_subjects(arm = &quot;Placebo&quot;, term =
&lt; &quot;SINUS BRADYCARDIA&quot;)
</pre>
<div class="conversation-turn conversation-turn--assistant conversation-turn--left" data-speaker="Assistant">
<div class="conversation-speaker">Assistant</div>
<div class="conversation-body"><p>The most common adverse events in the <strong>Xanomeline High Dose</strong> arm (N=72) compared to the <strong>Placebo</strong> arm (N=86) were:</p>
<ul>
<li><strong>Pruritus</strong>: 25 subjects (34.7%) on Xanomeline High Dose vs. 8 subjects (9.3%) on Placebo</li>
<li><strong>Application site pruritus</strong>: 21 subjects (29.2%) on Xanomeline High Dose vs. 6 subjects (7.0%) on Placebo</li>
<li><strong>Application site erythema</strong>: 14 subjects (19.4%) on Xanomeline High Dose vs. 3 subjects (3.5%) on Placebo</li>
<li><strong>Erythema</strong>: 14 subjects (19.4%) on Xanomeline High Dose vs. 8 subjects (9.3%) on Placebo</li>
<li><strong>Dizziness</strong>: 10 subjects (13.9%) on Xanomeline High Dose vs. 2 subjects (2.3%) on Placebo</li>
<li><strong>Application site irritation</strong>: 9 subjects (12.5%) on Xanomeline High Dose vs. 3 subjects (3.5%) on Placebo</li>
<li><strong>Hyperhidrosis</strong>: 8 subjects (11.1%) on Xanomeline High Dose vs. 2 subjects (2.3%) on Placebo</li>
<li><strong>Rash</strong>: 8 subjects (11.1%) on Xanomeline High Dose vs. 5 subjects (5.8%) on Placebo</li>
<li><strong>Sinus bradycardia</strong>: 8 subjects (11.1%) on Xanomeline High Dose vs. 2 subjects (2.3%) on Placebo</li>
<li><strong>Application site dermatitis</strong>: 7 subjects (9.7%) on Xanomeline High Dose vs. 5 subjects (5.8%) on Placebo</li>
</ul></div>
</div>
<div class="conversation-turn conversation-turn--pedro conversation-turn--right" data-speaker="Pedro">
<div class="conversation-speaker">Pedro</div>
<div class="conversation-body">How many subjects had a serious adverse event, and on which arms?</div>
</div>
<!-- source: code/demo/output/part-1-gemini-3.7-flash.txt, lines 71-81 -->
<pre>&lt; [tool request (count_subjects)]: count_subjects(serious_only = TRUE)
&lt; [tool request (count_subjects)]: count_subjects(arm = &quot;Placebo&quot;, serious_only
&lt; = TRUE)
&lt; [tool request (count_subjects)]: count_subjects(arm = &quot;Xanomeline Low Dose&quot;,
&lt; serious_only = TRUE)
&lt; [tool request (count_subjects)]: count_subjects(arm = &quot;Xanomeline High Dose&quot;,
&lt; serious_only = TRUE)
[tool result (count_subjects)]: {&quot;arm&quot;:&quot;All arms&quot;,&quot;filters&quot;:{&quot;serious_only&quot;:true,&quot;term&quot;:&quot;any&quot;,&quot;body_system&quot;:&quot;any&quot;},&quot;subjects_with_event&quot;:3,&quot;subjects_in_arm&quot;:254,&quot;percent&quot;:1.2}
[tool result (count_subjects)]: {&quot;arm&quot;:&quot;Placebo&quot;,&quot;filters&quot;:{&quot;serious_only&quot;:true,&quot;term&quot;:&quot;any&quot;,&quot;body_system&quot;:&quot;any&quot;},&quot;subjects_with_event&quot;:0,&quot;subjects_in_arm&quot;:86,&quot;percent&quot;:0}
[tool result (count_subjects)]: {&quot;arm&quot;:&quot;Xanomeline Low Dose&quot;,&quot;filters&quot;:{&quot;serious_only&quot;:true,&quot;term&quot;:&quot;any&quot;,&quot;body_system&quot;:&quot;any&quot;},&quot;subjects_with_event&quot;:2,&quot;subjects_in_arm&quot;:96,&quot;percent&quot;:2.1}
[tool result (count_subjects)]: {&quot;arm&quot;:&quot;Xanomeline High Dose&quot;,&quot;filters&quot;:{&quot;serious_only&quot;:true,&quot;term&quot;:&quot;any&quot;,&quot;body_system&quot;:&quot;any&quot;},&quot;subjects_with_event&quot;:1,&quot;subjects_in_arm&quot;:72,&quot;percent&quot;:1.4}
</pre>
<div class="conversation-turn conversation-turn--assistant conversation-turn--left" data-speaker="Assistant">
<div class="conversation-speaker">Assistant</div>
<div class="conversation-body"><p>Across all arms, <strong>3 subjects (1.2%)</strong> out of 254 had a serious adverse event:</p>
<ul>
<li><strong>Placebo:</strong> 0 subjects (0.0%) of 86</li>
<li><strong>Xanomeline Low Dose:</strong> 2 subjects (2.1%) of 96</li>
<li><strong>Xanomeline High Dose:</strong> 1 subject (1.4%) of 72</li>
</ul></div>
</div>
<p>We never told the model which tools to use. For the first question, it called <code>ae_summary()</code> once per arm for the top ten events. Four of the high dose’s ten weren’t in placebo’s, so it counted those on placebo with <code>count_subjects()</code> before it answered. For the second, it called <code>count_subjects()</code> with <code>serious_only = TRUE</code> for all arms together, then for each arm. Every number in both answers appears in a tool result.</p>
<p>To run all of this yourself, <code>source(&quot;demo/part-1.R&quot;)</code> asks the no-tools question and both agent questions, which takes about six requests. The offline tests run with <code>source(&quot;tests/run.R&quot;)</code> and make no API calls.</p>
<h2 id="the-system-prompt">The system prompt</h2>
<p>The system prompt sets the rules for every conversation:</p>
<!-- source: code/prompts/system.md -->
<pre>You answer questions about the safety data of one clinical trial: CDISC pilot study 01, which compared a xanomeline skin patch at two doses with placebo.

- Use the tools for every fact about the trial. Every number in your answer must appear in a tool result from this conversation. Never calculate, estimate or remember numbers yourself.
- Quote numbers exactly as the tools give them.
- Say which arm each number is for. Counts are subjects in the safety population, by the treatment they actually received, with treatment-emergent adverse events.
- If the tools cannot answer a question, say so.
- Keep answers short.
</pre><p>Two lines do most of the work. The first, that every number must come from a tool, is the rule the rest of the series tests. The third makes the model name the arm and population, so a reader always has a denominator to check.</p>
<p>A prompt is still only a request, and a model can ignore it. That’s why part 3 adds a check on where each number came from.</p>
<h2 id="why-narrow-tools">Why narrow tools</h2>
<p>You could give the model a single <code>run_r_code()</code> tool and let it answer anything. But then you’d be validating whatever code it happened to write that day.</p>
<p>A narrow tool can be tested against numbers checked by hand, logged, and allowed or refused on its own. It’s easier for the model too, since picking the right tool is a smaller job than writing correct {dplyr} code.</p>
<h2 id="what-could-still-go-wrong">What could still go wrong</h2>
<p>The agent works, but it isn’t ready for a team yet:</p>
<ul>
<li><strong>The model invents or rounds a number anyway.</strong> The prompt only asks it not to. Part 3 checks every answer’s numbers against the tool results.</li>
<li><strong>Patient-level data.</strong> Right now the model can open any patient’s record. Part 3 limits <code>subject_profile()</code> to a reviewer group, whose members confirm each record before it opens, and part 5 leaves it out of the tools shared with other AI agents.</li>
<li><strong>Many users at once.</strong> A console session suits one person. In part 2’s Shiny app, each person gets their own conversation.</li>
<li><strong>Cost and outages.</strong> Within minutes, the free tier sent us “503 high demand” and “429 quota exceeded” errors. It’s small, too: when we wrote this, about 20 requests a day per model, and a question takes one request per round of tool calls. {ellmer} tries each request up to three times by default, and <code>options(ellmer_max_tries = …)</code> changes that. Parts 2 and 4 deal with the rest.</li>
</ul>
<aside>
<h3 id="a-note-for-pharma-readers">A note for pharma readers</h3>
<p>Our counts follow {pharmaverseadam}, which rebuilds the pilot’s analysis datasets from its SDTM data. A formal analysis plan might settle three of its choices differently:</p>
<ul>
<li><strong>Treatment received.</strong> {pharmaverseadam} takes the arm each subject received from the SDTM data, which moves the twelve early stoppers to the low dose, and its <code>TRT01A</code> matches <code>ACTARM</code>. The pilot’s own analysis datasets kept them on the high dose (<code>TRT01A</code> equals <code>TRT01P</code>, giving 86, 84 and 84), so the second no-tools answer’s 25 of 84 may well be the original study’s figure.</li>
<li><strong>Serious events.</strong> Like the pilot’s own analysis, our tools count the <code>AESER</code> flag, which is set on only three treatment-emergent events. Another 33 meet a seriousness criterion with <code>AESER = &quot;N&quot;</code>, including three deaths, two of them on placebo. A study team would query that with data management.</li>
<li><strong>Rounding.</strong> R’s <code>round()</code> sends an exact half to the even digit, so 6 of 96 low-dose subjects with skin irritation (6.25%) shows as 6.2%, where SAS would print 6.3%. That happens once in this data.</li>
</ul>
</aside>
<aside>
<h3 id="in-python">In Python</h3>
<p>In chatlas (<code>pip install &quot;chatlas[google]&quot;</code>), a function’s type hints and docstring define the tool, and a <code>Literal</code> type makes the arm an enum:</p>
<pre>from typing import Literal

from chatlas import ChatGoogle

def count_subjects(
 arm: Literal[&quot;Placebo&quot;, &quot;Xanomeline Low Dose&quot;, &quot;Xanomeline High Dose&quot;] | None = None,
 serious_only: bool = False,
 term: str | None = None,
) -&gt; str:
 &quot;&quot;&quot;Count subjects in the safety population with at least one
 treatment-emergent adverse event matching the filters.&quot;&quot;&quot;
 ... # the same counting, with polars or pandas

chat = ChatGoogle(model=&quot;gemini-3.7-flash&quot;, system_prompt=open(&quot;prompts/system.md&quot;).read())
chat.register_tool(count_subjects)
chat.chat(&quot;Which adverse events were most common on the high dose?&quot;)
</pre></aside>
<h2 id="summary">Summary</h2>
<p>An agent is a model, some tools and a loop. The model decides what to count, R does the counting on carefully filtered data, and the prompt asks the model to quote only what the tools return.</p>
<p>Without tools, it gave us two confident answers that disagreed, and neither matched our data. With tools, every number came from R.</p>
<p>Next time, we put the agent in a Shiny app, so that several people can use it at once.</p>
<p>All the code for this part, including <code>demo/part-1.R</code> and the tests, is in <a href="https://github.com/jumpingrivers/blog/tree/main/blogs/agents-in-production/part-1" rel="nofollow" target="_blank">our blog repository</a>.</p>
<p>Building something like this? Tell us about it at <a href="https://ai-in-production.jumpingrivers.com/" rel="nofollow" target="_blank">AI in Production 2027</a>, where the call for talks is open until 29 January 2027. For a guided tour of LLMs from R and Python, see our <a href="https://www.jumpingrivers.com/training/course/llm-applications-r-python-shiny-rag-ai/" rel="nofollow" target="_blank">LLM-Driven Applications with R and Python</a> course.</p>
<p>
For updates and revisions to this article, see the <a href = "https://www.jumpingrivers.com/blog/agents-in-production-first-agent/">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/agents-in-production-first-agent/"> 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/11/agents-in-production-build-your-first-ai-agent-in-r-with-ellmer/">Agents in Production: Build Your First AI Agent in R with {ellmer}</a>]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">404608</post-id>	</item>
		<item>
		<title>Carolina Pradier, rOpenSci champion and mentor</title>
		<link>https://www.r-bloggers.com/2026/11/carolina-pradier-ropensci-champion-and-mentor/</link>
		
		<dc:creator><![CDATA[rOpenSci]]></dc:creator>
		<pubDate>Mon, 02 Nov 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://ropensci.org/blog/2026/11/02/champions-histories-carolina-pradier/</guid>

					<description><![CDATA[<p>We started this article as an interview series with Champions from our program, and we decided to publish for our 15th anniversary series. This is the interview with Carolina Pradier, main maintainer of the eph package.<br />
Carolina Pradier has long worke...</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/11/carolina-pradier-ropensci-champion-and-mentor/">Carolina Pradier, rOpenSci champion and mentor</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/11/02/champions-histories-carolina-pradier/"> 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>We started this article as an interview series with Champions from our program, and we decided to publish for our 15th anniversary series. This is the interview with <a href="https://ropensci.org/es/author/carolina-pradier/" rel="nofollow" target="_blank">Carolina Pradier</a>, main maintainer of the <a href="https://docs.ropensci.org/eph/" rel="nofollow" target="_blank">eph package</a>.</p>
<p><a href="https://ropensci.org/author/carolina-pradier/" rel="nofollow" target="_blank">Carolina Pradier</a> has long worked to make the world a more equitable place. Her doctoral research focused on inequalities in science with a focus on the mechanisms that structure gender inequality in the Latin American scientific community. <sup id="fnref:1"><a href="https://ropensci.org/blog/2026/11/02/champions-histories-carolina-pradier/#fn:1" class="footnote-ref" role="doc-noteref" rel="nofollow" target="_blank">1</a></sup> She is also part of <a href="https://ecofeminita.com/" rel="nofollow" target="_blank">Ecofeminita</a> (Asociación Civil Economía Feminista), an interdisciplinary organization created and led by women.
Carolina has a degree in Economics from the University of Buenos Aires and a holds a Master’s degree in Social Sciences of Work from the Faculty of Social Sciences of the University of Buenos Aires. She is a PhD candidate in Information Science from the University of Montreal and teaches at post-secondary institutions.</p>
<p>As a researcher, Carolina was part of the Center for Studies on Population, Employment and Development and now works for the <a href="https://www.unesco.org/en/open-science" rel="nofollow" target="_blank">UNESCO Chair on Open Science</a>. She also is the maintainer of the <a href="https://docs.ropensci.org/eph/" rel="nofollow" target="_blank">eph</a> R package, whose objective is to facilitate working with the Permanent Household Survey data.</p>
<figure><img src="https://i2.wp.com/ropensci.org/blog/2026/11/02/champions-histories-carolina-pradier/eph-docs.png?w=578&#038;ssl=1"
alt="Official documentation of the eph package with the rOpenSci template" data-recalc-dims="1"><figcaption>
<p>Official documentation of the eph package</p>
</figcaption>
</figure>
<h2>
rOpenSci
</h2><p>Carolina’s encounter with the rOpenSci Champions Program seemed natural. Her goals and those of the organization were aligned. In her application to the Champions Program she expressed:</p>
<blockquote>
<p>I would like to be part of the rOpenSci Champions Program to improve my skills as a developer, as well as to improve my abilities to teach and communicate these skills.&#8221;</p>
</blockquote>
<p>Next followed over 12 months of training. Through the coaching, mentoring, and exposure to colleagues, Carolina received information on tools and processes, as well as advice and feedback to improve her package. She was also able to develop her communication skills and feel more confident in communicating more effectively with the people who use eph.</p>
<p>Exchanges and meeting with other people made her work easier, and helped her stay organized and anticipate future problems she might encounter:</p>
<blockquote>
<p>I think I felt great when we finished the training and I started to work closely with my mentor, we had a good relationship and we could work very well and efficiently.&#8221;</p>
</blockquote>
<p>In addition to being selected to participate in the rOpenSci Champions Program through the rOpenSci application, Carolina had already gone through a previous selection. The rOpenSci Champions Program accepts only individual applications. Therefore, when Carolina’s team wished to participate, her colleagues encouraged her to participate on behalf of the team.</p>
<p>During the program, Carolina developed different events to help disseminate her work, share it with others, and invite them to get involved in open science. This idea was also present in the objectives that Carolina expressed in her first contact with rOpenSci:</p>
<blockquote>
<p>One of my main motivations is to bring new tools to the communities of social scientists in which I actively participate in order to strengthen our capacities to transform reality.&#8221;</p>
</blockquote>
<p>It was with this goal that Carolina <a href="https://vimeo.com/899372049" rel="nofollow" target="_blank">organized an event in Spanish</a> with news and ideas for the use of the eph package. This achieved a double objective: sharing content about R packages in Spanish and bringing more people closer to the R and open science community.</p>
<p>Carolina also participated in a <a href="https://ropensci.org/commcalls/july2023-championprogram/" rel="nofollow" target="_blank">Community Call on the rOpenSci Champions Program</a> where she shared her experiences as a champion, co-hosted a co-working session about <a href="https://ropensci.org/events/coworking-2024-08/" rel="nofollow" target="_blank">Building your first R package</a>, and joined as a <a href="https://ropensci.org/blog/2025/06/05/mentors-2025/" rel="nofollow" target="_blank">mentor for the cohort 2025-2026</a> of the Champions Program.</p>
<h2>
Perfecting a job well done
</h2><p>The <a href="https://github.com/ropensci/eph/" rel="nofollow" target="_blank">eph</a> package processes data from the Permanent Household Survey (<a href="https://www.indec.gob.ar/" rel="nofollow" target="_blank">INDEC</a>) in Argentina to replicate or conduct independent analyses from those published in the reports prepared by the government agency. It also allows the analysis of different categories and indicators not provided in the official reports.
Carolina explained that throughout the program they managed to improve the package:</p>
<blockquote>
<p>“We tried to improve the quality of the code (reliability and efficiency), we improved the documentation and created new functions.</p>
</blockquote>
<p>They also worked hard to translate the documentation and make it more accessible to Spanish-speaking users.
The <a href="https://github.com/ropensci/software-review/issues/593#issuecomment-1709968472" rel="nofollow" target="_blank">rOpenSci peer review process</a> showed how a rigorous approach can also be welcoming and kind.</p>
<figure><img src="https://i2.wp.com/ropensci.org/blog/2026/11/02/champions-histories-carolina-pradier/revision.png?w=578&#038;ssl=1"
alt="Comment on the eph review issue: First of all, congratulations on the package. It is an extremely useful package for the community and one of the most important for those working in public policy. It is also an example for other government agencies of how data can be made available to serve the community. It is also a nice example of how a project can be improved over time and not only sustains itself but is permanently growing." data-recalc-dims="1"><figcaption>
<p>Open, transparent and friendly commentary on the rOpenSci peer review of the eph package.</p>
</figcaption>
</figure>
<h2>
Open Science
</h2><p>The eph package is a tool for academic research, journalism and evidence-based public policy formulation. The Champions Program allowed Carolina to improve the software using different points of view from peer and expert review.</p>
<p>Beyond improving the package itself, one of Carolina’s goals was to help increase the impact of her community’s work:</p>
<blockquote>
<p>I am particularly excited about the prospect of participating in a program aimed at making the R community more inclusive. in addition, I feel that this experience would be a great opportunity as it would allow me to meet and work with highly skilled professionals whose work I deeply admire. Not only am I interested in learning from their experience, but I also believe that building these networks can help increase the impact of my community’s efforts and access new sources of support.”</p>
</blockquote>
<p>Some existing uses of eph show the kind of impact Carolina hopes the package can have. At EcoFeminita, the civil association Carolina is part of, the Ecofemidata team uses eph to provide data on gender inequality through indicators, statistics, and reproducible thematic reports. Among other uses, the team maintains an app that analyzes the labor market, income, time use, and domestic work.</p>
<p>The package has also inspired programmers, data scientists, and people from other disciplines to develop the <a href="https://pyeph.readthedocs.io/es/latest/" rel="nofollow" target="_blank">PyEPH library</a>, which has a similar purpose to the eph package in R but is used and supported by people in the Python data community. As eph keeps improving and the network around it grows, Carolina hopes to see more experiences like these.</p>
<h2>
Beyond learning: building with a community
</h2><p>Carolina Pradier was able to improve her R skills, acquire new skills, and offer a multilingual package so that more people can access open science. Experiences with her mentor, her peers, the people who participated in the review process, and the people who use the package were a great contribution to learning and personal growth. Her participation in the Champions Program was the beginning of a network of links that promise new projects.</p>
<blockquote>
<p>I always had someone to talk to to help me solve problems; it was never a frustrating process.&#8221;</p>
</blockquote>
<p>Carolina is now handing over maintenance of eph to a new maintainer. The package is used by many people and has a community around it, which is what makes it sustainable.</p>
<p>Shared knowledge allows us to anticipate, help and include. The more experiences like Carolina Pradier’s there are, the more shared and open open science there will be.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Three of the papers from Carolina’s doctoral thesis have already been published:
<a href="https://doi.org/10.1002/asi.24972" rel="nofollow" target="_blank">Science for whom? The influence of the regional academic circuit on gender inequalities in Latin America</a> (<em>JASIST</em>, 2025);
<a href="https://doi.org/10.1002/asi.70055" rel="nofollow" target="_blank">How multilingual is scholarly communication? Mapping the global distribution of languages in publications and citations</a> (<em>JASIST</em>, 2026);
and <a href="https://doi.org/10.1093/reseval/rvag062" rel="nofollow" target="_blank">Language hierarchies and gender inequalities: the coverage of OpenAlex and the Web of Science</a> (<em>Research Evaluation</em>, 2026). <a href="https://ropensci.org/blog/2026/11/02/champions-histories-carolina-pradier/#fnref:1" class="footnote-backref" role="doc-backlink" rel="nofollow" target="_blank"><img src="https://s.w.org/images/core/emoji/13.0.0/72x72/21a9.png" alt="↩" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎</a></p>
</li>
</ol>
</div>
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://ropensci.org/blog/2026/11/02/champions-histories-carolina-pradier/"> 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/11/carolina-pradier-ropensci-champion-and-mentor/">Carolina Pradier, rOpenSci champion and mentor</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404613</post-id>	</item>
		<item>
		<title>Top-cited scientists in clinical medicine by country: the Stanford–Elsevier top 2% list in R</title>
		<link>https://www.r-bloggers.com/2026/11/top-cited-scientists-in-clinical-medicine-by-country-the-stanford-elsevier-top-2-list-in-r/</link>
		
		<dc:creator><![CDATA[Loess]]></dc:creator>
		<pubDate>Sun, 01 Nov 2026 18:31:43 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://datascienceplus.com/?p=33208</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; "> Clinical medicine is by far the largest field in the 2026 Stanford–Elsevier top 2% list: 76,155 of the 249,310 listed authors, 31%, have it…</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/11/top-cited-scientists-in-clinical-medicine-by-country-the-stanford-elsevier-top-2-list-in-r/">Top-cited scientists in clinical medicine by country: the Stanford–Elsevier top 2% list in 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://datascienceplus.com/top-cited-scientists-in-clinical-medicine-by-country-the-stanford-elsevier-top-2-list-in-r/"> R Programming – DataScience+</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>Clinical medicine is by far the largest field in the 2026 Stanford–Elsevier top 2% list: 76,155 of the 249,310 listed authors, 31%, have it as their main field, more than the next three of the 20 fields combined. I wanted to know where those scientists work, and whether the country ranking still looks the same once you ask how many people each country has.</p>
<p>The data is John Ioannidis’s <a href="https://elsevier.digitalcommonsdata.com/datasets/btchxktzyw/9" rel="nofollow" target="_blank">science-wide author database</a>, built with Elsevier from Scopus (Ioannidis, J.P.A. (2026), &#8220;August 2026 data-update for ‘Updated science-wide author databases of standardized citation indicators’&#8221;, Mendeley Data, v9, doi:10.17632/btchxktzyw.9, licensed CC BY-NC 3.0). An author is listed if they rank in the top 100,000 by a composite citation score or in the top 2% of their main subfield. I use the single-year table, which scores citations received during 2025. Each row carries a main field from the Science-Metrix classification (<code>sm-field</code>) and the country of the author’s most recent institution (<code>cntry</code>).</p>
<p><strong>Find a top-cited scientist or institution:</strong> If you would rather skip the tutorial and find your own name or institution, topcitedscientists.com has the whole 2026 list: <a href="https://topcitedscientists.com/" rel="nofollow" target="_blank">search for a scientist by name</a> or <a href="https://topcitedscientists.com/institutions/" rel="nofollow" target="_blank">see institutions ranked</a> by their number of top-cited scientists.</p>
<h2>How do I count the Stanford–Elsevier top 2% scientists by country in R?</h2>
<p>Download the single-year author table from Mendeley Data, read its <code>Data</code> sheet with readxl, fill the blank countries from the <code>top_cntry_career</code> column, then filter on <code>sm-field</code> and <code>count()</code> the countries. This block runs on its own with readxl 1.5.0 and tidyverse 2.0.0 on R 4.6.1. The file is about 100 MB, so it raises the download timeout first:</p>
<pre>library(tidyverse)
library(readxl)

url &lt;- &quot;https://data.mendeley.com/public-files/datasets/btchxktzyw/files/a517a94c-3fb0-4cee-b97f-f2d476df66f0/file_downloaded&quot;
file &lt;- &quot;Table_1_Authors_singleyr_2025_pubs_since_1788_wopp_extracted_202608.xlsx&quot;

options(timeout = 600)
if (!file.exists(file)) {
  download.file(url, file, mode = &quot;wb&quot;)
}

authors &lt;- read_excel(file, sheet = &quot;Data&quot;) |&gt;
  mutate(
    filled = is.na(cntry),
    cntry = coalesce(cntry, str_to_lower(top_cntry_career))
  )

clinical &lt;- authors |&gt;
  filter(`sm-field` == &quot;Clinical Medicine&quot;) |&gt;
  count(cntry, sort = TRUE) |&gt;
  mutate(share = n / sum(n))

clinical
## # A tibble: 147 × 3
##    cntry     n  share
##    &lt;chr&gt; &lt;int&gt;  &lt;dbl&gt;
##  1 usa   28458 0.374 
##  2 gbr    6303 0.0828
##  3 chn    3924 0.0515
##  4 deu    3770 0.0495
##  5 ita    3710 0.0487
##  6 can    3140 0.0412
##  7 aus    2943 0.0386
##  8 fra    2120 0.0278
##  9 jpn    2074 0.0272
## 10 nld    1955 0.0257
## # &#x2139; 137 more rows
</pre>
<p>Three details in that code matter. <code>download.file()</code> gives up after 60 seconds by default, which a slow connection hits long before 100 MB. The workbook’s first sheet is a column key, so <code>read_excel(file)</code> without <code>sheet =</code> quietly returns its 53 rows of definitions instead of the data. And 169 authors have no <code>cntry</code>, 77 of them in clinical medicine, the most of any field; the dataset’s <code>top_cntry_career</code> column (the country that appears most often on their papers) fills them, and the <code>filled</code> column keeps a record of which rows were changed. That moves the US from 28,424 to 28,458.</p>
<p>The United States has 28,458 of the field’s top-cited scientists, 37.4%. The United Kingdom follows with 6,303, then China (3,924), Germany (3,770) and Italy (3,710). Those five hold 61% of the field, and 146 countries have at least one listed author.</p>
<p><strong>Related posts on DataScience+:</strong></p><ul><li><a href="https://datascienceplus.com/machine-learning-results-one-plot-to-rule-them-all/" rel="nofollow" target="_blank">Machine Learning Results in R: one plot to rule them all! (Part 1 – Classification Models)</a></li><li><a href="https://datascienceplus.com/visualize-your-portfolios-performance-and-generate-a-nice-report-with-r/" rel="nofollow" target="_blank">Visualize your Portfolio’s Performance and Generate a Nice Report with R</a></li><li><a href="https://datascienceplus.com/creating-slopegraphs-with-r/" rel="nofollow" target="_blank">Creating Slopegraphs with R</a></li></ul><h2>Per resident, Switzerland and Denmark lead</h2>
<p>A raw count mostly measures how big a country’s research system is. Dividing by population asks a different question: how dense the country’s pool of highly cited clinicians and medical researchers is. I take 2024 population from the World Bank’s API (indicator <code>SP.POP.TOTL</code>, keyless JSON) and keep countries with at least a million residents:</p>
<pre>wb &lt;- jsonlite::fromJSON(paste0(
  &quot;https://api.worldbank.org/v2/country/all/indicator/&quot;,
  &quot;SP.POP.TOTL?date=2024&#038;format=json&#038;per_page=400&quot;
))

population &lt;- wb[[2]] |&gt;
  as_tibble() |&gt;
  transmute(
    cntry = str_to_lower(countryiso3code),
    country = country$value,
    pop = value
  ) |&gt;
  filter(cntry != &quot;&quot;)

per_million &lt;- clinical |&gt;
  inner_join(population, by = &quot;cntry&quot;) |&gt;
  filter(pop &gt;= 1e6) |&gt;
  mutate(per_million = n / pop * 1e6) |&gt;
  arrange(desc(per_million))

per_million |&gt; select(country, n, per_million) |&gt; head(8)
## # A tibble: 8 × 3
##   country            n per_million
##   &lt;chr&gt;          &lt;int&gt;       &lt;dbl&gt;
## 1 Switzerland     1345       149. 
## 2 Denmark          820       137. 
## 3 Netherlands     1955       109. 
## 4 Australia       2943       108. 
## 5 Sweden          1028        97.3
## 6 United Kingdom  6303        91.0
## 7 United States  28458        83.7
## 8 Canada          3140        76.1
</pre>
<p>Switzerland has 149 top-cited clinical medicine scientists per million residents and Denmark 137, against 84 in the United States, which ranks 7th per resident behind the Netherlands, Australia, Sweden and the United Kingdom. China is 2.8 per million and India 0.9. The floor matters: without it, Monaco’s three listed authors would rank it among the leaders. Taiwan (428 authors) drops out of this table because the World Bank does not publish its population.</p>
<p><img decoding="async" src="https://i1.wp.com/datascienceplus.com/wp-content/uploads/2026/11/loess-top-cited-clinical-medicine-fig-countries-1-1.png?w=578&#038;ssl=1" alt="plot of chunk fig-countries" data-recalc-dims="1" /></p>
<h2>A country’s count also reflects its field mix</h2>
<p>China is second in the whole list, with 37,204 listed authors across all fields, yet only third in clinical medicine. The reason is what its top-cited scientists work on:</p>
<pre>field_mix &lt;- authors |&gt;
  summarise(
    listed = n(),
    clinical = mean(`sm-field` == &quot;Clinical Medicine&quot;),
    .by = cntry
  ) |&gt;
  arrange(desc(listed))

field_mix |&gt; head(5)
## # A tibble: 5 × 3
##   cntry listed clinical
##   &lt;chr&gt;  &lt;int&gt;    &lt;dbl&gt;
## 1 usa    73722    0.386
## 2 chn    37204    0.105
## 3 gbr    18320    0.344
## 4 deu    10942    0.345
## 5 aus     8974    0.328
</pre>
<p>Clinical medicine is the main field of 39% of listed US authors and 34% of British ones, against 31% across the whole list and 11% in China, where the largest field is Enabling &#038; Strategic Technologies (8,807 authors). So a field-by-country ranking says as much about where a country’s research effort goes as about how strong it is in that field.</p>
<p>Two caveats travel with every number here. The list counts citations, not quality, and the dataset’s authors warn against reading it as absolute evidence of excellence. And <code>cntry</code> is where an author works now, not their nationality or where they trained.</p>
<hr><p><em>This article was first published on <a href="https://datascienceplus.com/top-cited-scientists-in-clinical-medicine-by-country-the-stanford-elsevier-top-2-list-in-r/" rel="nofollow" target="_blank">DataScience+</a>, a community of R and Python tutorial authors. Have a data-science technique worth sharing? <a href="https://datascienceplus.com/write-for-us/" rel="nofollow" target="_blank">Write for us</a> — no pitch required.</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://datascienceplus.com/top-cited-scientists-in-clinical-medicine-by-country-the-stanford-elsevier-top-2-list-in-r/"> R Programming – DataScience+</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/11/top-cited-scientists-in-clinical-medicine-by-country-the-stanford-elsevier-top-2-list-in-r/">Top-cited scientists in clinical medicine by country: the Stanford–Elsevier top 2% list in R</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404597</post-id>	</item>
		<item>
		<title>Register for the Summer R Workshops in Brisbane, at UQ: Introduction to R and Statistical Modelling</title>
		<link>https://www.r-bloggers.com/2026/11/register-for-the-summer-r-workshops-in-brisbane-at-uq-introduction-to-r-and-statistical-modelling/</link>
		
		<dc:creator><![CDATA[Seascapemodels]]></dc:creator>
		<pubDate>Sun, 01 Nov 2026 13:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://www.seascapemodels.org/posts/2026-11-02-summer-R-workshops-brisbane/</guid>

					<description><![CDATA[<p>Learn the R skills needed to analyse data confidently, build powerful statistical models, create publication-quality figures, and answer complex research questions with the Centre for Biodiversity and Conservation Science (CBCS) 2027 Summer R Wo...</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/11/register-for-the-summer-r-workshops-in-brisbane-at-uq-introduction-to-r-and-statistical-modelling/">Register for the Summer R Workshops in Brisbane, at UQ: Introduction to R and Statistical Modelling</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://www.seascapemodels.org/posts/2026-11-02-summer-R-workshops-brisbane/"> Seascapemodels</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
 





<p>Learn the R skills needed to analyse data confidently, build powerful statistical models, create publication-quality figures, and answer complex research questions with the Centre for Biodiversity and Conservation Science (CBCS) 2027 Summer R Workshops @ UQ.</p>
<p>These hands-on workshops are designed for both beginners and researchers looking to strengthen their analytical skills. You’ll learn how to wrangle data efficiently, develop reproducible workflows, apply appropriate statistical methods, and avoid common analytical pitfalls. By the end of the workshop, you’ll have practical skills that can be immediately applied to your own research projects. Although most of our example datasets are ecological in nature, the approaches and skills are equally applicable in many other research contexts.</p>
<section id="why-learn-to-code-in-the-age-of-ai" class="level2">
<h2 class="anchored" data-anchor-id="why-learn-to-code-in-the-age-of-ai">Why learn to code in the age of AI?</h2>
<p>AI can generate code in seconds, but it cannot tell you whether the analysis is appropriate, the assumptions are valid, or the conclusions are scientifically defensible. Learning the fundamentals of R gives you the skills to critically evaluate code and identify errors, ensuring you remain the expert in the AI loop.</p>
<p>Each day is self-contained so you can attend the workshops you want to.</p>
<ul>
<li><strong>Day 1 (Monday 8th February)</strong> Introduction to R and data wrangling in the tidyverse</li>
<li><strong>Day 2 (Tuesday 9th February)</strong> Introduction to visualisation and linear modelling</li>
<li><strong>Day 3 (Wednesday 10th February)</strong> Generalised linear modelling</li>
<li><strong>Day 4 (Thursday 11th February)</strong> Mixed modelling</li>
<li><strong>Day 5 (Friday 12th February)</strong> Spatial modelling with temporal and spatial autocorrelation</li>
</ul>
<p>Registration for the 2027 Summer R Workshops @ UQ in February is OPEN.</p>
<p>We are passionate users of R and want to share our experience. Our workshops are accessible, interactive, informative and fun. They are run in-person only (no online option) to provide a tailored student experience and we provide all the code and learning materials. For more information on the topics covered and to view testimonials, see our website <a href="https://mathmarecol.github.io/RWorkshop/summer.html" rel="nofollow" target="_blank">https://mathmarecol.github.io/RWorkshop/summer.html</a> and the attached flyer.</p>
<p>These workshops are brought to you by the Centre for Biodiversity and Conservation Science (CBCS). For any questions about registration, email <a href="mailto:cbcs-workshops@uq.edu.au" rel="nofollow" target="_blank">cbcs-workshops@uq.edu.au</a>.</p>
<p>We look forward to seeing you there.</p>
<p>The R Workshops @ UQ team (Anthony, Dave, Jason, and Tin)</p>


</section>

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

		<post-id xmlns="com-wordpress:feed-additions:1">404610</post-id>	</item>
		<item>
		<title>My Messy Notes of Learning MICE From Scratch</title>
		<link>https://www.r-bloggers.com/2026/10/my-messy-notes-of-learning-mice-from-scratch/</link>
		
		<dc:creator><![CDATA[r on Everyday Is A School Day]]></dc:creator>
		<pubDate>Sat, 31 Oct 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://www.kenkoonwong.com/blog/mice/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
MICE always felt like magic to me 🪄 So I tried rebuilding it from scratch in R to understand it better. Lots of mistakes and help along the way, but my version ended up close to the mice package. Might be just luck. Still learning, but here are my mes...</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/my-messy-notes-of-learning-mice-from-scratch/">My Messy Notes of Learning MICE From Scratch</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.kenkoonwong.com/blog/mice/"> r on Everyday Is A School Day</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
<blockquote>
<p>MICE always felt like magic to me <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1fa84.png" alt="🪄" class="wp-smiley" style="height: 1em; max-height: 1em;" /> So I tried rebuilding it from scratch in R to understand it better. Lots of mistakes and help along the way, but my version ended up close to the mice package. Might be just luck. Still learning, but here are my messy notes! <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f383.png" alt="🎃" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f4ca.png" alt="📊" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
</blockquote>
<p align="center">
  <img loading="lazy" src="https://i0.wp.com/www.kenkoonwong.com/blog/mice/mice.png?w=80%25&#038;ssl=1" alt="image" height="auto" data-recalc-dims="1">
</p>




<h2 id="motivations">Motivations
  <a href="https://www.kenkoonwong.com/blog/mice/#motivations" rel="nofollow" target="_blank"><svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
      <path d="M0 0h24v24H0z" fill="currentColor"></path>
      <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path>
    </svg></a>
</h2>
<p>We used mulitple imputation by chained equations (MICE) quite often in imputing missing data in many occasion, but I never really understood the mechanism behind it except that it appears magical! <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f9d9.png" alt="🧙" class="wp-smiley" style="height: 1em; max-height: 1em;" /> I want to see if I can build this from scratch as a mean to understand the foundation of MICE so that at least it will give me the illusion of understanding. <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f923.png" alt="🤣" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Join me in my messy journey of trying to code it from the ground up (FYI, I’m not coding OLS or MLE from the ground up…), and let’s see if we can create some “magic” here! This is going to be a bumpy one with lots of coding, and maybe less of mathematical formulae. I’ll try my best to provide a TL;DR for each section so that we can get a high level view as well. Buckle up! And oh, Happy Halloween! <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f383.png" alt="🎃" class="wp-smiley" style="height: 1em; max-height: 1em;" /> What a better time to look at magical recipes of statistics! Let’s go!</p>




<h2 id="objectives">Objectives:
  <a href="https://www.kenkoonwong.com/blog/mice/#objectives" rel="nofollow" target="_blank"><svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
      <path d="M0 0h24v24H0z" fill="currentColor"></path>
      <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path>
    </svg></a>
</h2>
<ul>
<li>
<a href="https://www.kenkoonwong.com/blog/mice/#mice" rel="nofollow" target="_blank">What Is MICE</a></li>
<li>
<a href="https://www.kenkoonwong.com/blog/mice/#sim" rel="nofollow" target="_blank">Simulate Our DGP</a></li>
<li>
<a href="https://www.kenkoonwong.com/blog/mice/#procedure" rel="nofollow" target="_blank">The Procedure To Impute And Create Multiple Datasets</a>
<ul>
<li>
<a href="https://www.kenkoonwong.com/blog/mice/#pool" rel="nofollow" target="_blank">Pool them!</a></li>
</ul>
</li>
<li>
<a href="https://www.kenkoonwong.com/blog/mice/#opportunities" rel="nofollow" target="_blank">Opportunities For Improvement</a></li>
<li>
<a href="https://www.kenkoonwong.com/blog/mice/#lessons" rel="nofollow" target="_blank">Lessons Learnt</a></li>
</ul>




<h2 id="mice">What Is MICE
  <a href="https://www.kenkoonwong.com/blog/mice/#mice" rel="nofollow" target="_blank"><svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
      <path d="M0 0h24v24H0z" fill="currentColor"></path>
      <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path>
    </svg></a>
</h2>
<p>TL;DR: fill in each gap several times with plausible random guesses, analyse each completed dataset, then combine the results.</p>
<p>When data are missing, the tempting fix is to plug in one best guess, like the mean. The problem is that a single guess pretends we know the missing value exactly, so our standard errors come out too small. MICE takes a different approach: it fills in each missing value with a plausible random draw, and does this several times to create multiple complete datasets. The differences between those datasets capture our uncertainty about what the missing values really were.</p>
<p>The “chained equations” part is how those draws are made. Instead of one giant model for all variables at once, MICE fits a simple regression for each incomplete variable, using all the others as predictors, and cycles through them, each one feeding its freshly imputed values into the next. After a few rounds, the imputations settle down. Finally, we analyse each completed dataset and pool the results with Rubin’s rules. <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/2728.png" alt="✨" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>




<h2 id="sim">Simulate Our DGP
  <a href="https://www.kenkoonwong.com/blog/mice/#sim" rel="nofollow" target="_blank"><svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
      <path d="M0 0h24v24H0z" fill="currentColor"></path>
      <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path>
    </svg></a>
</h2>
<p>Alright let’s simulate our data generating process (the truth) and add in 
<a href="https://www.kenkoonwong.com/blog/missing/" rel="nofollow" target="_blank">missing data mechanism</a> to create our dataset that we want to impute from (imagine this is the dataset that we have and we want to apply mice on it).</p>
<pre>library(tidyverse)
library(broom)

set.seed(1)
n &lt;- 1000
p1 &lt;- rnorm(n)
p2 &lt;- rnorm(n, 0.5*p1)
p3 &lt;- rnorm(n, 0.4*p1 + 0.3*p2)
p4 &lt;- rbinom(n, 1, 0.5)
p5 &lt;- rbinom(n, 1, plogis(-0.5 + 0.3*p1 - 0.2*p2 + 0.1*p3))
outcome &lt;- rbinom(n, 1, plogis(-0.5 + 0.3*p1 - 0.2*p2 + 0.1*p3 + 0.5*p4 + 0.4*p5))

df &lt;- tibble(p1,p2,p3,p4,p5,outcome)

# missing mechanism 
## mcar
p1_miss &lt;- ifelse(runif(n) &lt; 0.1, NA, p1)
p3_miss &lt;- ifelse(runif(n) &lt; 0.2, NA, p3)

## mar 
p2_miss &lt;- ifelse(runif(n) &lt; plogis(-0.5 + 0.3*p1), NA, p2)
p4_miss &lt;- ifelse(runif(n) &lt; plogis(-0.5 + 0.3*p1 - 0.2*p2), NA, p4)

## mnar
p5_miss &lt;- ifelse(runif(n) &lt; plogis(-0.5 + 0.8*p5), NA, p5)

## recombine
df_miss &lt;- tibble(p1 = p1_miss, p2 = p2_miss, p3 = p3_miss, p4 = p4_miss, p5 = p5_miss, outcome = outcome)
</pre>



<h2 id="procedure">The Procedure To Impute And Create Multiple Datasets
  <a href="https://www.kenkoonwong.com/blog/mice/#procedure" rel="nofollow" target="_blank"><svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
      <path d="M0 0h24v24H0z" fill="currentColor"></path>
      <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path>
    </svg></a>
</h2>
<p>Alright, let’s go through the procedure for MICE real quick.</p>
<ol>
<li>
<a href="https://www.kenkoonwong.com/blog/mice/#idx" rel="nofollow" target="_blank">Index all missing values and record them in a dataframe.</a></li>
<li>For each chain:</li>
</ol>
<ul>
<li>
<a href="https://www.kenkoonwong.com/blog/mice/#fill" rel="nofollow" target="_blank">Fill every missing value with a random starting value.</a></li>
<li>For each column with missing data:
<ul>
<li>Bootstrap the rows where that column is observed, using the current filled-in values for the other columns.</li>
<li>Fit a model with that column as the response and all other columns, including the outcome, as predictors: linear regression for continuous columns, logistic for binary.</li>
<li>Predict the missing values, then add noise. For continuous columns, add a normal draw using the model’s residual SD. For binary columns, draw 0 or 1 from the predicted probability.</li>
<li>Write the new values back immediately, so the next column’s model uses them. This is the “chained” part.</li>
</ul>
</li>
<li>Repeat the column loop for 10 iterations.</li>
<li>Keep the final imputed values from the 10th iteration.</li>
</ul>
<ol start="3">
<li>Repeat step 2 for 10 chains.</li>
<li>Build 10 completed datasets, one per chain.</li>
<li>
<a href="https://www.kenkoonwong.com/blog/mice/#fitall" rel="nofollow" target="_blank">Fit the analysis model on each dataset.</a></li>
<li>
<a href="https://www.kenkoonwong.com/blog/mice/#pool" rel="nofollow" target="_blank">Pool by averaging the coefficients, plus Rubin’s rules if you need standard errors.</a></li>
</ol>
<p>Phew. What a procedure! At least we now know the machinery behind MICE!</p>




<h3 id="idx">Index all missing values and record them in a dataframe
  <a href="https://www.kenkoonwong.com/blog/mice/#idx" rel="nofollow" target="_blank"><svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
      <path d="M0 0h24v24H0z" fill="currentColor"></path>
      <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path>
    </svg></a>
</h3>
<pre>## procedure of simple mice
### 1. index all missing values
df_miss_idx &lt;- df_miss |&gt; is.na()

#### create an empty dataframe based on number of missing data
empty_row &lt;- df_miss_idx |&gt; as.vector() |&gt; sum()

empty_df &lt;- which(df_miss_idx, arr.ind = T) |&gt; as_tibble()

data_type &lt;- df_miss |&gt; 
  map_chr(~class(.x)[1]) 

data_type_colnum &lt;- 1:length(data_type)
colname &lt;- colnames(df_miss)

data_type_df &lt;- tibble(colnum = data_type_colnum, dtype=data_type, colname = colname)

empty_df &lt;- empty_df |&gt;
  left_join(data_type_df, by = c(&quot;col&quot; = &quot;colnum&quot;)) |&gt;
  mutate(value = NA)
</pre><p>Notice that we added datatype to help us identify continuous data vs binary data?</p>




<h3 id="fill">Fill in missing values with random starting values
  <a href="https://www.kenkoonwong.com/blog/mice/#fill" rel="nofollow" target="_blank"><svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
      <path d="M0 0h24v24H0z" fill="currentColor"></path>
      <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path>
    </svg></a>
</h3>
<p>Let’s create a an empty dataframe then write functions to do these so we can use them on our for loop</p>
<pre>### 2. First iteration, first chain
### 2a. randomly impute all values, use conditions per column to assess types of data before random normal, binomial distribution
fill_empty_df &lt;- function(empty_df) { 
  df &lt;- empty_df |&gt;
  mutate(value = case_when(
    dtype == &quot;numeric&quot; ~ rnorm(n()),
    dtype == &quot;integer&quot; ~ rbinom(n(),1,0.5),
    TRUE ~ NA_real_
  )) |&gt;
  mutate(model = case_when(
    dtype == &quot;numeric&quot; ~ &quot;linreg&quot;,
    dtype == &quot;integer&quot; ~ &quot;logreg&quot;,
    TRUE ~ NA_character_
  ))
  
  return(df)
}

grab_xvec &lt;- function(df, outcome) {
  df |&gt; 
    select(-all_of(outcome)) |&gt;
    colnames()
}

gen_formula &lt;- function(df, outcome) {
  x_vec &lt;- grab_xvec(df, outcome)
  x &lt;- paste(x_vec,collapse = &quot;+&quot;)
  formula &lt;- paste0(outcome,&quot;~&quot;,x)
  return(formula)
}


formula_df &lt;- empty_df |&gt;
  distinct(colname, .keep_all = T) |&gt;
  select(col, colname) |&gt;
  mutate(formula = map(.x=colname, .f=~gen_formula(df_miss,.x)) |&gt; unlist()) 


missing_fill &lt;- function(empty_df) {
  df_miss_idx_fill &lt;- matrix(nrow = nrow(df_miss), ncol = ncol(df_miss)) 
  
  for (i_mis in 1:empty_row) {
    rownum &lt;- empty_df[[i_mis, &quot;row&quot;]]
    colnum &lt;- empty_df[[i_mis, &quot;col&quot;]]
    data &lt;- empty_df[[i_mis, &quot;value&quot;]]
    df_miss_idx_fill[rownum,colnum] &lt;- data
  }
  
  df_miss_idx_fill &lt;- df_miss_idx_fill |&gt; as_tibble() 
  colnames(df_miss_idx_fill) &lt;- colnames(df_miss)
  df_miss_idx_fill &lt;- df_miss_idx_fill |&gt;
    mutate(p4 = as.integer(p4),
           p5 = as.integer(p5))
  
  df_miss_df_miss_idx_fill &lt;&lt;- map2_dfc(df_miss, df_miss_idx_fill, coalesce) #learn about map2_dfc
}
</pre><p><code>empty_df</code> contains randomly pulled number from respective datatype. <code>grab_xvec</code> grabs the predictor columns then <code>gen_formula</code> turns that into model formula for fitting. And <code>missing_fill</code> starts filling out the missing data <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f44d.png" alt="👍" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Sorry if my code is not efficient. I’m sure we can have LLM make it even more efficient <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f923.png" alt="🤣" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>




<h3 id="next-we-iterate">Next, we iterate!
  <a href="https://www.kenkoonwong.com/blog/mice/#next-we-iterate" rel="nofollow" target="_blank"><svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
      <path d="M0 0h24v24H0z" fill="currentColor"></path>
      <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path>
    </svg></a>
</h3>
<pre># grab the colomuns that have missing values 
col_missing &lt;- empty_df |&gt; distinct(col) |&gt; pull()

# create empty df so we can fill in
empty_df_mice &lt;- tibble()
  
# set up chain
for (chain in 1:10) {
  
  # internally create another df so we can use and override data
  impute_df &lt;- fill_empty_df(empty_df)
  
# for each chain iterate up to 10 times (this is where the chain equation starts)
for (iter in 1:10) {
  
# for each iteration look at each column with missing data
for (i in col_missing) {
  
  # refresh the filled dataset with the latest imputations
  missing_fill(impute_df)
  
  # set seed for each chain, iter, and column so that we can reproduce the same for testing purposes
  seed &lt;- paste0(chain,iter,i) |&gt; as.numeric()
  set.seed(seed)
  
  # look for datatype of the column we&#39;re evaluating now
  dtype &lt;- impute_df |&gt;
    filter(col == i) |&gt;
    distinct(model)
  
  # a long winded way of sorting what columns to use for modeling and what to readd back
  empty_df_i &lt;- impute_df |&gt;
    filter(col == i)
  
  empty_df_i_not &lt;- impute_df |&gt;
    filter(col != i)
  
  # finding out what rows are missing and remove those for modelling
  missing_vec &lt;- empty_df_i |&gt;
    pull(row)
  
  df_miss_i_full &lt;- df_miss_df_miss_idx_fill |&gt;
    slice(-missing_vec)
  
  # resample (in place of posterior sampling)
  df_miss_i &lt;- slice_sample(df_miss_i_full, replace = T, n = nrow(df_miss_i_full))
  
  
  # find out the formula for model
  formula &lt;- formula_df |&gt;
    filter(col == i) |&gt;
    pull(formula)
  
  # algo for which estimator to use
  if (dtype == &quot;linreg&quot;) {
  model_i &lt;- lm(formula, data = df_miss_i)
  } 
  
  if (dtype == &quot;logreg&quot;) {
   model_i &lt;- glm(formula, data = df_miss_i, family = &quot;binomial&quot;)   
  }

  newdata &lt;- df_miss_df_miss_idx_fill |&gt;
    slice(missing_vec)
  
  if (dtype == &quot;logreg&quot;) {
  empty_df_i &lt;- empty_df_i |&gt;
    mutate(value = rbinom(nrow(empty_df_i), 1, predict(model_i, newdata = newdata, type = &quot;response&quot;)))
  } else {
  empty_df_i &lt;- empty_df_i |&gt;
    mutate(value = predict(model_i, newdata = newdata) + rnorm(nrow(empty_df_i),0,sigma(model_i)))
  }
  
  impute_df &lt;- empty_df_i_not |&gt;
    rbind(empty_df_i)
  
}
  # only save the last iter data for each chain
  if (iter == 10) {
    empty_df_mice &lt;- empty_df_mice |&gt;
      rbind(impute_df |&gt; mutate(chain = chain)) 
  }
  
}
}

# write a function to create dataframe based on chains
missing_fill_full &lt;- function(x) {
  df_miss_idx_fill &lt;- matrix(nrow = nrow(df_miss), ncol = ncol(df_miss)) 
  
  for (i_mis in 1:empty_row) {
    rownum &lt;- x[[i_mis, &quot;row&quot;]]
    colnum &lt;- x[[i_mis, &quot;col&quot;]]
    data &lt;- x[[i_mis, &quot;value&quot;]]
    df_miss_idx_fill[rownum,colnum] &lt;- data
  }
  
  df_miss_idx_fill &lt;- df_miss_idx_fill |&gt; as_tibble() 
  colnames(df_miss_idx_fill) &lt;- colnames(df_miss)
  df_miss_idx_fill &lt;- df_miss_idx_fill |&gt;
    mutate(p4 = as.integer(p4),
           p5 = as.integer(p5))
  
  # merges both df and collapse those overlap completely
  map2_dfc(df_miss, df_miss_idx_fill, coalesce) 

}

# create 10 dataframes for each chain
for (data in 1:10) {
  df_miss_mice_i &lt;- missing_fill_full(empty_df_mice |&gt; filter(chain == data))
  assign(paste0(&quot;df_miss_mice_&quot;,data), df_miss_mice_i)
}
</pre><p>Wow, OK that’s a lot of code! Hopefully the comments above will be self-explainatory. Some of my code may be redundant. Now that we have 10 imputed dataset. The next recipe of the magic is fit each dataset to a model then pool them <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f3ca-200d-2642-fe0f.png" alt="🏊‍♂️" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>




<h3 id="fitall">Fit All!
  <a href="https://www.kenkoonwong.com/blog/mice/#fitall" rel="nofollow" target="_blank"><svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
      <path d="M0 0h24v24H0z" fill="currentColor"></path>
      <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path>
    </svg></a>
</h3>
<pre># put all 10 dataset into a list (probably should&#39;ve done this before for efficienty, oh well)
imp_list &lt;- map(.x=1:10, ~ missing_fill_full(empty_df_mice |&gt; filter(chain == .x)))

# fit one at a time and save
fits &lt;- map(imp_list, ~ glm(outcome ~ p1 + p2 + p3 + p4 + p5,
                            data = .x, family = binomial))
</pre><p>Let’s take a peek at the first fit</p>
<pre>fits[[1]] |&gt; broom::tidy(conf.int = T)
</pre><p align="center">
  <img loading="lazy" src="https://i1.wp.com/www.kenkoonwong.com/blog/mice/fit1.png?w=450&#038;ssl=1" alt="image" height="auto" data-recalc-dims="1">
</p>
<p>Alright! Looks pretty ok.</p>




<h3 id="pool">Pool them
  <a href="https://www.kenkoonwong.com/blog/mice/#pool" rel="nofollow" target="_blank"><svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
      <path d="M0 0h24v24H0z" fill="currentColor"></path>
      <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path>
    </svg></a>
</h3>
<pre># assess the point estimate of coefficient
coef &lt;- 1:6
fit_list &lt;- 1:10
temp_coef &lt;- c()
# temp_coef_var &lt;- c()
model_coef &lt;- c()
model_coef_var &lt;- c()
for (i in coef) {
  for (j in fit_list) {
  temp_coef[j] &lt;- fits[[j]]$coefficients[[i]]
  }
  model_coef[i] &lt;- mean(temp_coef)
  model_coef_var[i] &lt;- var(temp_coef)
}

# assess the pooled SE to construct 95% CI of the coefficients
se &lt;- do.call(rbind, map(.x = fits, ~ tidy(.x) |&gt; pull(std.error))) |&gt; as_tibble()
within_var &lt;- colMeans(se^2) 
between_var &lt;- model_coef_var 
total_var &lt;- within_var + between_var + between_var / length(fit_list)
final_se &lt;- sqrt(total_var)

# degrees of freedom (Barnard–Rubin)
lambda &lt;- (1 + 1/length(fit_list)) * between_var / total_var
df_com &lt;- 1000 - 6
df_old &lt;- (length(fit_list) - 1) / lambda^2
df_obs &lt;- (df_com + 1) / (df_com + 3) * df_com * (1 - lambda)
degfree &lt;- df_old * df_obs / (df_old + df_obs)

tval &lt;- qt(0.975, degfree)
mice_coef &lt;- tibble(term = fits[[1]] |&gt; tidy() |&gt; pull(term), estimate = model_coef, std.error = final_se, tval = tval,
       conf.low = estimate - tval * std.error, conf.high = estimate + tval * std.error) |&gt;
  select(-tval) |&gt;
  mutate(method = &quot;our_mice&quot;)
</pre><p>To combine our 10 models, we use Rubin’s rules. For the estimate, we just average the 10 coefficients. For the standard error, we add two kinds of uncertainty: the usual uncertainty within each model, and how much the 10 coefficients disagree with each other because of the missing data. Since that disagreement is estimated from only 10 datasets, we use a t value with Barnard–Rubin degrees of freedom instead of z score of 1.96 for 95% CI.</p>
<pre>library(mice)

imp  &lt;- mice(df_miss, m = 10, maxit = 10, seed = 1, printFlag = FALSE)
fit  &lt;- with(imp, glm(outcome ~ p1 + p2 + p3 + p4 + p5, family = binomial))
actual_mice_coef &lt;- tidy(pool(fit), conf.int = TRUE) |&gt;
  select(term, estimate, std.error, conf.low, conf.high) |&gt;
  mutate(method = &quot;actual_mice&quot;)
</pre><p>For completeness sake, let’s use the professional’s method and see if our method matches up.</p>




<h3 id="grab-all-the-coefficients-from-different-models">Grab All the Coefficients From Different Models
  <a href="https://www.kenkoonwong.com/blog/mice/#grab-all-the-coefficients-from-different-models" rel="nofollow" target="_blank"><svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
      <path d="M0 0h24v24H0z" fill="currentColor"></path>
      <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path>
    </svg></a>
</h3>
<pre>## function to grab stats
grab_stats &lt;- function(df, method) {
  model &lt;- glm(outcome ~ p1 + p2 + p3 + p4 + p5, df, family = &quot;binomial&quot;) 
  coef &lt;- model |&gt; tidy(conf.int = T) |&gt; select(-statistic, -p.value) |&gt; mutate(method = method)
  return(coef)
}

## full data
full_data_coef &lt;- grab_stats(df, &quot;full_data&quot;)

## complete case
cco_coef &lt;- grab_stats(df_miss, &quot;complete_case&quot;)

## complete case analysis
all_coef &lt;- rbind(mice_coef, full_data_coef, cco_coef, actual_mice_coef)

## truth
truth &lt;- tibble(term = c(&quot;(Intercept)&quot;, &quot;p1&quot;, &quot;p2&quot;, &quot;p3&quot;, &quot;p4&quot;, &quot;p5&quot;),
                truth = c(-0.5, 0.3, -0.2, 0.1, 0.5, 0.4))

## visualize all
all_coef |&gt;
  ggplot(aes(x = estimate, y = term, color = method)) +
  geom_vline(xintercept = 0, linetype = &quot;dotted&quot;, colour = &quot;grey60&quot;) +
  geom_pointrange(aes(xmin = conf.low, xmax = conf.high),
                  position = position_dodge(width = 0.6)) +
  geom_point(data = truth, aes(x = truth, y = term),
             inherit.aes = FALSE, shape = 4, size = 4) +
  labs(x = &quot;Estimate (log-odds)&quot;, y = NULL,
       caption = &quot;× = true coefficient&quot;) +
  theme_bw()
</pre><p align="center">
  <img loading="lazy" src="https://i1.wp.com/www.kenkoonwong.com/blog/mice/mice_coef.png?w=450&#038;ssl=1" alt="image" height="auto" data-recalc-dims="1">
</p>
<p>Our from-scratch MICE (purple) lines up closely with the mice package (red) for every coefficient, and both stay near the full-data fit (teal). Their intervals are slightly wider than the full-data ones, reflecting the extra uncertainty from the missing data. Complete-case analysis (green) drops every row with any missing value, so its estimates wander and its intervals are much wider. The <code>×</code> marks show the true coefficients from our data-generating process.</p>
<p>For p4, both MICE versions land a bit above the full-data estimate. Since they agree, it’s likely a quirk of this dataset rather than a coding problem. p5 is MNAR, yet both versions look fine here. That may just be luck in a single run, technically it shouldn’t really work, right? <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f914.png" alt="🤔" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p>Hooray! <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f64c.png" alt="🙌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> We did it! Wow, that was a long journey! I started this since 10/9/26 on the procedure, draft codes etc. Met lots of hurdles over the weeks. I’d be honest with you, it was bumpy! My initial go was a wrong approach where I average out the 10 datasets imputed data (which was wrong), then realized that that is not the proper pooling. Thanks to LLM being consistent tutor and back and forth conversaiton to converge to the proper technique. Now, I know how it works under the hood!</p>




<h2 id="opportunities">Opportunities For Improvement
  <a href="https://www.kenkoonwong.com/blog/mice/#opportunities" rel="nofollow" target="_blank"><svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
      <path d="M0 0h24v24H0z" fill="currentColor"></path>
      <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path>
    </svg></a>
</h2>
<ul>
<li>next we should learn sampling from posterior instead of resampling</li>
<li>Try predictive mean matching, mice’s default. It borrows real observed values instead of adding normal noise, which keeps imputations within realistic ranges.</li>
<li>Make the code faster and tidier. Vectorise missing_fill(), keep everything in lists instead of global variables, and pass the random generator explicitly</li>
<li>try out sensitivity analysis with mice</li>
</ul>




<h2 id="lessons">Lessons learnt
  <a href="https://www.kenkoonwong.com/blog/mice/#lessons" rel="nofollow" target="_blank"><svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
      <path d="M0 0h24v24H0z" fill="currentColor"></path>
      <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z"></path>
    </svg></a>
</h2>
<ul>
<li>learnt the machinery of mice!</li>
<li>learnt about <code>map2_dfc</code></li>
<li>learnt about Barnard–Rubin degrees of freedom and how to pool using Rubin’s rule.</li>
<li>learnt total variance formula</li>
</ul>
<p>If you like this article:</p>
<ul>
<li>please feel free to send me a 
<a href="https://www.kenkoonwong.com/blog/" rel="nofollow" target="_blank">comment or visit my other blogs</a></li>
<li>please feel free to follow me on 
<a href="https://bsky.app/profile/kenkoonwong.bsky.social" rel="nofollow" target="_blank">BlueSky</a>, 
<a href="https://twitter.com/kenkoonwong/" rel="nofollow" target="_blank">twitter</a>, 
<a href="https://github.com/kenkoonwong/" rel="nofollow" target="_blank">GitHub</a> or 
<a href="https://rstats.me/@kenkoonwong" rel="nofollow" target="_blank">Mastodon</a></li>
<li>if you would like collaborate please feel free to 
<a href="https://www.kenkoonwong.com/contact/" rel="nofollow" target="_blank">contact me</a></li>
</ul>

<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.kenkoonwong.com/blog/mice/"> r on Everyday Is A School Day</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/my-messy-notes-of-learning-mice-from-scratch/">My Messy Notes of Learning MICE From Scratch</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">404592</post-id>	</item>
		<item>
		<title>September 2026 Top 40 New CRAN Packages</title>
		<link>https://www.r-bloggers.com/2026/10/september-2026-top-40-new-cran-packages/</link>
		
		<dc:creator><![CDATA[Joseph Rickert]]></dc:creator>
		<pubDate>Fri, 30 Oct 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://rworks.dev/posts/september-2026-top-40-new-cran-packages/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>Agriculture<br />
HRRI v1.0.9: Provides diagnostic functions for integrating longitudinal soil, plant and microbial observations during redox disturbance and recovery. Functions calculate stoichiometric potential oxygen demand, accessible electron ...</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/september-2026-top-40-new-cran-packages/">September 2026 Top 40 New CRAN Packages</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://rworks.dev/posts/september-2026-top-40-new-cran-packages/"> R Works</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
 





<div class="columns">
<div class="column" style="width:45%;">
<section id="agriculture" class="level3">
<h3 class="anchored" data-anchor-id="agriculture">Agriculture</h3>
<p><a href="https://cran.r-project.org/package=HRRI" rel="nofollow" target="_blank">HRRI</a> v1.0.9: Provides diagnostic functions for integrating longitudinal soil, plant and microbial observations during redox disturbance and recovery. Functions calculate stoichiometric potential oxygen demand, accessible electron capacity from explicitly supplied inventories and kinetic parameters, recovery signatures, fixed-reference domain scores, and exploratory multiblock scores with observation-coverage diagnostics. For background and methods see <a href="https://doi.org/10.1021/acs.est.5b00006" rel="nofollow" target="_blank">Sander, Hofstetter and Gorski (2015)</a>, <a href="https://doi.org/10.1038%2Fngeo2084" rel="nofollow" target="_blank">Kluepfel et al. (2014)</a>, and <a href="https://doi.org/10.1016/j.gca.2005.12.005" rel="nofollow" target="_blank">Thompson et al. (2006)</a>. There are three vignettes including <a href="https://cran.r-project.org/web/packages/HRRI/vignettes/HRRI_gallery.html" rel="nofollow" target="_blank">HRRI: Reading the Figures — A Worked Example</a> and <a href="https://cran.r-project.org/web/packages/HRRI/vignettes/HRRI_paper_figures.html" rel="nofollow" target="_blank">HRRI: Reproduce All Six Paper Figures in R</a>.</p>
<p><a href="https://i2.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/HERRI.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-1" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/HERRI.png?w=578&#038;ssl=1" class="img-fluid" alt="Plots showing identifiability, sensitivity and accounting" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=VeraCrop" rel="nofollow" target="_blank">VeraCrop</a> v0.1.0: Implements automated variable-type detection, preprocessing, encoding, scaling, model diagnostics, variable selection, and yield gap computation for agricultural comparative performance analysis. The comparative performance analysis approach is described in <a href="https://doi.org/10.1016/j.scienta.2003.11.017" rel="nofollow" target="_blank">de Bie (2004)</a>. For an overview of yield gap assessment methodologies, see <a href="https://doi.org/10.1016/j.agsy.2025.104392" rel="nofollow" target="_blank">Kamkar et al. (2025)</a>. See the <a href="https://cran.r-project.org/web/packages/VeraCrop/vignettes/getting-started.html" rel="nofollow" target="_blank">vignette</a> to get started.</p>
<p><a href="https://i2.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/VeraCrop.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-2" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/VeraCrop.png?w=578&#038;ssl=1" class="img-fluid" alt="Plot of observed vs predicted yield" data-recalc-dims="1"></a></p>
</section>
<section id="causal-inference" class="level3">
<h3 class="anchored" data-anchor-id="causal-inference">Causal Inference</h3>
<p><a href="https://cran.r-project.org/package=phdid" rel="nofollow" target="_blank">phdid</a> v0.1.0: In staggered difference-in-differences designs the treatment effect is a vector of cohort-time effects. Estimating each separately is unbiased but imprecise when some are equal, while pooling them all is precise but biased under genuine heterogeneity. This package treats the choice as a partition-selection problem on the cohort-time cells and provides two estimators: a Dirichlet process mixture fitted by a collapsed Gibbs sampler and an <em>L0</em>-penalized estimator that returns a single partition. Methods are described in <a href="https://doi.org/10.2139/ssrn.7207083" rel="nofollow" target="_blank">Arora and Wagle (2026)</a>. There are two vignettes <a href="https://cran.r-project.org/web/packages/phdid/vignettes/mpdta.html" rel="nofollow" target="_blank">Minimum wages and teen employment: recovering partial homogeneity</a> and <a href="https://cran.r-project.org/web/packages/phdid/vignettes/simulation.html" rel="nofollow" target="_blank">When partial homogeneity helps, and when it does not</a>.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/phdid.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-3" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/phdid.png?w=578&#038;ssl=1" class="img-fluid" alt="Plot of cohort effects" data-recalc-dims="1"></a></p>
</section>
<section id="climate-studies" class="level3">
<h3 class="anchored" data-anchor-id="climate-studies">Climate Studies</h3>
<p><a href="https://cran.r-project.org/package=xaci" rel="nofollow" target="_blank">xaci</a> v0.1.0: Computes the Actuarial Climate Index (ACI) and its components (temperature, precipitation, drought, wind, sea level) from gridded climate data (‘NetCDF’) and tide gauge records. Implements the methodology described in <a href="https://hal.science/hal-04491982v2" rel="nofollow" target="_blank">Garrido, Milhaud &#038; Olympio (2025)</a> for a French/European actuarial climate index, building on the American Academy of Actuaries framework, to any country in the world. There are seven vignettes including <a href="https://cran.r-project.org/web/packages/xaci/vignettes/xaci-intro.html" rel="nofollow" target="_blank">Get Started</a> and <a href="https://cran.r-project.org/web/packages/xaci/vignettes/xaci-visualization.html" rel="nofollow" target="_blank">Visualizing ACI Results</a>.</p>
<p><a href="https://i2.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/xaci.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-4" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/xaci.png?w=578&#038;ssl=1" class="img-fluid" alt="ACI Summary Dashboard" data-recalc-dims="1"></a></p>
</section>
<section id="computational-methods" class="level3">
<h3 class="anchored" data-anchor-id="computational-methods">Computational Methods</h3>
<p><a href="https://cran.r-project.org/package=koopman.dmd" rel="nofollow" target="_blank">koopman.dmd</a> v0.2.2: Implements Dynamic Mode Decomposition (DMD) with Koopman operator theory extensions, powered by a Rust backend via <code>extendr</code>; standard DMD as described in <a href="https://doi.org/10.1017/S0022112010001217" rel="nofollow" target="_blank">Schmid (2010)</a>, DMD with control for forced linear systems following <a href="https://doi.org/10.1137/15M1013857" rel="nofollow" target="_blank">Proctor, Brunton, and Kutz (2016)</a>; and Extended DMD with lifting functions, Hankel-DMD via time-delay embedding, Generalized Laplace Analysis for direct eigenfunction computation, and harmonic time averages and mesochronic harmonic plots for phase space analysis as developed in <a href="https://doi.org/10.48550/arXiv.2009.05883" rel="nofollow" target="_blank">Mezic (2020)</a>. See the <a href="https://cran.r-project.org/web/packages/koopman.dmd/vignettes/koopman-dmd.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://cran.r-project.org/package=RcppTrust" rel="nofollow" target="_blank">RcppTrust</a> v0.1.0: Implements functions for local optimization using derivatives and trust regions which are guaranteed to converge to a local minimum of the objective function. This is a thread-safe <code>C++</code> port of the algorithm in the <code>trust</code> package exposed both as a drop-in <code>R</code> function and as a <code>C</code>-callable, thread-safe entry point in a manner so it can be used from parallel <code>C++</code> code such as in <code>nlmixr2est</code>. See the <a href="https://cran.r-project.org/web/packages/trust/vignettes/trust.pdf" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://cran.r-project.org/package=tulpa" rel="nofollow" target="_blank">tulpa</a> v 0.7.5: Provides a general-purpose engine for fitting Bayesian hierarchical models with spatial fields, temporal effects, spatially varying coefficients, and multiple inference backends. Scalable spatial structure includes Hilbert space approximate Gaussian processes (<a href="https://doi.org/10.1007/s11222-022-10167-2" rel="nofollow" target="_blank">Riutort-Mayol et al. (2023)</a>), nearest-neighbor Gaussian processes (<a href="https://doi.org/10.1080/01621459.2015.1044091" rel="nofollow" target="_blank">Datta et al. (2016)</a>), intrinsic conditional autoregressive models (<a href="https://doi.org/10.1007/BF00116466" rel="nofollow" target="_blank">Besag, York, and Mollie (1991)</a>), the reparameterized Besag-York-Mollie model (<a href="https://doi.org/10.1177/0962280216660421" rel="nofollow" target="_blank">Riebler et al. (2016)</a>), and stochastic partial differential equation fields ( <a href="https://doi.org/10.1111/j.1467-9868.2011.00777.x" rel="nofollow" target="_blank">Lindgren, Rue, and Lindstrom (2011)</a>). Temporal structure covers random walks, autoregressive processes, and Gaussian processes. Inference is tiered by correctness guarantee: exact Hamiltonian Monte Carlo with the No-U-Turn sampler, Laplace and nested Laplace approximations with hyperparameter integration (<a href="https://doi.org/10.1111/j.1467-9868.2008.00700.x" rel="nofollow" target="_blank">Rue, Martino, and Chopin (2009)</a>), and variational inference. Model-specific packages plug observation likelihoods into the engine through a templated C++ callback interface. There are three vignettes including <a href="https://cran.r-project.org/web/packages/tulpa/vignettes/quickstart.html" rel="nofollow" target="_blank">Getting started</a> and <a href="https://cran.r-project.org/web/packages/tulpa/vignettes/spatial-models.html" rel="nofollow" target="_blank">Spatial Fields</a>.</p>
<p><a href="https://cran.r-project.org/package=WSwavelet" rel="nofollow" target="_blank">WSwavelet</a> v0.1.0: Implements Bayesian wavelet denoising using a resolution-adaptive spike-and-slab prior whose continuous slab is a mixture of a compactly supported Wendland-type density and the semicircle density. Provides Gaussian and Laplace working-likelihood versions, empirical-Bayes fitting of resolution-specific slab weights, robust noise-scale estimation, posterior-mean coefficient shrinkage, reconstruction by the inverse discrete wavelet transform, and diagnostic tools. See the <a href="https://cran.r-project.org/web/packages/WSwavelet/vignettes/WSwavelet_vignette.pdf" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i2.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/WSwavelet.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-5" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/WSwavelet.png?w=578&#038;ssl=1" class="img-fluid" alt="Gaussian Reconstruction of a Signal" data-recalc-dims="1"></a></p>
</section>
<section id="ecology" class="level3">
<h3 class="anchored" data-anchor-id="ecology">Ecology</h3>
<p><a href="https://cran.r-project.org/package=acousticTS" rel="nofollow" target="_blank">acousticTS</a> v2.0.8: Acoustic target strength (TS) represents the intensity of an echo returning from an individual scatterer such as bubbles, fish, or zooplankton. TS can be used to convert integrated or volumetric backscatter collected from fisheries acoustic surveys into units of number density (e.g. animals per <img src="https://latex.codecogs.com/png.latex?m%5E3">), abundance (e.g. number of animals), and biomass (e.g. kg). This parameter can also be used to aid in classifying backscatter, such as separating likely echoes of large predatory fish (e.g. adult cod) from smaller prey (e.g. shrimp). Applications are described by <a href="https://doi.org/10.1121/10.0022459" rel="nofollow" target="_blank">Lucca et al. (2023)</a>, with fisheries-acoustics principles from <a href="https://doi.org/10.1002/9780470995303" rel="nofollow" target="_blank">Simmonds and MacLennan (2005)</a>. See the <a href="https://cran.r-project.org/web/packages/acousticTS/vignettes/boundary_conditions.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://cran.r-project.org/package=SAMPLE" rel="nofollow" target="_blank">SAMPLE</a> v1.0.1: Estimates the sampling effort needed to obtain stable species occurrence and prevalence rates from presence-absence data. The method repeatedly subsamples observations, evaluates changes in estimated rates, and reports the minimum stable sample size. For methodological details, see <a href="https://doi.org/10.1002/ece3.70998" rel="nofollow" target="_blank">Bravo et al. (2025)</a>. Look <a href="https://github.com/yacinebenchehida/SAMPLE" rel="nofollow" target="_blank">here</a> to get started.</p>
<p><img src="https://i1.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/SAMPLE.png?w=578&#038;ssl=1" class="img-fluid" alt="Plots showing how prevalence rate increases with sampling" data-recalc-dims="1"> <a href="https://cran.r-project.org/package=sptrends" rel="nofollow" target="_blank">sptrends</a> 1.6.3: Provides a unified and reproducible framework for statistical inference of spatiotemporal trends in gridded environmental data that addresses the interconnected challenges of serial correlation, spatial dependence and multiple testing that commonly arise when analyzing gridded environmental time series. Core methods support serial-correlation treatment through trend-preserving prewhitening, pixel-wise and spatially explicit trend inference, slope estimation and multiple-testing correction. Dedicated workflows reproduce published methodologies, for example <a href="https://doi.org/10.1016/j.rsase.2024.101377" rel="nofollow" target="_blank">Gutiérrez-Hernández and García (2025)</a> for True Significant Trends workflow and <a href="https://doi.org/10.3390/rs16203886" rel="nofollow" target="_blank">Gutiérrez-Hernández and García (2024)</a> for Robust Trend Analysis workflow. Supporting utilities facilitate raster data import and inspection, anomaly calculation, spatial autocorrelation diagnostics, and more. There are six vignettes including: <a href="https://cran.r-project.org/web/packages/sptrends/vignettes/b-prewhitening.html" rel="nofollow" target="_blank">Dealing with serial correlation</a> and <a href="https://cran.r-project.org/web/packages/sptrends/vignettes/d-slope-estimation.html" rel="nofollow" target="_blank">Trend magnitude estimation</a>.</p>
<p><a href="https://i2.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/sptrends.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-6" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/sptrends.png?w=578&#038;ssl=1" class="img-fluid" alt="Plot indicating serial correlation" data-recalc-dims="1"></a></p>
</section>
<section id="economics" class="level3">
<h3 class="anchored" data-anchor-id="economics">Economics</h3>
<p><a href="https://cran.r-project.org/package=PricingBandits" rel="nofollow" target="_blank">PricingBandits</a> v2.1.0: Implements multi-armed bandit approaches for pricing experiments with an unknown demand curve, as developed in <a href="https://doi.org/10.1287/mksc.2022.0247" rel="nofollow" target="_blank">Weaver, Kumar, and Jain (2025)</a>. Includes Upper Confidence Bound and Thompson Sampling baselines, Gaussian process variants, monotonic Gaussian process variants that constrain demand to be weakly decreasing in price, and heterogeneous-noise extensions. The willingness-to-pay distribution is fully user-specified via a vector of consumer valuations, so any demand environment can be simulated or replayed. See the <a href="https://cran.r-project.org/web/packages/PricingBandits/vignettes/pricingbandits-intro.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/PricingBandits.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-7" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/PricingBandits.png?w=578&#038;ssl=1" class="img-fluid" alt="Plots of cumulative revenue vs Consumers" data-recalc-dims="1"></a></p>
</section>
<section id="epidemiology" class="level3">
<h3 class="anchored" data-anchor-id="epidemiology">Epidemiology</h3>
<p><a href="https://cran.r-project.org/package=ambre" rel="nofollow" target="_blank">ambre</a> v2.1.2: Provides a Quantitative Microbial Risk Assessment framework for the reuse of treated wastewater in agricultural irrigation. Following a multi-barrier approach, the package simulates pathogen inflow and removal along a treatment train, estimates human exposure, and computes health risk indicators such as infection probability, illness probability, and Disability-Adjusted Life Years. It also supports an economic analysis of the treatment scenarios considered. For more details see <a href="https://doi.org/10.36904/20240595" rel="nofollow" target="_blank">Thomas et al. (2024)</a>. There are eight vignettes including <a href="https://cran.r-project.org/web/packages/ambre/vignettes/a-get-started.html" rel="nofollow" target="_blank">Get started</a> and <a href="https://cran.r-project.org/web/packages/ambre/vignettes/d-interpreting-risk.html" rel="nofollow" target="_blank">Interpreting risk against the WHO target</a>.</p>
<p><a href="https://i2.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/ambre.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-8" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/ambre.png?w=578&#038;ssl=1" class="img-fluid" alt="Plots of exposure distributions" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=tbl.now" rel="nofollow" target="_blank">tbl.now</a> v1.2.0: Defines tidy data structures and package-agnostic workflows for epidemiological nowcasting. The <code>tbl_now</code> class records event, report, and revision dates alongside strata, covariates tagged by model component, censoring, and reporting-delay metadata while remaining compatible with <code>dplyr</code>. Tools support validation, manipulation, diagnostics, visualization, format conversion, retrospective evaluation, and multiple modelling engines. See the <a href="https://cran.r-project.org/web/packages/tbl.now/vignettes/tbl.now.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i1.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/tblnow.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-9" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/tblnow.png?w=578&#038;ssl=1" class="img-fluid" alt="Automatic effects plots" data-recalc-dims="1"></a></p>
</section>
<section id="genomics" class="level3">
<h3 class="anchored" data-anchor-id="genomics">Genomics</h3>
<p><a href="https://cran.r-project.org/package=mSigSpectra" rel="nofollow" target="_blank">mSigSpectra</a> v0.1.3: Reads variant call format (VCF) files in a caller-agnostic way, annotates variants with flanking sequence context and transcriptional strand, and builds mutational-spectrum catalogs of single base substitutions, doublet base substitutions, and small insertions and deletions at several resolutions (SBS96, SBS192, SBS1536, DBS78, DBS136, DBS144, ID83, ID89, ID166, ID476) and in both counts and density representations. Used in the preparation of <a href="https://doi.org/10.5281/zenodo.18451842" rel="nofollow" target="_blank">Rozen et al. (2026)</a>. See the <a href="https://cran.r-project.org/web/packages/mSigSpectra/vignettes/mSigSpectra.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/mSigSpectra.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-10" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/mSigSpectra.png?w=578&#038;ssl=1" class="img-fluid" alt="Plot of variant counts generated by the Strelka variant caller using the GRCh37 reference genome" data-recalc-dims="1"></a></p>
</section>
<section id="machine-learning" class="level3">
<h3 class="anchored" data-anchor-id="machine-learning">Machine Learning</h3>
<p><a href="https://cran.r-project.org/package=fastgbm" rel="nofollow" target="_blank">fastgbm</a> v0.6.1: Implements a fast gradient boosting machine covering four task types with one interface: regression (squared error), binary and multiclass classification (logistic and one-vs-rest), and right-censored survival analysis via Cox (Breslow ties), accelerated failure time (normal location-scale), or piecewise-exponential objectives. Provides native missing-value routing, baseline-hazard estimation and survival-probability prediction for the survival objectives, and deterministic multi-threaded training via <code>RcppParallel</code>. Methods are described in <a href="https://doi.org/10.1214/aos/1013203451" rel="nofollow" target="_blank">Friedman (2001)</a>. There are six vignettes including <a href="https://cran.r-project.org/web/packages/fastgbm/vignettes/getting-started.html" rel="nofollow" target="_blank">getting Started</a> and <a href="https://cran.r-project.org/web/packages/fastgbm/vignettes/algorithm.html" rel="nofollow" target="_blank">Algorithm</a>.</p>
<p><a href="https://i2.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/fastgbm.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-11" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/fastgbm.png?w=578&#038;ssl=1" class="img-fluid" alt="Plots of training times by feature count" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=xplaineff" rel="nofollow" target="_blank">xplaineff</a> v0.1.1: Implements the Generalized Additive Decomposition of Global EffecTs algorithm for interpretable machine learning. The package recursively partitions the feature space to minimize heterogeneity of feature effects (e.g., Accumulated Local Effects or Partial Dependence), producing a tree of regions where effects are more stable. It supports both ALE and PD strategies, works with <code>mlr3</code> learners and provides visualization of the interaction tree and regional effect plots. The method is described in <a href="https://jmlr.org/papers/volume25/23-0699/23-0699.pdf" rel="nofollow" target="_blank">Herbinger et al. (2024)</a>. See the <a href="https://cran.r-project.org/web/packages/xplaineff/vignettes/xplaineff.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i2.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/xplaineff.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-12" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/xplaineff.png?w=578&#038;ssl=1" class="img-fluid" alt="Partial Dependence Plot" data-recalc-dims="1"></a></p>
</section>
<section id="medical-statistics" class="level3">
<h3 class="anchored" data-anchor-id="medical-statistics">Medical Statistics</h3>
<p><a href="https://cran.r-project.org/package=chms" rel="nofollow" target="_blank">chms</a> v7.2: <em>ActiGraph wGT3X-BT</em> accelerometer processing methods using the standardized workflow developed by Statistics Canada for cycle 7 of the Canadian Health Measures Survey. The package promotes transparent and reproducible data processing while supporting the harmonization of analytical approaches among researchers wishing to align with Statistics Canada’s methods. For general details about the processing methods, please consult <a href="https://doi.org/10.25318/82-003-x202600200001-eng" rel="nofollow" target="_blank">Clarke et al. (2026)</a>. See <a href="https://cran.r-project.org/web/packages/chms/readme/README.html" rel="nofollow" target="_blank">README</a> to get started.</p>
<p><a href="https://cran.r-project.org/package=rsmart" rel="nofollow" target="_blank">rsmart</a> v0.1.0: Implements the interim augmented inverse probability weighted estimator for the value of treatment regimes embedded in sequential multiple assignment randomized trials. It subsumes both the inverse probability weighted estimator and the augmented inverse probability weighted estimator, enabling inference at interim and final analyses. Provides functions for value estimation, sandwich variance computation, group sequential stopping boundaries, and sample size determination for multi-stage SMARTs. See <a href="https://doi.org/10.1111/biom.13854" rel="nofollow" target="_blank">Manschot, Laber, and Davidian (2023)</a> for additional details. There are four vignettes including <a href="https://cran.r-project.org/web/packages/rsmart/vignettes/start-here.html" rel="nofollow" target="_blank">Getting Started</a> and <a href="https://cran.r-project.org/web/packages/rsmart/vignettes/iaipwe.html" rel="nofollow" target="_blank">Full demo of IAIPWE</a>.</p>
<p><a href="https://i1.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/rsmart.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-13" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/rsmart.png?w=578&#038;ssl=1" class="img-fluid" alt="Diagram of SMART" data-recalc-dims="1"></a></p>
</section>
</div><div class="column" style="width:10%;">

</div><div class="column" style="width:45%;">
<section id="meta-analysis" class="level3">
<h3 class="anchored" data-anchor-id="meta-analysis">Meta-analysis</h3>
<p><a href="https://cran.r-project.org/package=metaGLMM" rel="nofollow" target="_blank">metaGLMM</a> v1.0.0: Extends traditional random-effects meta-analysis by embedding it within a generalized linear mixed-effects model framework. The package supports covariate adjustment and non-normal responses using aggregate data, and provides likelihood-based inference with computationally efficient likelihood evaluation. The underlying methodology is described in <a href="https://doi.org/10.1093/biomtc/ujag148" rel="nofollow" target="_blank">Hanada and Sugimoto (2026)</a>. There are seven vignettes including <a href="https://cran.r-project.org/web/packages/metaGLMM/vignettes/getting-started.html" rel="nofollow" target="_blank">Getting started</a> and <a href="https://cran.r-project.org/web/packages/metaGLMM/vignettes/prediction-forest.html" rel="nofollow" target="_blank">Prediction and forest plots</a>.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/MetaGLMM.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-14" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/MetaGLMM.png?w=578&#038;ssl=1" class="img-fluid" alt="Forest plot showing effect size" data-recalc-dims="1"></a></p>
</section>
<section id="networks" class="level3">
<h3 class="anchored" data-anchor-id="networks">Networks</h3>
<p><a href="https://cran.r-project.org/package=nethist" rel="nofollow" target="_blank">nethist</a> v1.0.0: Estimates network histograms, a blockmodel approximation to the graphon underlying a network’s connectivity pattern, for both single-layer and multilayer networks. Implements graphon estimation methods including the profile-likelihood method of <a href="https://doi.org/10.1073/pnas.1400374111" rel="nofollow" target="_blank">Olhede and Wolfe (2014)</a> and the least-squares method of <a href="https://doi.org/10.1214/15-AOS1354" rel="nofollow" target="_blank">Gao, Lu, and Zhou (2015)</a> for single-layer networks, and the multilayer extension of <a href="https://doi.org/10.48550/arXiv.2608.14536" rel="nofollow" target="_blank">Song and Olhede (2026)</a>. See the <a href="https://cran.r-project.org/web/packages/nethist/vignettes/example.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i2.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/nethist.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-15" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/nethist.png?w=578&#038;ssl=1" class="img-fluid" alt="Violin plots summarizing network topology" data-recalc-dims="1"></a></p>
</section>
<section id="phylogenetics" class="level3">
<h3 class="anchored" data-anchor-id="phylogenetics">Phylogenetics</h3>
<p><a href="https://cran.r-project.org/package=ConsTree" rel="nofollow" target="_blank">ConsTree</a> v1.0.0: Efficient construction of phylogenetic consensus trees. Methods include strict, majority-rule, majority-rule (+), loose (combinable component, semi-strict), greedy, Adams, frequency difference, R*, and local consensus. See the <a href="https://cran.r-project.org/web/packages/ConsTree/vignettes/ConsTree.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i1.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/ConsTree.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-16" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/ConsTree.png?w=578&#038;ssl=1" class="img-fluid" alt="Plots of consensus trees" data-recalc-dims="1"></a></p>
</section>
<section id="statistics" class="level3">
<h3 class="anchored" data-anchor-id="statistics">Statistics</h3>
<p><a href="https://cran.r-project.org/package=attrition" rel="nofollow" target="_blank">attrition</a> v1.0.0: Implements the double-sampling bounds estimator of <a href="https://doi.org/10.1017/pan.2016.6" rel="nofollow" target="_blank">Coppock et al.(2017)</a> for randomized experiments with nonignorable missing outcomes. Provides worst-case (Manski) bounds, double-sampling bounds with analytic variance and Imbens-Manski confidence intervals, <a href="https://doi.org/10.1111/j.1467-937X.2009.00536.x" rel="nofollow" target="_blank">Lee (2009)</a> trimming bounds with analytic and bootstrap standard errors, covariate adjustment via poststratification, and a sensitivity analysis for violations of the outcome stability assumption. See the <a href="https://cran.r-project.org/web/packages/attrition/vignettes/attrition.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i1.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/attrition.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-17" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/attrition.png?w=578&#038;ssl=1" class="img-fluid" alt="Visualization of Bounds" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=FitVerse" rel="nofollow" target="_blank">FitVerse</a> v1.0-2: Provides a unified interface for fitting parametric probability distributions to continuous univariate data that supports fifty-two distribution families spanning symmetric, right-skewed, heavy-tailed, bounded, and extreme-value shapes distributions, and three estimation methods. Automatic best-fit selection is performed using multiple criteria and tests. Includes publication-quality diagnostic plot, histogram overlaid with density curves and optional interactive plots via <code>plotly</code>. Additional tools include bootstrap confidence intervals for parameter estimates and return levels, batch fitting across multiple columns and more. Functions support data characterization in survey sampling, hydrology, and actuarial workflows. See <a href="https://doi.org/10.1111/j.2517-6161.1990.tb01775.x" rel="nofollow" target="_blank">Hosking (1990)</a> for background and the <a href="https://cran.r-project.org/web/packages/FitVerse/vignettes/fitverse-introduction.html" rel="nofollow" target="_blank">vignette</a> for examples.</p>
<p><a href="https://i2.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/FitVerse.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-18" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/FitVerse.png?w=578&#038;ssl=1" class="img-fluid" alt="Plot of fitted Gumbel distribution fitted via MLE" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=hdcce" rel="nofollow" target="_blank">hdcce</a> v0.1.0: Estimation and inference for panel data models with interactive fixed effects. The methods cover the linear specification of <a href="https://doi.org/10.3982/QE2308" rel="nofollow" target="_blank">Ruecker, Vogt, Linton, and Walsh (2025)</a>, and the dictionary design of <a href="https://doi.org/10.48550/arXiv.2608.02055" rel="nofollow" target="_blank">Ruecker, Vogt, and Linton (2026)</a>. See the <a href="https://cran.r-project.org/web/packages/hdcce/vignettes/hdcce.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://cran.r-project.org/package=HDElliptical" rel="nofollow" target="_blank">HDElliptical</a> v0.1.3: Provides fast, documented implementations of robust estimation, testing, dimension reduction, classification, and clustering methods for high-dimensional elliptically symmetric data. Computational kernels use <code>Rcpp</code> and <code>RcppArmadillo</code>. The package follows methods reviewed in <a href="https://github.com/flnankai/HDElliptical/releases" rel="nofollow" target="_blank">Feng (2026)</a>. See the vignettes <a href="https://cran.r-project.org/web/packages/HDElliptical/vignettes/chapter-1-foundations.html" rel="nofollow" target="_blank">Robust Directional Foundations</a> and <a href="https://cran.r-project.org/web/packages/HDElliptical/vignettes/chapter-2-location-tests.html" rel="nofollow" target="_blank">Location Tests</a>.</p>
<p><a href="https://cran.r-project.org/package=iop" rel="nofollow" target="_blank">iop</a> v0.1.0: Provides estimation, inference, and quantities of interest for ordered probit and ordered logit models whose outcome contains an inflated category: a single ordered category that mixes observations generated by the ordered process with observations generated by a distinct split-population process. Fits the zero-inflated ordered probit of <a href="https://doi.org/10.1016/j.jeconom.2007.01.002" rel="nofollow" target="_blank">Harris and Zhao (2007)</a> and its middle- and top-inflated extensions (See for example <a href="https://doi.org/10.1093/fpa/orae006" rel="nofollow" target="_blank">Bagozzi, Joo and Mukherjee (2024)</a>). Also provides analytic, robust, and cluster-robust standard errors, survey weights and offsets, model comparison, regime-specific predicted probabilities and first differences and additional features. There are four vignettes including <a href="https://cran.r-project.org/web/packages/iop/vignettes/iop.html" rel="nofollow" target="_blank">Getting started</a> and <a href="https://cran.r-project.org/web/packages/iop/vignettes/model.html" rel="nofollow" target="_blank">The model, identification, and estimation</a>.</p>
<p><a href="https://cran.r-project.org/package=normalblockr" rel="nofollow" target="_blank">normalblockr</a> v0.3.0: Implements the Normal-Block model, a Gaussian graphical model with a latent clustering structure for the multivariate analysis of continuous data that clusters variables and, building on the graphical lasso, infers a network of statistical dependencies between clusters rather than between individual variables, for known or unknown clusterings, with an optional zero-inflation extension for data with an excess of exact zeros. See <a href="https://doi.org/10.1016/j.csda.2026.108347" rel="nofollow" target="_blank">Tous &#038; Chiquet (2026)</a> for the model itself and its variational expectation-maximization estimation procedure. There are four vignettes including <a href="https://cran.r-project.org/web/packages/normalblockr/vignettes/mean-block-breast-cancer.html" rel="nofollow" target="_blank">Mean-Block models</a> and <a href="https://cran.r-project.org/web/packages/normalblockr/vignettes/zero-inflated-normal-block.html" rel="nofollow" target="_blank">Zero-inflated Normal-Block models</a>.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/normalblockr.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-19" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/normalblockr.png?w=578&#038;ssl=1" class="img-fluid" alt="Plot of model selection criteria" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=mvdpd" rel="nofollow" target="_blank">mvdpd</a> v0.1-1: Provides robust multivariate estimation based on multivariate, composite and componentwise Density Power Divergence minimization in multivariate normal distribution for casewise and cellwise contamination and robust estimation for multivariate ordered gamma model using multivariate and composite DPD minimization. See <a href="https://doi.org/10.48550/arXiv.2608.18914" rel="nofollow" target="_blank">Ghosh, Agostinelli, and Basu (2026)</a> for details) and the vignettes <a href="https://cran.r-project.org/web/packages/mvdpd/vignettes/mvnormDPD_examples.html" rel="nofollow" target="_blank">mvnormDPD examples</a> and <a href="https://cran.r-project.org/web/packages/mvdpd/vignettes/twocenturies50_example.html" rel="nofollow" target="_blank">mvogammaDPD examples</a>.</p>
<p><a href="https://rworks.dev/posts/september-2026-top-40-new-cran-packages/mvdpd.svg" class="lightbox" data-gallery="quarto-lightbox-gallery-20" rel="nofollow" target="_blank"><img src="https://rworks.dev/posts/september-2026-top-40-new-cran-packages/mvdpd.svg" class="img-fluid" alt="Boxplot comparisons"></a></p>
<p><a href="https://cran.r-project.org/package=pmsims" rel="nofollow" target="_blank">pmsims</a> v1.0.0: Provides a flexible, simulation-based toolkit for exploring how much data are needed to develop reliable prediction models. It works by repeatedly generating data, fitting models, and evaluating performance to show how sample size affects predictive accuracy, calibration, and overfitting. The package supports continuous, binary, and time-to-event outcomes and can be used with both regression-based modelling approaches and machine-learning methods. Methods are described in <a href="https://doi.org/10.1186/s12874-026-02935-9" rel="nofollow" target="_blank">Olaniran et al. (2026)</a> and <a href="https://doi.org/10.48550/arXiv.2602.23507" rel="nofollow" target="_blank">Shamsutdinova et al. (2026)</a>. See the <a href="https://cran.r-project.org/web/packages/pmsims/vignettes/pmsims.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i1.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/pmsims.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-21" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/pmsims.png?w=578&#038;ssl=1" class="img-fluid" alt="Package workflow diagram" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=spfcICOMP" rel="nofollow" target="_blank">spfcICOMP</a> v0.1.0: Implements shrinkage principal fitted components for sufficient dimension reduction in high-dimensional regression and classification. Provides regularized covariance estimation using Oracle Approximating Shrinkage and Maximum Entropy Covariance, structural-dimension selection using conventional and information-complexity criteria, response-guided feature screening, reduced-space prediction, and simulation utilities. Methodological foundations include <a href="https://doi.org/10.1214/08-STS275" rel="nofollow" target="_blank">Cook and Forzani (2008)</a>, <a href="https://doi.org/10.1109/TSP.2010.2053029" rel="nofollow" target="_blank">Chen et al. (2010)</a>, <a href="https://doi.org/10.1006/jmps.1999.1277" rel="nofollow" target="_blank">Bozdogan (2000)</a>, and <a href="https://doi.org/10.48550/arXiv.1909.13017" rel="nofollow" target="_blank">Olorede and Yahya (2019)</a>. See the <a href="https://cran.r-project.org/web/packages/spfcICOMP/vignettes/getting-started.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://cran.r-project.org/package=ZINB.GP" rel="nofollow" target="_blank">ZINB.GP</a> v1.0.0: Fits Bayesian zero-inflated negative binomial regression models with Gaussian process random effects for spatial, temporal, or spatiotemporal count data. Provides Markov chain Monte Carlo sampling, configurable random effects in the zero-inflation and count components, and posterior predictive draws. Implements a full GP version of the methods described by <a href="https://doi.org/10.1016/j.jspi.2023.106098" rel="nofollow" target="_blank">He and Huang (2024)</a>. See the vignettes <a href="https://cran.r-project.org/web/packages/ZINB.GP/vignettes/oregon-landslides.html" rel="nofollow" target="_blank">Oregon Landslides</a> and <a href="https://cran.r-project.org/web/packages/ZINB.GP/vignettes/simulating-zinb-gp.html" rel="nofollow" target="_blank">Simulating and Fitting a Spatiotemporal ZINB-GP Model</a>.</p>
<p><a href="https://i2.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/ZINBGP.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-22" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/ZINBGP.png?w=578&#038;ssl=1" class="img-fluid" alt="Plots of At-Risk and Conditional spatial effects" data-recalc-dims="1"></a></p>
</section>
<section id="surveys" class="level3">
<h3 class="anchored" data-anchor-id="surveys">Surveys</h3>
<p><a href="https://cran.r-project.org/package=risq" rel="nofollow" target="_blank">risq</a> v3.0.1: Calculate representativity indicators for survey quality based on survey data and response models. Use partial indicators to analyse the impact of individual variables and categories. Monitor changes in representativity during data collection. Improve representativity through adaptive survey design. Supports both R-indicators and coefficients of variation. See <a href="https://api.semanticscholar.org/CorpusID:33654901" rel="nofollow" target="_blank">Schouten, Cobben, Bethlehem (2009)</a>, <a href="https://doi.org/10.1016/j.jspi.2011.07.008" rel="nofollow" target="_blank">Shlomo, Skinner, Schouten (2012)</a> and <a href="https://doi.org/10.1111/insr.12159" rel="nofollow" target="_blank">Schouten, Shlomo (2017)</a>. There are two vignettes: <a href="https://cran.r-project.org/web/packages/risq/vignettes/introduction.html" rel="nofollow" target="_blank">Introduction</a> and <a href="https://cran.r-project.org/web/packages/risq/vignettes/theory.pdf" rel="nofollow" target="_blank">Theory</a>.</p>
</section>
<section id="utilities" class="level3">
<h3 class="anchored" data-anchor-id="utilities">Utilities</h3>
<p><a href="https://cran.r-project.org/package=dplyneage" rel="nofollow" target="_blank">dplyneage</a> v0.3.1: Implements column lineage visualizations using <code>React Flow</code> for<code>'dplyr</code> and <code>dbplyr</code> pipelines. Provides a tidyverse-style interface for tracking data transformations through pipeline operations. There are two vignettes including <a href="https://cran.r-project.org/web/packages/dplyneage/vignettes/getting-started.html" rel="nofollow" target="_blank">Getting Started with dplyneage</a> and <a href="https://cran.r-project.org/web/packages/dplyneage/vignettes/building-reactflow.html" rel="nofollow" target="_blank">Building the React Flow Bundle</a>.</p>
<p><a href="https://i1.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/dplyneage.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-23" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/dplyneage.png?w=578&#038;ssl=1" class="img-fluid" alt="Diagram tracing outputs back to their sources" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=ducklake" rel="nofollow" target="_blank">ducklake</a> v0.6.0: A <code>tidyverse</code>-friendly interface to <a href="https://ducklake.select/" rel="nofollow" target="_blank">DuckLake</a>, the <code>DuckDB</code> lakehouse format. Attach versioned data lakes from <code>R</code> and work with them using familiar <code>dplyr</code> verbs, with support for ACID transactions, time travel queries, snapshot audit trails, data inlining, encrypted storage, multiple catalog backends (<code>DuckDB</code>, <code>PostgreSQL</code>, <code>SQLite</code>, <code>MySQL</code>), and remote access over the ‘Quack’ protocol from <code>DuckDB</code>. There are nine vignettes including <a href="https://cran.r-project.org/web/packages/ducklake/vignettes/ducklake.html" rel="nofollow" target="_blank">ducklake Cookbook</a> and <a href="https://cran.r-project.org/web/packages/ducklake/vignettes/time-travel.html" rel="nofollow" target="_blank">Time Travel Queries</a>.</p>
<p><a href="https://cran.r-project.org/package=getaca" rel="nofollow" target="_blank">getaca</a> v0.1.6: Provides functions to declare, retrieve, verify, track and actively manage external data dependencies too large or too fast-moving to ship inside a package. Resources are identified by package, name and version, pinned to a Secure Hash Algorithm (SHA-256) checksum, and resolved through an explicit policy so that the same installed package always resolves the same bytes. A registry served from a remote host may be signed with Ed25519 and verified against a key the declaring package ships, so the declaration and the key that vouches for it arrive by different routes. Designed for reproducible offline use and graceful behavior during package checks. There are eight vignettes including <a href="https://cran.r-project.org/web/packages/getaca/vignettes/quickstart.html" rel="nofollow" target="_blank">Quick Start</a> and <a href="https://cran.r-project.org/web/packages/getaca/vignettes/policies.html" rel="nofollow" target="_blank">Policies and Channels</a>.</p>
<p><a href="https://cran.r-project.org/package=icebergr" rel="nofollow" target="_blank">icebergr</a> v0.2.0: Implements a native client for <code>Apache Iceberg</code>, the open table format used by <code>Snowflake</code>, <code>Databricks</code>, <code>BigQuery</code>, <code>AWS</code> and <code>Dremio</code> that connects to <code>REST</code> and <code>AWS Glue</code> catalogs, lists namespaces and tables, reads the schema and partition specification of a table, scans data with predicates and projections pushed down, travels back through snapshot history, and appends new data. <code>Apache Arrow</code> is the interchange layer throughout. See the <a href="https://cran.r-project.org/web/packages/icebergr/readme/README.html" rel="nofollow" target="_blank">README</a> for the full matrix of supported and unsupported features. There are seven vignettes including <a href="https://cran.r-project.org/web/packages/icebergr/vignettes/getting-started.html" rel="nofollow" target="_blank">Getting Started</a> and <a href="https://cran.r-project.org/web/packages/icebergr/vignettes/table-format.html" rel="nofollow" target="_blank">What Iceberg Is, and What It Fixes</a>.</p>
<p><a href="https://i1.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/icebergr.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-24" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/icebergr.png?w=578&#038;ssl=1" class="img-fluid" alt="icebergr data path" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=pkgcheck" rel="nofollow" target="_blank">pkgcheck</a> v0.3.4: Check whether a package is ready for submission to the <a href="https://doi.org/10.5281/zenodo.2553043" rel="nofollow" target="_blank">rOpenSci</a> peer review system. Incorporates the <code>goodpractice</code> package and many additional checks, including aspects related to maintenance of online public code repositories. There are four vignettes including <a href="https://cran.r-project.org/web/packages/pkgcheck/vignettes/list-checks.html" rel="nofollow" target="_blank">Current checks</a> and <a href="https://cran.r-project.org/web/packages/pkgcheck/vignettes/extending-checks.html" rel="nofollow" target="_blank">Adding or modifying checks</a>.</p>
</section>
<section id="visualization" class="level3">
<h3 class="anchored" data-anchor-id="visualization">Visualization</h3>
<p><a href="https://cran.r-project.org/package=ggCheysson" rel="nofollow" target="_blank">ggCheysson</a> v1.1.0: Implements for <code>ggplot2</code> the stylistic elements (fonts, hatched patterns, color palettes) used by <em>Emile Cheysson</em> in the <em>Albums de Statistique Graphique</em>, sometimes called the pinnacle of the Golden Age of Statistical Graphics. There are three vignettes including <a href="https://cran.r-project.org/web/packages/ggCheysson/vignettes/getting-started.html" rel="nofollow" target="_blank">Getting Started</a> and <a href="https://cran.r-project.org/web/packages/ggCheysson/vignettes/guerry-maps.html" rel="nofollow" target="_blank">Mapping Guerry’s Moral Statistics with Cheysson Palettes</a>.</p>
<p><a href="https://i2.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/ggCheysson.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-25" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/ggCheysson.png?w=578&#038;ssl=1" class="img-fluid" alt="Map of France showing Cheysson colors" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=ggmosaic2" rel="nofollow" target="_blank">ggmosaic2</a> v0.5.1: Extends <code>ggplot2</code> framework by providing the geom <code>geom_mosaic</code>. Allows fitted loglinear models which can be used to show patterns of association among variables in frequency tables. There are four vignettes including <a href="https://cran.r-project.org/web/packages/ggmosaic2/vignettes/introducing-ggmosaic2.html" rel="nofollow" target="_blank">Introducing ggmosaic2</a> and <a href="https://cran.r-project.org/web/packages/ggmosaic2/vignettes/frequency-table-forms.html" rel="nofollow" target="_blank">Three Forms of Frequency Tables</a>.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/ggmosaic2.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-26" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/september-2026-top-40-new-cran-packages/ggmosaic2.png?w=578&#038;ssl=1" class="img-fluid" alt="Example of a mosaic plot" data-recalc-dims="1"></a></p>
</section>
</div>
</div>



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

		<post-id xmlns="com-wordpress:feed-additions:1">404582</post-id>	</item>
		<item>
		<title>Le prix du jeton s&#8217;effondre. Votre facture, non.</title>
		<link>https://www.r-bloggers.com/2026/10/le-prix-du-jeton-seffondre-votre-facture-non/</link>
		
		<dc:creator><![CDATA[Boris Guarisma]]></dc:creator>
		<pubDate>Thu, 29 Oct 2026 12:23:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://blog.bguarisma.com/prix-du-jeton-cout-tache-reussie</guid>

					<description><![CDATA[<p>Le prix du jeton IA s'effondre, pas votre facture : seul compte le coût d'une tâche réussie, échecs et reprises humaines compris.</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/10/le-prix-du-jeton-seffondre-votre-facture-non/">Le prix du jeton s’effondre. Votre facture, non.</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/prix-du-jeton-cout-tache-reussie"> 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>Le prix de l&#8217;intelligence artificielle baisse plus vite que celui de presque tout ce que l&#8217;industrie a connu. Cette baisse est réelle, mais elle ne mesure pas ce que vous payez. Un projet d&#8217;IA en entreprise n&#8217;achète pas des jetons : il achète des tâches réussies, et chaque tâche réussie porte le coût de celles qui ont échoué. Plus le jeton devient bon marché, plus la rentabilité d&#8217;un projet se joue ailleurs que sur la facture d&#8217;API : dans son <strong>taux de réussite</strong>, et dans le travail humain qu&#8217;il faut <strong>pour rattraper ses échecs</strong>.</p>
<h2>Le jeton n&#8217;est pas un baril de pétrole</h2>
<p>Les chiffres de baisse sont spectaculaires. En septembre 2026, l&#8217;institut de recherche Epoch AI estimait que le coût pour atteindre un niveau de performance donné avait baissé d&#8217;environ 47 % par trimestre depuis 2023, soit une division par 13 chaque année. C&#8217;est six fois plus rapide que la baisse du coût du calcul informatique lui-même.</p>
<p>Il y a pourtant un détail qui compte : Epoch AI ne mesure pas le prix du jeton. Elle mesure ce que coûte l&#8217;atteinte d&#8217;un résultat sur un banc d&#8217;essai, et prend soin de distinguer ces deux grandeurs. La nuance n&#8217;est pas académique, car le jeton n&#8217;a rien d&#8217;une matière première.</p>
<p><strong>Il n&#8217;a pas de prix universel.</strong> Un baril de pétrole se compare à un autre baril. Le jeton d&#8217;un modèle ne vaut pas celui d&#8217;un autre : les tarifs varient d&#8217;un facteur dix ou plus selon le modèle, et les forfaits exprimés en « jetons inclus » sont à peu près illisibles pour un acheteur.</p>
<p><strong>Sa quantité n&#8217;est pas prévisible.</strong> Personne ne sait, avant de lancer une tâche, combien de jetons elle consommera. Le même modèle peut produire une réponse parfaite du premier coup, ou s&#8217;égarer dans des développements inutiles et des relances. D&#8217;un modèle à l&#8217;autre, l&#8217;écart est considérable. En février 2026, le cabinet d&#8217;évaluation Artificial Analysis relevait qu&#8217;à tarif strictement identique, Claude Opus 4.6 avait consommé deux fois plus de jetons de sortie que son prédécesseur Opus 4.5 pour passer la même batterie de tests (58 millions contre 29 millions). Même prix unitaire, deux fois plus de jetons de sortie à payer. Dans le même relevé, GPT-5.2 consommait 130 millions de jetons de sortie, plus du double d&#8217;Opus 4.6, pour une facture totale pourtant comparable : 2 304 dollars contre 2 486. Le prix du jeton et le nombre de jetons varient ensemble, et dans des sens imprévisibles. Pris isolément, aucun des deux ne dit ce que coûte le travail.</p>
<h2>Une image venue de l&#8217;atelier</h2>
<p>Dans l&#8217;industrie, ce problème est résolu depuis longtemps. Aucun directeur d&#8217;usine ne juge une fonderie sur le prix du kilo de métal. Il la juge sur le <strong>coût de la bonne pièce</strong> : ce que coûte une pièce conforme, rebuts et retouches compris. Une fonderie qui vend son métal deux fois moins cher, mais rebute une pièce sur quatre et mobilise un régleur pour reprendre les autres, coûte plus cher que sa concurrente. L&#8217;indicateur qui compte est le <strong>rendement au premier passage</strong> : la part des pièces qui sortent conformes du premier coup, sans retouche.</p>
<p>L&#8217;IA générative relève exactement de cette logique. Le jeton est le kilo de métal. Ce que l&#8217;entreprise achète, c&#8217;est la bonne pièce.</p>
<h2>Le cas : deux devis pour 1 000 notes de frais</h2>
<p>Prenons un cas simulé, mais réaliste. Le directeur administratif et financier d&#8217;une entreprise de taille intermédiaire veut confier à un agent IA le contrôle des notes de frais : environ 1 000 par mois. L&#8217;agent enchaîne trois étapes : lire le justificatif, vérifier la conformité à la politique de remboursement, rédiger la décision.</p>
<p>Deux offres sont sur la table. Le modèle « économique » est affiché à 0,50 € le million de jetons ; le modèle « frontière » à 5 €, dix fois plus. Sur le papier, la décision paraît évidente.</p>
<p>Un pilote d&#8217;un mois corrige une première fois ce calcul. Le modèle économique, moins capable, boucle davantage, relit, se reprend et produit des réponses plus longues : il consomme en moyenne 60 000 jetons par note, contre 15 000 pour le modèle frontière. Le coût de calcul par note s&#8217;établit donc à 0,03 € contre 0,075 €. L&#8217;écart de prix, qui était de un à dix, n&#8217;est plus que de un à deux et demi.</p>
<p>Ce n&#8217;est pourtant que la moitié de l&#8217;histoire. La question qui décide de tout reste ouverte : combien de ces notes sont justes ?</p>
<h2>Chaque réussite paie pour les échecs</h2>
<p>Un agent IA est probabiliste. Il peut se tromper sans le signaler, et produire une décision fausse avec la même assurance qu&#8217;une décision juste.</p>
<p>Sur une chaîne de trois étapes, les erreurs s&#8217;accumulent.</p>
<p>Si le modèle économique réussit chaque étape dans 90 % des cas, les trois étapes réussissent ensemble dans environ 73 % des cas (0,90 × 0,90 × 0,90). Pour le modèle frontière, fiable à 97 % par étape, on obtient environ 91 %. Les industriels connaissent ce calcul sous le nom de <strong>rendement cumulé</strong> : la conformité d&#8217;une ligne est le produit de la conformité de chacun de ses postes. Ce n&#8217;est qu&#8217;un ordre de grandeur, valable si les erreurs sont indépendantes d&#8217;une étape à l&#8217;autre ; seul le pilote dit le chiffre réel. Mais il explique pourquoi une fiabilité qui semble élevée à chaque étape s&#8217;effondre sur la chaîne entière.</p>
<p>Les ingénieurs résument la conséquence économique d&#8217;une formule : <strong>le coût d&#8217;une tâche réussie</strong> est le coût d&#8217;une tentative divisé par la probabilité de succès. Traduit en langage de gestion : <strong>si une note sur quatre échoue, chaque note réussie paie sa propre tentative, plus une part de celles qui ont raté.</strong></p>
<p>Reste à savoir comment on paie un échec. Il existe deux manières.</p>
<ul>
<li><strong>Relancer la machine.</strong> Le coût est dérisoire : pour le modèle économique, 0,03 € divisé par 73 % donne environ 0,04 € par note réussie. Si l&#8217;histoire s&#8217;arrêtait là, le modèle économique gagnerait.</li>
<li><strong>Faire reprendre la note par un comptable.</strong> Une décision de remboursement erronée ne se relance pas à l&#8217;aveugle : quelqu&#8217;un doit reprendre le dossier, vérifier, trancher. Comptons un quart d&#8217;heure à 60 € de l&#8217;heure, soit 15 € par reprise.</li>
</ul>
<p>Dans la plupart des processus métier, c&#8217;est la seconde voie qui s&#8217;impose. Et elle change tout.</p>
<h2>Le verdict : le modèle le moins cher coûte trois fois plus</h2>
<table>
<thead>
<tr>
<th>Sur un mois, 1 000 notes</th>
<th align="right">Modèle « économique »</th>
<th align="right">Modèle « frontière »</th>
</tr>
</thead>
<tbody><tr>
<td>Prix affiché</td>
<td align="right">0,50 € / million de jetons</td>
<td align="right">5 € / million de jetons</td>
</tr>
<tr>
<td>Calcul (jetons)</td>
<td align="right">30 €</td>
<td align="right">75 €</td>
</tr>
<tr>
<td>Notes en échec, reprises par un comptable</td>
<td align="right">271</td>
<td align="right">87</td>
</tr>
<tr>
<td>Coût des reprises (15 € par note)</td>
<td align="right">4 065 €</td>
<td align="right">1 305 €</td>
</tr>
<tr>
<td><strong>Coût par note acceptée</strong></td>
<td align="right"><strong>≈ 4,10 €</strong></td>
<td align="right"><strong>≈ 1,38 €</strong></td>
</tr>
</tbody></table>
<p>Le modèle dix fois moins cher au jeton revient trois fois plus cher par note acceptée. Les jetons pèsent moins de 1 % du coût pour le modèle économique, environ 5 % pour le modèle frontière. Tout le reste correspond à ce que la facture d&#8217;API ne montre jamais : le travail humain qui rattrape les échecs.</p>
<p>Ce tableau est même optimiste. Il suppose que chaque échec est repéré. En pratique, pour trouver les notes fausses, il faut un contrôle, et ce contrôle a lui aussi un coût. Il ne compte pas non plus les serveurs, l&#8217;orchestration ni la maintenance des règles. On ne paie pas un modèle : on paie un dispositif.</p>
<p>La règle de décision tient en une ligne :</p>
<blockquote>
<p><strong>Coût d&#8217;une tâche réussie = tout ce qu&#8217;on a dépensé pour l&#8217;obtenir (tentatives, relances, reprises) ÷ nombre de tâches réussies.</strong></p>
</blockquote>
<p>C&#8217;est le coût de la bonne pièce, appliqué à l&#8217;IA.</p>
<h2>La dérive silencieuse : quand la baisse de prix masque une hausse de coût</h2>
<p>Supposons que le DAF ait fait le bon choix et retenu le modèle frontière. Six mois plus tard, le fournisseur publie une nouvelle version et divise son prix par deux. La facture d&#8217;API passe de 75 € à 37,50 € par mois. Le tableau de bord affiche une économie de 50 %.</p>
<p>Mais un système probabiliste ne tombe pas en panne : il s&#8217;érode. La nouvelle version se comporte un peu différemment, les justificatifs changent de format, la politique de remboursement évolue. Sans qu&#8217;aucune alerte ne se déclenche, le taux de réussite passe de 91 % à 85 %. Cela fait 150 reprises au lieu de 87, soit 2 250 € de travail humain. Le coût par note acceptée passe d&#8217;environ 1,38 € à 2,29 € : <strong>+66 %, au moment même où le prix du jeton a baissé de moitié.</strong></p>
<p>Ces chiffres sont une illustration, pas une loi : une dérive peut être plus lente, plus rapide, ou nulle. C&#8217;est précisément la raison pour laquelle on ne peut pas la supposer et qu&#8217;il faut la mesurer. Celui qui ne suit que la facture de jetons célébrera une économie. Seule la mesure régulière du taux de réussite révèle la hausse.</p>
<h2>Ce qu&#8217;il faut exiger avant de signer</h2>
<p>La baisse du prix du jeton est une bonne nouvelle pour l&#8217;industrie. Pour un décideur, elle déplace la question : plus le calcul devient bon marché, plus le coût d&#8217;une tâche se réduit au coût de ses échecs. Trois questions suffisent à remettre un projet sur ses pieds.</p>
<ol>
<li><strong>Quel est le coût d&#8217;une tâche réussie, échecs et reprises compris ?</strong> Non le prix du jeton, ni le prix d&#8217;une requête.</li>
<li><strong>Quel taux de réussite a été mesuré sur nos propres dossiers ?</strong> Non celui d&#8217;un banc d&#8217;essai public, ni celui qu&#8217;annonce le fournisseur.</li>
<li><strong>Qui mesure ce taux chaque mois, et en particulier après chaque mise à jour du modèle ?</strong></li>
</ol>
<p>Ne construisez plus vos dossiers d&#8217;investissement sur le coût théorique d&#8217;une requête. Évaluez le dispositif complet, et exigez la mesure du coût unitaire par tâche validée. Dans une usine, personne n&#8217;accepterait de piloter une ligne au prix du kilo de métal. Il n&#8217;y a aucune raison de le faire avec l&#8217;IA.</p>
<hr />
<p><em>Pour aller plus loin, le livre blanc gratuit</em> <a href="https://qognito.io/livres-blancs/la-facture-fantome-ia/" rel="nofollow" target="_blank"><em>La facture fantôme de l&#8217;IA</em></a> <em>examine pourquoi les gains annoncés ne produisent pas toujours les économies attendues. Le</em> <a href="https://qognito.io/formations/saga-ia/volume-1/" rel="nofollow" target="_blank"><em>Volume 1 de SAGA-IA</em></a> <em>permet d&#8217;éprouver la viabilité économique de votre propre projet, avec des cas et des exercices corrigés.</em></p>
<p><em>Le cas des notes de frais est une simulation construite pour cet article ; ses montants et ses taux sont des conventions explicites, non des observations.</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://blog.bguarisma.com/prix-du-jeton-cout-tache-reussie"> 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/le-prix-du-jeton-seffondre-votre-facture-non/">Le prix du jeton s’effondre. Votre facture, non.</a>]]></content:encoded>
					
		
		<enclosure url="https://cdn.hashnode.com/uploads/covers/61bc41016916c3593a6b04c3/cdbb60ba-18f4-4402-8da1-39ec9b605cd1.jpg" length="0" type="image/jpeg" />

		<post-id xmlns="com-wordpress:feed-additions:1">404512</post-id>	</item>
		<item>
		<title>Modelling cell-type proportions in repeated-measures designs with sccomp: a TMA case study</title>
		<link>https://www.r-bloggers.com/2026/10/modelling-cell-type-proportions-in-repeated-measures-designs-with-sccomp-a-tma-case-study/</link>
		
		<dc:creator><![CDATA[Stefano Mangiola]]></dc:creator>
		<pubDate>Thu, 29 Oct 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://tidyomics.github.io/tidyomicsBlog/posts/2026-10-23-sccomp-neotrip-compositional-dynamics/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
Contributor: Pierre-Paul Axisa, Researcher, CBMR, Copenhagen, Denmark.</p>
<p>Introduction<br />
Testing cell proportional differences from single-cell-resolved count data is central to modern tissue analyses. It becomes a repeated-measures problem as soon as several observations come from the same donor, such as multiple tumour cores in a tissue microarray (TMA) or serial biopsies ...</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/modelling-cell-type-proportions-in-repeated-measures-designs-with-sccomp-a-tma-case-study/">Modelling cell-type proportions in repeated-measures designs with sccomp: a TMA case study</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://tidyomics.github.io/tidyomicsBlog/posts/2026-10-23-sccomp-neotrip-compositional-dynamics/"> tidyomicsBlog</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="small">
<p>Contributor: Pierre-Paul Axisa, Researcher, CBMR, Copenhagen, Denmark.</p>
</div>
<section id="introduction" class="level1">
<h1>Introduction</h1>
<p>Testing cell proportional differences from single-cell-resolved count data is central to modern tissue analyses. It becomes a repeated-measures problem as soon as several observations come from the same donor, such as multiple tumour cores in a tissue microarray (TMA) or serial biopsies over a time course. Averaging cores up to the patient and testing one phenotype at a time drops both the compositional constraint and the repeated-measures structure.</p>
<p><a href="https://github.com/MangiolaLaboratory/sccomp" rel="nofollow" target="_blank">sccomp</a> is a Bayesian model for mixed-effect compositional designs that identifies outliers probabilistically, tests for differential variability, and removes unwanted effects. It takes a long tibble with one row per sample and cell type, a count column, and the design columns, and <code>sccomp_estimate()</code>, <code>sccomp_test()</code>, and <code>sccomp_predict()</code> return tibbles ready for <code>dplyr</code>, <code>tidyr</code>, and <code>ggplot2</code>.</p>
</section>
<section id="source-study-and-study-design" class="level1">
<h1>Source study and study design</h1>
<p>We apply <a href="https://github.com/MangiolaLaboratory/sccomp" rel="nofollow" target="_blank">sccomp</a> to the NeoTRIP trial. <a href="https://www.nature.com/articles/s41586-023-06498-3" rel="nofollow" target="_blank">Wang <em>et al.</em> (2023)</a> used imaging mass cytometry (IMC) to map how multicellular spatial organization in triple-negative breast cancer relates to neoadjuvant immunotherapy response. In the phase III NeoTRIP trial (NCT02620280), early high-risk TNBC patients were randomised 1:1 to carboplatin + nab-paclitaxel (C) or the same regimen plus atezolizumab (C&#038;I) for eight 3-week neoadjuvant cycles before surgery. Research FFPE biopsies:</p>
<ul>
<li>Baseline (<em>n</em> = 243)</li>
<li>On-treatment (first day of cycle 2; <em>n</em> = 207)</li>
<li>Post-treatment (surgical excision; <em>n</em> = 210)</li>
<li>Profiled with a 43-marker IMC panel at subcellular resolution</li>
<li>Deep-learning segmentation and semi-supervised clustering defined 17 epithelial and 20 TME cell phenotypes</li>
<li>Multiple regions of interest (ROIs) were imaged per biopsy</li>
</ul>
<p>Each patient contributes several samples. The biopsies are nested in the patient, and the regions of interest are nested in the biopsy. Patient NT193, in the chemotherapy arm with residual disease, was imaged at all three timepoints: 10 ROIs at baseline, 10 on-treatment, and 3 at surgery. In the model, each ROI is one compositional sample and the patient is the repeated-measures group.</p>
<div class="cell" data-layout-align="center">
<details class="code-fold">
<summary>One patient, three timepoints, several ROIs</summary>
<pre>mk_spatial_tile &lt;- function(dat, square = TRUE) {
  if (nrow(dat) == 0) {
    dat &lt;- tibble::tibble(
      Location_Center_X = numeric(),
      Location_Center_Y = numeric(),
      draw = character()
    )
  }
  tile &lt;- ggplot(dat, aes(x = Location_Center_X, y = Location_Center_Y)) +
    geom_point(data = filter(dat, draw == &quot;other&quot;), colour = &quot;#39FF14&quot;, size = 0.10) +
    geom_point(data = filter(dat, draw == &quot;pdl1&quot;), colour = &quot;#FFFF00&quot;, size = 0.16) +
    geom_point(data = filter(dat, draw == &quot;m2&quot;), colour = &quot;#FF00FF&quot;, size = 0.16) +
    scale_x_continuous(labels = NULL, guide = &quot;none&quot;, expand = c(0, 0)) +
    scale_y_reverse(labels = NULL, guide = &quot;none&quot;, expand = c(0, 0)) +
    theme_void(base_size = 8) +
    theme(
      plot.background = element_rect(fill = &quot;white&quot;, colour = NA),
      panel.background = element_rect(fill = &quot;black&quot;, colour = NA),
      legend.position = &quot;none&quot;,
      plot.margin = margin(
        if (square) 1 else 0, if (square) 1 else 0,
        if (square) 1 else 0, if (square) 1 else 0,
        unit = &quot;pt&quot;
      )
    )
  if (square) tile &lt;- tile + theme(aspect.ratio = 1)
  tile
}

roi &lt;- read_csv(
  file.path(results_dir, &quot;sccomp_long_tme_immune_roi.csv&quot;),
  col_select = c(PatientID, BiopsyPhase, ImageID),
  show_col_types = FALSE
) |&gt;
  filter(PatientID == &quot;NT193&quot;) |&gt;
  distinct() |&gt;
  mutate(BiopsyPhase = factor(BiopsyPhase, phase_levels)) |&gt;
  arrange(BiopsyPhase, ImageID) |&gt;
  mutate(i = row_number(), .by = BiopsyPhase)

n_phase &lt;- roi |&gt;
  summarise(n = n(), .by = BiopsyPhase) |&gt;
  mutate(x0 = cumsum(n + 2) - (n + 2))
roi &lt;- roi |&gt;
  left_join(n_phase, by = &quot;BiopsyPhase&quot;) |&gt;
  mutate(x = i + x0)

cells_nt &lt;- read_fst(file.path(post_dir, &quot;nt193-cells.fst&quot;)) |&gt;
  as_tibble()
dx &lt;- 0.46
pad &lt;- 0.62
xmin &lt;- min(roi$x) - pad
xmax &lt;- max(roi$x) + pad
fig_w &lt;- 12
fig_h &lt;- 5.4
target_in &lt;- (2 * dx) / (xmax - xmin) * fig_w
headroom &lt;- 1.9
tile_h &lt;- target_in * headroom / (fig_h - target_in)

phase &lt;- roi |&gt;
  summarise(x = mean(x), .by = BiopsyPhase) |&gt;
  mutate(
    y = tile_h + 0.62,
    label = case_match(
      as.character(BiopsyPhase),
      &quot;Baseline&quot; ~ &quot;Baseline\n10 ROIs&quot;,
      &quot;On-treatment&quot; ~ &quot;On-treatment\n10 ROIs&quot;,
      &quot;Post-treatment&quot; ~ &quot;Post-treatment\n3 ROIs&quot;
    )
  )
patient &lt;- tibble::tibble(
  x = mean(phase$x), y = phase$y[1] + 0.95, label = &quot;Donor&quot;
)
edges &lt;- bind_rows(
  phase |&gt; mutate(x, y, xend = patient$x, yend = patient$y),
  roi |&gt; mutate(
    x,
    y = tile_h,
    xend = phase$x[match(BiopsyPhase, phase$BiopsyPhase)],
    yend = phase$y[1]
  )
)
phase_cols &lt;- c(
  Baseline = &quot;#1B9E77&quot;,
  `On-treatment` = &quot;#D95F02&quot;,
  `Post-treatment` = &quot;#7570B3&quot;
)

tree &lt;- ggplot() +
  geom_segment(
    data = edges, aes(x = x, y = y, xend = xend, yend = yend),
    colour = &quot;grey70&quot;, linewidth = 0.4
  ) +
  geom_label(
    data = phase,
    aes(x, y, label = label, fill = BiopsyPhase),
    colour = &quot;white&quot;, fontface = &quot;bold&quot;, size = 3, label.size = 0
  ) +
  geom_label(
    data = patient, aes(x, y, label = label),
    fill = &quot;grey15&quot;, colour = &quot;white&quot;, fontface = &quot;bold&quot;, size = 3.6, label.size = 0
  ) +
  scale_fill_manual(values = phase_cols, guide = &quot;none&quot;) +
  coord_cartesian(
    xlim = c(xmin, xmax),
    ylim = c(-0.04, patient$y + 0.4),
    clip = &quot;off&quot;
  ) +
  labs(title = &quot;Patient NT193&quot;) +
  theme_void(base_size = 11) +
  theme(
    plot.title = element_text(face = &quot;bold&quot;, hjust = 0.5, size = 12),
    plot.margin = margin(6, 6, 6, 6)
  )

for (i in seq_len(nrow(roi))) {
  tree &lt;- tree + annotation_custom(
    ggplotGrob(mk_spatial_tile(filter(cells_nt, ImageID == roi$ImageID[i]), square = FALSE)),
    xmin = roi$x[i] - dx,
    xmax = roi$x[i] + dx,
    ymin = 0,
    ymax = tile_h
  )
}
tree</pre>
</details>
<div class="cell-output-display">
<div id="fig-nesting" class="quarto-float quarto-figure quarto-figure-center anchored" alt="Tree for patient NT193. The patient sits at the top and three biopsy timepoints branch underneath. Each timepoint ends in the imaged regions of interest, drawn as spatial tiles: magenta M2 macrophages, yellow PD-L1-positive antigen-presenting cells, and green other immune cells. There are 10 tiles at baseline, 10 on-treatment, and 3 post-treatment." data-fig-align="center" width="450">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-nesting-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<img src="https://i0.wp.com/tidyomics.github.io/tidyomicsBlog/posts/2026-10-23-sccomp-neotrip-compositional-dynamics/index_files/figure-html/fig-nesting-1.png?w=450&#038;ssl=1" id="fig-nesting" class="img-fluid quarto-figure quarto-figure-center anchored figure-img" alt="Tree for patient NT193. The patient sits at the top and three biopsy timepoints branch underneath. Each timepoint ends in the imaged regions of interest, drawn as spatial tiles: magenta M2 macrophages, yellow PD-L1-positive antigen-presenting cells, and green other immune cells. There are 10 tiles at baseline, 10 on-treatment, and 3 post-treatment." data-recalc-dims="1" />
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig quarto-uncaptioned" id="fig-nesting-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure 1
</figcaption>
</figure>
</div>
</div>
</div>
<p>Wang <em>et al.</em> linked cell densities, spatial interactions, and proliferative fractions to pathological complete response (pCR), and showed that immunotherapy remodels tissue composition over time, with response-specific dynamics of immune infiltration (notably in their Fig. 5 compositional trajectory analysis).</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Quantity</th>
<th style="text-align: right;">Value</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Per-protocol patients</td>
<td style="text-align: right;">257</td>
</tr>
<tr class="even">
<td>ROI-level immune samples</td>
<td style="text-align: right;">1,740</td>
</tr>
<tr class="odd">
<td>Immune cell phenotypes</td>
<td style="text-align: right;">15</td>
</tr>
<tr class="even">
<td>Median ROIs per patient</td>
<td style="text-align: right;">7</td>
</tr>
</tbody>
</table>
</section>
<section id="sccomp-mixed-effects-compositional-modelling" class="level1">
<h1>sccomp mixed-effects compositional modelling</h1>
<p>Wang <em>et al.</em> summarised longitudinal composition as mean phenotype proportions per patient and compared arms, timepoints, and response groups with Wilcoxon tests, <em>Z</em>-scored trend plots (their Fig. 5), and per-phenotype weighted binomial GLMs on tumour-level proportions. Those tests treat each phenotype in isolation and average ROIs to the patient before inference, setting aside compositional closure (proportions sum to one) and the nesting of biopsies and ROIs within patients.</p>
<p>With <a href="https://github.com/MangiolaLaboratory/sccomp" rel="nofollow" target="_blank">sccomp</a>, we model the ROI-level cell counts directly on a latent log-ratio scale with mixed effects on the mean and variability. Each ROI is one compositional sample (<code>sample = &quot;ROI_id&quot;</code> in <code>sccomp_estimate()</code>), several ROIs share a biopsy timepoint (<code>BiopsyPhase</code>), and a patient random intercept <code>(1 | PatientID)</code> accounts for baseline differences across individuals and shrinkage across the cohort:</p>
<p><img src="https://latex.codecogs.com/png.latex?%5Ctext%7Bcomposition%7D%20%5Csim%20%5Ctext%7BArm%7D%20%5Ctimes%20%5Ctext%7BBiopsyPhase%7D%20%5Ctimes%20%5Ctext%7BpCR%7D%20+%20(1%20%5Cmid%20%5Ctext%7BPatientID%7D)"/></p>
</section>
<section id="sccomp-analysis" class="level1">
<h1>sccomp analysis</h1>
<p>The blocks below are the minimal tidyverse code to reproduce the analysis locally from the parsed input (run top to bottom), using the immune TME compartment with ROI-level samples and a patient random intercept.</p>
</section><section id="from-long-format-tidy-input" class="level2">
<h2 class="anchored" data-anchor-id="from-long-format-tidy-input">From long-format tidy input</h2>
<p>The input <code>long</code> is a tibble with one row per ROI × immune phenotype: <code>ROI_id</code>, a count, the design columns (<code>Arm</code>, <code>BiopsyPhase</code>, <code>pCR</code>), and the grouping <code>PatientID</code>. Several ROIs share one <code>BiopsyPhase</code>. Preview:</p>
<div class="cell" data-layout-align="center">
<details open="" class="code-fold">
<summary>Load long-format input</summary>
<pre>library(readr)
library(dplyr)

long &lt;- read_csv(file.path(results_dir, &quot;sccomp_long_tme_immune_roi.csv&quot;),
                 show_col_types = FALSE) |&gt;
  rename(ROI_id = sample) |&gt;
  mutate(
    count = as.integer(count),
    BiopsyPhase = factor(BiopsyPhase, c(&quot;Baseline&quot;, &quot;On-treatment&quot;, &quot;Post-treatment&quot;)),
    Arm = factor(Arm, c(&quot;C&quot;, &quot;C&I&quot;)),
    pCR = factor(pCR, c(&quot;RD&quot;, &quot;pCR&quot;))
  )

long |&gt;
  select(ROI_id, cell_group, count, PatientID, BiopsyPhase, Arm, pCR) |&gt;
  gt::gt_preview() |&gt;
  gt::tab_caption(&quot;Long-format sccomp input (1,740 ROIs x 15 immune phenotypes)&quot;)</pre>
</details>
<div class="cell-output-display">
<div id="doqwnhuyyc" style="padding-left:0px;padding-right:0px;padding-top:10px;padding-bottom:10px;overflow-x:auto;overflow-y:auto;width:auto;height:auto;">
<style>#doqwnhuyyc table {
  font-family: system-ui, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

#doqwnhuyyc thead, #doqwnhuyyc tbody, #doqwnhuyyc tfoot, #doqwnhuyyc tr, #doqwnhuyyc td, #doqwnhuyyc th {
  border-style: none;
}

#doqwnhuyyc p {
  margin: 0;
  padding: 0;
}

#doqwnhuyyc .gt_table {
  display: table;
  border-collapse: collapse;
  line-height: normal;
  margin-left: auto;
  margin-right: auto;
  color: #333333;
  font-size: 16px;
  font-weight: normal;
  font-style: normal;
  background-color: #FFFFFF;
  width: auto;
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #A8A8A8;
  border-right-style: none;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #A8A8A8;
  border-left-style: none;
  border-left-width: 2px;
  border-left-color: #D3D3D3;
}

#doqwnhuyyc .gt_caption {
  padding-top: 4px;
  padding-bottom: 4px;
}

#doqwnhuyyc .gt_title {
  color: #333333;
  font-size: 125%;
  font-weight: initial;
  padding-top: 4px;
  padding-bottom: 4px;
  padding-left: 5px;
  padding-right: 5px;
  border-bottom-color: #FFFFFF;
  border-bottom-width: 0;
}

#doqwnhuyyc .gt_subtitle {
  color: #333333;
  font-size: 85%;
  font-weight: initial;
  padding-top: 3px;
  padding-bottom: 5px;
  padding-left: 5px;
  padding-right: 5px;
  border-top-color: #FFFFFF;
  border-top-width: 0;
}

#doqwnhuyyc .gt_heading {
  background-color: #FFFFFF;
  text-align: center;
  border-bottom-color: #FFFFFF;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
}

#doqwnhuyyc .gt_bottom_border {
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
}

#doqwnhuyyc .gt_col_headings {
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
}

#doqwnhuyyc .gt_col_heading {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: normal;
  text-transform: inherit;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
  vertical-align: bottom;
  padding-top: 5px;
  padding-bottom: 6px;
  padding-left: 5px;
  padding-right: 5px;
  overflow-x: hidden;
}

#doqwnhuyyc .gt_column_spanner_outer {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: normal;
  text-transform: inherit;
  padding-top: 0;
  padding-bottom: 0;
  padding-left: 4px;
  padding-right: 4px;
}

#doqwnhuyyc .gt_column_spanner_outer:first-child {
  padding-left: 0;
}

#doqwnhuyyc .gt_column_spanner_outer:last-child {
  padding-right: 0;
}

#doqwnhuyyc .gt_column_spanner {
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  vertical-align: bottom;
  padding-top: 5px;
  padding-bottom: 5px;
  overflow-x: hidden;
  display: inline-block;
  width: 100%;
}

#doqwnhuyyc .gt_spanner_row {
  border-bottom-style: hidden;
}

#doqwnhuyyc .gt_group_heading {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  text-transform: inherit;
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
  vertical-align: middle;
  text-align: left;
}

#doqwnhuyyc .gt_empty_group_heading {
  padding: 0.5px;
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  vertical-align: middle;
}

#doqwnhuyyc .gt_from_md > :first-child {
  margin-top: 0;
}

#doqwnhuyyc .gt_from_md > :last-child {
  margin-bottom: 0;
}

#doqwnhuyyc .gt_row {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  margin: 10px;
  border-top-style: solid;
  border-top-width: 1px;
  border-top-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
  vertical-align: middle;
  overflow-x: hidden;
}

#doqwnhuyyc .gt_stub {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  text-transform: inherit;
  border-right-style: solid;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
  padding-left: 5px;
  padding-right: 5px;
}

#doqwnhuyyc .gt_stub_row_group {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  text-transform: inherit;
  border-right-style: solid;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
  padding-left: 5px;
  padding-right: 5px;
  vertical-align: top;
}

#doqwnhuyyc .gt_row_group_first td {
  border-top-width: 2px;
}

#doqwnhuyyc .gt_row_group_first th {
  border-top-width: 2px;
}

#doqwnhuyyc .gt_summary_row {
  color: #333333;
  background-color: #FFFFFF;
  text-transform: inherit;
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
}

#doqwnhuyyc .gt_first_summary_row {
  border-top-style: solid;
  border-top-color: #D3D3D3;
}

#doqwnhuyyc .gt_first_summary_row.thick {
  border-top-width: 2px;
}

#doqwnhuyyc .gt_last_summary_row {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
}

#doqwnhuyyc .gt_grand_summary_row {
  color: #333333;
  background-color: #FFFFFF;
  text-transform: inherit;
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
}

#doqwnhuyyc .gt_first_grand_summary_row {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  border-top-style: double;
  border-top-width: 6px;
  border-top-color: #D3D3D3;
}

#doqwnhuyyc .gt_last_grand_summary_row_top {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  border-bottom-style: double;
  border-bottom-width: 6px;
  border-bottom-color: #D3D3D3;
}

#doqwnhuyyc .gt_striped {
  background-color: rgba(128, 128, 128, 0.05);
}

#doqwnhuyyc .gt_table_body {
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
}

#doqwnhuyyc .gt_footnotes {
  color: #333333;
  background-color: #FFFFFF;
  border-bottom-style: none;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 2px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
}

#doqwnhuyyc .gt_footnote {
  margin: 0px;
  font-size: 90%;
  padding-top: 4px;
  padding-bottom: 4px;
  padding-left: 5px;
  padding-right: 5px;
}

#doqwnhuyyc .gt_sourcenotes {
  color: #333333;
  background-color: #FFFFFF;
  border-bottom-style: none;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 2px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
}

#doqwnhuyyc .gt_sourcenote {
  font-size: 90%;
  padding-top: 4px;
  padding-bottom: 4px;
  padding-left: 5px;
  padding-right: 5px;
}

#doqwnhuyyc .gt_left {
  text-align: left;
}

#doqwnhuyyc .gt_center {
  text-align: center;
}

#doqwnhuyyc .gt_right {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

#doqwnhuyyc .gt_font_normal {
  font-weight: normal;
}

#doqwnhuyyc .gt_font_bold {
  font-weight: bold;
}

#doqwnhuyyc .gt_font_italic {
  font-style: italic;
}

#doqwnhuyyc .gt_super {
  font-size: 65%;
}

#doqwnhuyyc .gt_footnote_marks {
  font-size: 75%;
  vertical-align: 0.4em;
  position: initial;
}

#doqwnhuyyc .gt_asterisk {
  font-size: 100%;
  vertical-align: 0;
}

#doqwnhuyyc .gt_indent_1 {
  text-indent: 5px;
}

#doqwnhuyyc .gt_indent_2 {
  text-indent: 10px;
}

#doqwnhuyyc .gt_indent_3 {
  text-indent: 15px;
}

#doqwnhuyyc .gt_indent_4 {
  text-indent: 20px;
}

#doqwnhuyyc .gt_indent_5 {
  text-indent: 25px;
}

#doqwnhuyyc .katex-display {
  display: inline-flex !important;
  margin-bottom: 0.75em !important;
}

#doqwnhuyyc div.Reactable > div.rt-table > div.rt-thead > div.rt-tr.rt-tr-group-header > div.rt-th-group:after {
  height: 0px !important;
}
</style>

<table class="gt_table caption-top table table-sm table-striped small" data-quarto-bootstrap="false">
<caption>Long-format sccomp input (1,740 ROIs x 15 immune phenotypes)</caption>
<thead>
<tr class="gt_col_headings header">
<th id="a::stub" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col"></th>
<th id="ROI_id" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">ROI_id</th>
<th id="cell_group" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">cell_group</th>
<th id="count" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">count</th>
<th id="PatientID" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">PatientID</th>
<th id="BiopsyPhase" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">BiopsyPhase</th>
<th id="Arm" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">Arm</th>
<th id="pCR" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">pCR</th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr class="odd">
<th id="stub_1_1" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">1</th>
<td class="gt_row gt_left" headers="stub_1_1 ROI_id">NTImg0011</td>
<td class="gt_row gt_left" headers="stub_1_1 cell_group">CD8^+T</td>
<td class="gt_row gt_right" headers="stub_1_1 count">19</td>
<td class="gt_row gt_left" headers="stub_1_1 PatientID">NT031</td>
<td class="gt_row gt_left" headers="stub_1_1 BiopsyPhase">Baseline</td>
<td class="gt_row gt_left" headers="stub_1_1 Arm">C&I</td>
<td class="gt_row gt_left" headers="stub_1_1 pCR">RD</td>
</tr>
<tr class="even">
<th id="stub_1_2" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">2</th>
<td class="gt_row gt_left" headers="stub_1_2 ROI_id">NTImg0011</td>
<td class="gt_row gt_left" headers="stub_1_2 cell_group">CD8^+GZMB^+T</td>
<td class="gt_row gt_right" headers="stub_1_2 count">1</td>
<td class="gt_row gt_left" headers="stub_1_2 PatientID">NT031</td>
<td class="gt_row gt_left" headers="stub_1_2 BiopsyPhase">Baseline</td>
<td class="gt_row gt_left" headers="stub_1_2 Arm">C&I</td>
<td class="gt_row gt_left" headers="stub_1_2 pCR">RD</td>
</tr>
<tr class="odd">
<th id="stub_1_3" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">3</th>
<td class="gt_row gt_left" headers="stub_1_3 ROI_id">NTImg0011</td>
<td class="gt_row gt_left" headers="stub_1_3 cell_group">CD8^+TCF1^+T</td>
<td class="gt_row gt_right" headers="stub_1_3 count">1</td>
<td class="gt_row gt_left" headers="stub_1_3 PatientID">NT031</td>
<td class="gt_row gt_left" headers="stub_1_3 BiopsyPhase">Baseline</td>
<td class="gt_row gt_left" headers="stub_1_3 Arm">C&I</td>
<td class="gt_row gt_left" headers="stub_1_3 pCR">RD</td>
</tr>
<tr class="even">
<th id="stub_1_4" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">4</th>
<td class="gt_row gt_left" headers="stub_1_4 ROI_id">NTImg0011</td>
<td class="gt_row gt_left" headers="stub_1_4 cell_group">CD8^+PD1^+T_{Ex}</td>
<td class="gt_row gt_right" headers="stub_1_4 count">7</td>
<td class="gt_row gt_left" headers="stub_1_4 PatientID">NT031</td>
<td class="gt_row gt_left" headers="stub_1_4 BiopsyPhase">Baseline</td>
<td class="gt_row gt_left" headers="stub_1_4 Arm">C&I</td>
<td class="gt_row gt_left" headers="stub_1_4 pCR">RD</td>
</tr>
<tr class="odd">
<th id="stub_1_5" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">5</th>
<td class="gt_row gt_left" headers="stub_1_5 ROI_id">NTImg0011</td>
<td class="gt_row gt_left" headers="stub_1_5 cell_group">CD4^+TCF1^+T</td>
<td class="gt_row gt_right" headers="stub_1_5 count">4</td>
<td class="gt_row gt_left" headers="stub_1_5 PatientID">NT031</td>
<td class="gt_row gt_left" headers="stub_1_5 BiopsyPhase">Baseline</td>
<td class="gt_row gt_left" headers="stub_1_5 Arm">C&I</td>
<td class="gt_row gt_left" headers="stub_1_5 pCR">RD</td>
</tr>
<tr class="even">
<th id="stub_1_6" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier; font-size: x-small; background-color: #E4E4E4">6..26099</th>
<td class="gt_row gt_left" headers="stub_1_6 ROI_id" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_left" headers="stub_1_6 cell_group" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_right" headers="stub_1_6 count" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_left" headers="stub_1_6 PatientID" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_left" headers="stub_1_6 BiopsyPhase" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_left" headers="stub_1_6 Arm" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_left" headers="stub_1_6 pCR" style="background-color: #E4E4E4"></td>
</tr>
<tr class="odd">
<th id="stub_1_7" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">26100</th>
<td class="gt_row gt_left" headers="stub_1_7 ROI_id">NTImg1889</td>
<td class="gt_row gt_left" headers="stub_1_7 cell_group">PD-L1^+IDO^+APCs</td>
<td class="gt_row gt_right" headers="stub_1_7 count">71</td>
<td class="gt_row gt_left" headers="stub_1_7 PatientID">NT091</td>
<td class="gt_row gt_left" headers="stub_1_7 BiopsyPhase">On-treatment</td>
<td class="gt_row gt_left" headers="stub_1_7 Arm">C&I</td>
<td class="gt_row gt_left" headers="stub_1_7 pCR">RD</td>
</tr>
</tbody>
</table>

</div>
</div>
</div>
</section>
<section id="fit-the-model" class="level2">
<h2 class="anchored" data-anchor-id="fit-the-model">Fit the model</h2>
<div class="cell" data-layout-align="center">
<pre>library(sccomp)

fit &lt;- sccomp_estimate(
  long,
  formula_composition = ~ Arm * BiopsyPhase * pCR + (1 | PatientID),
  formula_variability = ~ 1,
  sample = &quot;ROI_id&quot;,
  cell_group = &quot;cell_group&quot;,
  abundance = &quot;count&quot;,
  inference_method = &quot;hmc&quot;,
  cores = parallel::detectCores(),
  verbose = FALSE
) |&gt;
  sccomp_test()</pre>
</div>
<div class="cell" data-layout-align="center" data-cache.extra="2.5.2">
<details open="" class="code-fold">
<summary>Fit and test the model</summary>
<pre>library(sccomp)

fit &lt;- sccomp_estimate(
  long,
  formula_composition = ~ Arm * BiopsyPhase * pCR + (1 | PatientID),
  formula_variability = ~ 1,
  sample = &quot;ROI_id&quot;,
  cell_group = &quot;cell_group&quot;,
  abundance = &quot;count&quot;,
  inference_method = &quot;hmc&quot;,
  cores = parallel::detectCores(),
  verbose = FALSE
) |&gt;
  sccomp_test()</pre>
</details>
</div>
<div class="cell" data-layout-align="center">
<div class="cell-output cell-output-stdout">
<pre>sccomp model
============

Model specifications:
  Family: multi_beta_binomial 
  Composition formula: ~Arm * BiopsyPhase * pCR + (1 | PatientID) 
  Variability formula: ~1 
  Inference method: hmc 

Data: Samples: 1740   Cell groups: 15 

Column prefixes: c_ -&gt; composition parameters  v_ -&gt; variability parameters

Convergence diagnostics:
  For each parameter, n_eff is the effective sample size and R_k_hat is the potential
  scale reduction factor on split chains (at convergence, R_k_hat = 1).

# A tibble: 4,035 × 19
   cell_group parameter  factor c_lower c_effect  c_upper   c_pH0   c_FDR c_rhat
   &lt;chr&gt;      &lt;chr&gt;      &lt;chr&gt;    &lt;dbl&gt;    &lt;dbl&gt;    &lt;dbl&gt;   &lt;dbl&gt;   &lt;dbl&gt;  &lt;dbl&gt;
 1 CD20^+B    (Intercep… &lt;NA&gt;   -0.710   -0.514  -0.314   0       0         1.00
 2 CD20^+B    ArmC&I     Arm    -0.579   -0.292  -0.00208 0.0911  3.66e-2   1.00
 3 CD20^+B    BiopsyPha… Biops… -0.0970   0.134   0.359   0.377   1.30e-1   1.00
 4 CD20^+B    BiopsyPha… Biops…  0.903    1.12    1.33    0       0         1.00
 5 CD20^+B    pCRpCR     pCR    -0.162    0.112   0.390   0.465   2.21e-1   1.00
 6 CD20^+B    ArmC&I:Bi… &lt;NA&gt;   -0.281    0.0545  0.396   0.602   3.17e-1   1.00
 7 CD20^+B    ArmC&I:Bi… &lt;NA&gt;   -0.479   -0.151   0.189   0.384   1.24e-1   1.00
 8 CD20^+B    ArmC&I:pC… &lt;NA&gt;   -0.227    0.164   0.540   0.368   2.55e-1   1.00
 9 CD20^+B    BiopsyPha… &lt;NA&gt;   -0.274    0.0505  0.377   0.625   3.86e-1   1.00
10 CD20^+B    BiopsyPha… &lt;NA&gt;   -0.897   -0.572  -0.259   0.00175 4.17e-4   1.00
# &#x2139; 4,025 more rows
# &#x2139; 10 more variables: c_ess_bulk &lt;dbl&gt;, c_ess_tail &lt;dbl&gt;, v_lower &lt;dbl&gt;,
#   v_effect &lt;dbl&gt;, v_upper &lt;dbl&gt;, v_pH0 &lt;dbl&gt;, v_FDR &lt;dbl&gt;, v_rhat &lt;dbl&gt;,
#   v_ess_bulk &lt;dbl&gt;, v_ess_tail &lt;dbl&gt;</pre>
</div>
</div>
<p><code>sccomp_proportional_fold_change()</code> reports how fitted proportions change between two conditions. The result is descriptive; significance still comes from <code>sccomp_test()</code>. Here, chemoimmunotherapy at surgery, residual disease versus pathological complete response:</p>
<div class="cell" data-layout-align="center">
<details open="" class="code-fold">
<summary>Proportional fold change</summary>
<pre>fit |&gt;
  sccomp_proportional_fold_change(
    formula_composition = ~ Arm * BiopsyPhase * pCR,
    from = &quot;C&I:Post-treatment:RD&quot;,  # chemoimmunotherapy, surgery, residual disease
    to = &quot;C&I:Post-treatment:pCR&quot;    # same arm and visit, pathological complete response
  ) |&gt;
  gt::gt_preview() |&gt;
  gt::tab_caption(
    &quot;Proportional fold change: C&I post-treatment, pCR versus residual disease&quot;
  )</pre>
</details>
<div class="cell-output cell-output-stdout">
<pre>Running standalone generated quantities after 1 MCMC chain, with 1 thread(s) per chain...

Chain 1  Elapsed Time: 0.1 seconds (Generated Quantities) 
Chain 1 finished in 0.0 seconds.</pre>
</div>
<div class="cell-output-display">
<div id="jxjctfsyfc" style="padding-left:0px;padding-right:0px;padding-top:10px;padding-bottom:10px;overflow-x:auto;overflow-y:auto;width:auto;height:auto;">
<style>#jxjctfsyfc table {
  font-family: system-ui, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

#jxjctfsyfc thead, #jxjctfsyfc tbody, #jxjctfsyfc tfoot, #jxjctfsyfc tr, #jxjctfsyfc td, #jxjctfsyfc th {
  border-style: none;
}

#jxjctfsyfc p {
  margin: 0;
  padding: 0;
}

#jxjctfsyfc .gt_table {
  display: table;
  border-collapse: collapse;
  line-height: normal;
  margin-left: auto;
  margin-right: auto;
  color: #333333;
  font-size: 16px;
  font-weight: normal;
  font-style: normal;
  background-color: #FFFFFF;
  width: auto;
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #A8A8A8;
  border-right-style: none;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #A8A8A8;
  border-left-style: none;
  border-left-width: 2px;
  border-left-color: #D3D3D3;
}

#jxjctfsyfc .gt_caption {
  padding-top: 4px;
  padding-bottom: 4px;
}

#jxjctfsyfc .gt_title {
  color: #333333;
  font-size: 125%;
  font-weight: initial;
  padding-top: 4px;
  padding-bottom: 4px;
  padding-left: 5px;
  padding-right: 5px;
  border-bottom-color: #FFFFFF;
  border-bottom-width: 0;
}

#jxjctfsyfc .gt_subtitle {
  color: #333333;
  font-size: 85%;
  font-weight: initial;
  padding-top: 3px;
  padding-bottom: 5px;
  padding-left: 5px;
  padding-right: 5px;
  border-top-color: #FFFFFF;
  border-top-width: 0;
}

#jxjctfsyfc .gt_heading {
  background-color: #FFFFFF;
  text-align: center;
  border-bottom-color: #FFFFFF;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
}

#jxjctfsyfc .gt_bottom_border {
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
}

#jxjctfsyfc .gt_col_headings {
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
}

#jxjctfsyfc .gt_col_heading {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: normal;
  text-transform: inherit;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
  vertical-align: bottom;
  padding-top: 5px;
  padding-bottom: 6px;
  padding-left: 5px;
  padding-right: 5px;
  overflow-x: hidden;
}

#jxjctfsyfc .gt_column_spanner_outer {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: normal;
  text-transform: inherit;
  padding-top: 0;
  padding-bottom: 0;
  padding-left: 4px;
  padding-right: 4px;
}

#jxjctfsyfc .gt_column_spanner_outer:first-child {
  padding-left: 0;
}

#jxjctfsyfc .gt_column_spanner_outer:last-child {
  padding-right: 0;
}

#jxjctfsyfc .gt_column_spanner {
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  vertical-align: bottom;
  padding-top: 5px;
  padding-bottom: 5px;
  overflow-x: hidden;
  display: inline-block;
  width: 100%;
}

#jxjctfsyfc .gt_spanner_row {
  border-bottom-style: hidden;
}

#jxjctfsyfc .gt_group_heading {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  text-transform: inherit;
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
  vertical-align: middle;
  text-align: left;
}

#jxjctfsyfc .gt_empty_group_heading {
  padding: 0.5px;
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  vertical-align: middle;
}

#jxjctfsyfc .gt_from_md > :first-child {
  margin-top: 0;
}

#jxjctfsyfc .gt_from_md > :last-child {
  margin-bottom: 0;
}

#jxjctfsyfc .gt_row {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  margin: 10px;
  border-top-style: solid;
  border-top-width: 1px;
  border-top-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
  vertical-align: middle;
  overflow-x: hidden;
}

#jxjctfsyfc .gt_stub {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  text-transform: inherit;
  border-right-style: solid;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
  padding-left: 5px;
  padding-right: 5px;
}

#jxjctfsyfc .gt_stub_row_group {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  text-transform: inherit;
  border-right-style: solid;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
  padding-left: 5px;
  padding-right: 5px;
  vertical-align: top;
}

#jxjctfsyfc .gt_row_group_first td {
  border-top-width: 2px;
}

#jxjctfsyfc .gt_row_group_first th {
  border-top-width: 2px;
}

#jxjctfsyfc .gt_summary_row {
  color: #333333;
  background-color: #FFFFFF;
  text-transform: inherit;
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
}

#jxjctfsyfc .gt_first_summary_row {
  border-top-style: solid;
  border-top-color: #D3D3D3;
}

#jxjctfsyfc .gt_first_summary_row.thick {
  border-top-width: 2px;
}

#jxjctfsyfc .gt_last_summary_row {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
}

#jxjctfsyfc .gt_grand_summary_row {
  color: #333333;
  background-color: #FFFFFF;
  text-transform: inherit;
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
}

#jxjctfsyfc .gt_first_grand_summary_row {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  border-top-style: double;
  border-top-width: 6px;
  border-top-color: #D3D3D3;
}

#jxjctfsyfc .gt_last_grand_summary_row_top {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  border-bottom-style: double;
  border-bottom-width: 6px;
  border-bottom-color: #D3D3D3;
}

#jxjctfsyfc .gt_striped {
  background-color: rgba(128, 128, 128, 0.05);
}

#jxjctfsyfc .gt_table_body {
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
}

#jxjctfsyfc .gt_footnotes {
  color: #333333;
  background-color: #FFFFFF;
  border-bottom-style: none;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 2px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
}

#jxjctfsyfc .gt_footnote {
  margin: 0px;
  font-size: 90%;
  padding-top: 4px;
  padding-bottom: 4px;
  padding-left: 5px;
  padding-right: 5px;
}

#jxjctfsyfc .gt_sourcenotes {
  color: #333333;
  background-color: #FFFFFF;
  border-bottom-style: none;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 2px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
}

#jxjctfsyfc .gt_sourcenote {
  font-size: 90%;
  padding-top: 4px;
  padding-bottom: 4px;
  padding-left: 5px;
  padding-right: 5px;
}

#jxjctfsyfc .gt_left {
  text-align: left;
}

#jxjctfsyfc .gt_center {
  text-align: center;
}

#jxjctfsyfc .gt_right {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

#jxjctfsyfc .gt_font_normal {
  font-weight: normal;
}

#jxjctfsyfc .gt_font_bold {
  font-weight: bold;
}

#jxjctfsyfc .gt_font_italic {
  font-style: italic;
}

#jxjctfsyfc .gt_super {
  font-size: 65%;
}

#jxjctfsyfc .gt_footnote_marks {
  font-size: 75%;
  vertical-align: 0.4em;
  position: initial;
}

#jxjctfsyfc .gt_asterisk {
  font-size: 100%;
  vertical-align: 0;
}

#jxjctfsyfc .gt_indent_1 {
  text-indent: 5px;
}

#jxjctfsyfc .gt_indent_2 {
  text-indent: 10px;
}

#jxjctfsyfc .gt_indent_3 {
  text-indent: 15px;
}

#jxjctfsyfc .gt_indent_4 {
  text-indent: 20px;
}

#jxjctfsyfc .gt_indent_5 {
  text-indent: 25px;
}

#jxjctfsyfc .katex-display {
  display: inline-flex !important;
  margin-bottom: 0.75em !important;
}

#jxjctfsyfc div.Reactable > div.rt-table > div.rt-thead > div.rt-tr.rt-tr-group-header > div.rt-th-group:after {
  height: 0px !important;
}
</style>

<table class="gt_table caption-top table table-sm table-striped small" data-quarto-bootstrap="false">
<caption>Proportional fold change: C&I post-treatment, pCR versus residual disease</caption>
<thead>
<tr class="gt_col_headings header">
<th id="a::stub" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col"></th>
<th id="cell_group" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">cell_group</th>
<th id="proportion_fold_change" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">proportion_fold_change</th>
<th id="average_uncertainty" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">average_uncertainty</th>
<th id="statement" class="gt_col_heading gt_columns_bottom_border gt_center" data-quarto-table-cell-role="th" scope="col">statement</th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr class="odd">
<th id="stub_1_1" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">1</th>
<td class="gt_row gt_left" headers="stub_1_1 cell_group">CD20^+B</td>
<td class="gt_row gt_right" headers="stub_1_1 proportion_fold_change">-1.300258</td>
<td class="gt_row gt_right" headers="stub_1_1 average_uncertainty">0.02649652</td>
<td class="gt_row gt_center" headers="stub_1_1 statement">1.3-fold decrease (from 0.0645 to 0.0496)</td>
</tr>
<tr class="even">
<th id="stub_1_2" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">2</th>
<td class="gt_row gt_left" headers="stub_1_2 cell_group">CD4^+PD1^+T</td>
<td class="gt_row gt_right" headers="stub_1_2 proportion_fold_change">-1.595534</td>
<td class="gt_row gt_right" headers="stub_1_2 average_uncertainty">0.04749542</td>
<td class="gt_row gt_center" headers="stub_1_2 statement">1.6-fold decrease (from 0.0439 to 0.0275)</td>
</tr>
<tr class="odd">
<th id="stub_1_3" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">3</th>
<td class="gt_row gt_left" headers="stub_1_3 cell_group">CD4^+TCF1^+T</td>
<td class="gt_row gt_right" headers="stub_1_3 proportion_fold_change">-1.100683</td>
<td class="gt_row gt_right" headers="stub_1_3 average_uncertainty">0.01633892</td>
<td class="gt_row gt_center" headers="stub_1_3 statement">1.1-fold decrease (from 0.0918 to 0.0834)</td>
</tr>
<tr class="even">
<th id="stub_1_4" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">4</th>
<td class="gt_row gt_left" headers="stub_1_4 cell_group">CD56^+NK</td>
<td class="gt_row gt_right" headers="stub_1_4 proportion_fold_change">1.571215</td>
<td class="gt_row gt_right" headers="stub_1_4 average_uncertainty">0.04918382</td>
<td class="gt_row gt_center" headers="stub_1_4 statement">1.6-fold increase (from 0.0808 to 0.1269)</td>
</tr>
<tr class="odd">
<th id="stub_1_5" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">5</th>
<td class="gt_row gt_left" headers="stub_1_5 cell_group">CD79a^+Plasma</td>
<td class="gt_row gt_right" headers="stub_1_5 proportion_fold_change">1.132715</td>
<td class="gt_row gt_right" headers="stub_1_5 average_uncertainty">0.02740352</td>
<td class="gt_row gt_center" headers="stub_1_5 statement">1.1-fold increase (from 0.0721 to 0.0816)</td>
</tr>
<tr class="even">
<th id="stub_1_6" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier; font-size: x-small; background-color: #E4E4E4">6..14</th>
<td class="gt_row gt_left" headers="stub_1_6 cell_group" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_right" headers="stub_1_6 proportion_fold_change" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_right" headers="stub_1_6 average_uncertainty" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_center" headers="stub_1_6 statement" style="background-color: #E4E4E4"></td>
</tr>
<tr class="odd">
<th id="stub_1_7" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">15</th>
<td class="gt_row gt_left" headers="stub_1_7 cell_group">Treg</td>
<td class="gt_row gt_right" headers="stub_1_7 proportion_fold_change">-3.406119</td>
<td class="gt_row gt_right" headers="stub_1_7 average_uncertainty">0.25870011</td>
<td class="gt_row gt_center" headers="stub_1_7 statement">3.4-fold decrease (from 0.0275 to 0.0081)</td>
</tr>
</tbody>
</table>

</div>
</div>
</div>
</section>
<section id="model-checks" class="level2">
<h2 class="anchored" data-anchor-id="model-checks">Model checks</h2>
<p><strong>Convergence.</strong> All 4,035 composition parameters have median <img src="https://latex.codecogs.com/png.latex?%5Chat%7BR%7D"/> = 1.001 (maximum 1.014). Bulk effective sample size has median 4,837 (minimum 495). Tail effective sample size has median 2,850 (minimum 923).</p>
<p>Variability is an intercept for each phenotype. Those 15 coefficients have maximum <img src="https://latex.codecogs.com/png.latex?%5Chat%7BR%7D"/> = 1.003, minimum bulk ESS 3,404, and minimum tail ESS 2,451. The composition parameters with the smallest bulk ESS:</p>
<div class="cell" data-layout-align="center">
<details open="" class="code-fold">
<summary>Smallest bulk effective sample sizes</summary>
<pre>fit |&gt;
  filter(is.finite(c_ess_bulk)) |&gt;
  slice_min(c_ess_bulk, n = 6, with_ties = FALSE) |&gt;
  gt::gt_preview() |&gt;
  gt::tab_caption(
    &quot;Composition parameters with the smallest bulk effective sample size&quot;
  )</pre>
</details>
<div class="cell-output-display">
<div id="mbrzptoegs" style="padding-left:0px;padding-right:0px;padding-top:10px;padding-bottom:10px;overflow-x:auto;overflow-y:auto;width:auto;height:auto;">
<style>#mbrzptoegs table {
  font-family: system-ui, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

#mbrzptoegs thead, #mbrzptoegs tbody, #mbrzptoegs tfoot, #mbrzptoegs tr, #mbrzptoegs td, #mbrzptoegs th {
  border-style: none;
}

#mbrzptoegs p {
  margin: 0;
  padding: 0;
}

#mbrzptoegs .gt_table {
  display: table;
  border-collapse: collapse;
  line-height: normal;
  margin-left: auto;
  margin-right: auto;
  color: #333333;
  font-size: 16px;
  font-weight: normal;
  font-style: normal;
  background-color: #FFFFFF;
  width: auto;
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #A8A8A8;
  border-right-style: none;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #A8A8A8;
  border-left-style: none;
  border-left-width: 2px;
  border-left-color: #D3D3D3;
}

#mbrzptoegs .gt_caption {
  padding-top: 4px;
  padding-bottom: 4px;
}

#mbrzptoegs .gt_title {
  color: #333333;
  font-size: 125%;
  font-weight: initial;
  padding-top: 4px;
  padding-bottom: 4px;
  padding-left: 5px;
  padding-right: 5px;
  border-bottom-color: #FFFFFF;
  border-bottom-width: 0;
}

#mbrzptoegs .gt_subtitle {
  color: #333333;
  font-size: 85%;
  font-weight: initial;
  padding-top: 3px;
  padding-bottom: 5px;
  padding-left: 5px;
  padding-right: 5px;
  border-top-color: #FFFFFF;
  border-top-width: 0;
}

#mbrzptoegs .gt_heading {
  background-color: #FFFFFF;
  text-align: center;
  border-bottom-color: #FFFFFF;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
}

#mbrzptoegs .gt_bottom_border {
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
}

#mbrzptoegs .gt_col_headings {
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
}

#mbrzptoegs .gt_col_heading {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: normal;
  text-transform: inherit;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
  vertical-align: bottom;
  padding-top: 5px;
  padding-bottom: 6px;
  padding-left: 5px;
  padding-right: 5px;
  overflow-x: hidden;
}

#mbrzptoegs .gt_column_spanner_outer {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: normal;
  text-transform: inherit;
  padding-top: 0;
  padding-bottom: 0;
  padding-left: 4px;
  padding-right: 4px;
}

#mbrzptoegs .gt_column_spanner_outer:first-child {
  padding-left: 0;
}

#mbrzptoegs .gt_column_spanner_outer:last-child {
  padding-right: 0;
}

#mbrzptoegs .gt_column_spanner {
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  vertical-align: bottom;
  padding-top: 5px;
  padding-bottom: 5px;
  overflow-x: hidden;
  display: inline-block;
  width: 100%;
}

#mbrzptoegs .gt_spanner_row {
  border-bottom-style: hidden;
}

#mbrzptoegs .gt_group_heading {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  text-transform: inherit;
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
  vertical-align: middle;
  text-align: left;
}

#mbrzptoegs .gt_empty_group_heading {
  padding: 0.5px;
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  vertical-align: middle;
}

#mbrzptoegs .gt_from_md > :first-child {
  margin-top: 0;
}

#mbrzptoegs .gt_from_md > :last-child {
  margin-bottom: 0;
}

#mbrzptoegs .gt_row {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  margin: 10px;
  border-top-style: solid;
  border-top-width: 1px;
  border-top-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
  vertical-align: middle;
  overflow-x: hidden;
}

#mbrzptoegs .gt_stub {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  text-transform: inherit;
  border-right-style: solid;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
  padding-left: 5px;
  padding-right: 5px;
}

#mbrzptoegs .gt_stub_row_group {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  text-transform: inherit;
  border-right-style: solid;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
  padding-left: 5px;
  padding-right: 5px;
  vertical-align: top;
}

#mbrzptoegs .gt_row_group_first td {
  border-top-width: 2px;
}

#mbrzptoegs .gt_row_group_first th {
  border-top-width: 2px;
}

#mbrzptoegs .gt_summary_row {
  color: #333333;
  background-color: #FFFFFF;
  text-transform: inherit;
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
}

#mbrzptoegs .gt_first_summary_row {
  border-top-style: solid;
  border-top-color: #D3D3D3;
}

#mbrzptoegs .gt_first_summary_row.thick {
  border-top-width: 2px;
}

#mbrzptoegs .gt_last_summary_row {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
}

#mbrzptoegs .gt_grand_summary_row {
  color: #333333;
  background-color: #FFFFFF;
  text-transform: inherit;
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
}

#mbrzptoegs .gt_first_grand_summary_row {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  border-top-style: double;
  border-top-width: 6px;
  border-top-color: #D3D3D3;
}

#mbrzptoegs .gt_last_grand_summary_row_top {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  border-bottom-style: double;
  border-bottom-width: 6px;
  border-bottom-color: #D3D3D3;
}

#mbrzptoegs .gt_striped {
  background-color: rgba(128, 128, 128, 0.05);
}

#mbrzptoegs .gt_table_body {
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
}

#mbrzptoegs .gt_footnotes {
  color: #333333;
  background-color: #FFFFFF;
  border-bottom-style: none;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 2px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
}

#mbrzptoegs .gt_footnote {
  margin: 0px;
  font-size: 90%;
  padding-top: 4px;
  padding-bottom: 4px;
  padding-left: 5px;
  padding-right: 5px;
}

#mbrzptoegs .gt_sourcenotes {
  color: #333333;
  background-color: #FFFFFF;
  border-bottom-style: none;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 2px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
}

#mbrzptoegs .gt_sourcenote {
  font-size: 90%;
  padding-top: 4px;
  padding-bottom: 4px;
  padding-left: 5px;
  padding-right: 5px;
}

#mbrzptoegs .gt_left {
  text-align: left;
}

#mbrzptoegs .gt_center {
  text-align: center;
}

#mbrzptoegs .gt_right {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

#mbrzptoegs .gt_font_normal {
  font-weight: normal;
}

#mbrzptoegs .gt_font_bold {
  font-weight: bold;
}

#mbrzptoegs .gt_font_italic {
  font-style: italic;
}

#mbrzptoegs .gt_super {
  font-size: 65%;
}

#mbrzptoegs .gt_footnote_marks {
  font-size: 75%;
  vertical-align: 0.4em;
  position: initial;
}

#mbrzptoegs .gt_asterisk {
  font-size: 100%;
  vertical-align: 0;
}

#mbrzptoegs .gt_indent_1 {
  text-indent: 5px;
}

#mbrzptoegs .gt_indent_2 {
  text-indent: 10px;
}

#mbrzptoegs .gt_indent_3 {
  text-indent: 15px;
}

#mbrzptoegs .gt_indent_4 {
  text-indent: 20px;
}

#mbrzptoegs .gt_indent_5 {
  text-indent: 25px;
}

#mbrzptoegs .katex-display {
  display: inline-flex !important;
  margin-bottom: 0.75em !important;
}

#mbrzptoegs div.Reactable > div.rt-table > div.rt-thead > div.rt-tr.rt-tr-group-header > div.rt-th-group:after {
  height: 0px !important;
}
</style>

<table class="gt_table caption-top table table-sm table-striped small" data-quarto-bootstrap="false">
<caption>Composition parameters with the smallest bulk effective sample size</caption>
<thead>
<tr class="gt_col_headings header">
<th id="a::stub" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col"></th>
<th id="cell_group" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">cell_group</th>
<th id="parameter" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">parameter</th>
<th id="factor" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">factor</th>
<th id="c_lower" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">c_lower</th>
<th id="c_effect" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">c_effect</th>
<th id="c_upper" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">c_upper</th>
<th id="c_pH0" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">c_pH0</th>
<th id="c_FDR" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">c_FDR</th>
<th id="c_rhat" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">c_rhat</th>
<th id="c_ess_bulk" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">c_ess_bulk</th>
<th id="c_ess_tail" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">c_ess_tail</th>
<th id="v_lower" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">v_lower</th>
<th id="v_effect" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">v_effect</th>
<th id="v_upper" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">v_upper</th>
<th id="v_pH0" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">v_pH0</th>
<th id="v_FDR" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">v_FDR</th>
<th id="v_rhat" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">v_rhat</th>
<th id="v_ess_bulk" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">v_ess_bulk</th>
<th id="v_ess_tail" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">v_ess_tail</th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr class="odd">
<th id="stub_1_1" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">1</th>
<td class="gt_row gt_left" headers="stub_1_1 cell_group">DCs</td>
<td class="gt_row gt_left" headers="stub_1_1 parameter">ArmC&I</td>
<td class="gt_row gt_left" headers="stub_1_1 factor">Arm</td>
<td class="gt_row gt_right" headers="stub_1_1 c_lower">-0.08375482</td>
<td class="gt_row gt_right" headers="stub_1_1 c_effect">0.1601228</td>
<td class="gt_row gt_right" headers="stub_1_1 c_upper">0.399447699</td>
<td class="gt_row gt_right" headers="stub_1_1 c_pH0">0.31431431</td>
<td class="gt_row gt_right" headers="stub_1_1 c_FDR">0.148577149</td>
<td class="gt_row gt_right" headers="stub_1_1 c_rhat">1.009882</td>
<td class="gt_row gt_right" headers="stub_1_1 c_ess_bulk">600.7795</td>
<td class="gt_row gt_right" headers="stub_1_1 c_ess_tail">1411.607</td>
<td class="gt_row gt_right" headers="stub_1_1 v_lower">NA</td>
<td class="gt_row gt_right" headers="stub_1_1 v_effect">NA</td>
<td class="gt_row gt_right" headers="stub_1_1 v_upper">NA</td>
<td class="gt_row gt_right" headers="stub_1_1 v_pH0">NA</td>
<td class="gt_row gt_right" headers="stub_1_1 v_FDR">NA</td>
<td class="gt_row gt_right" headers="stub_1_1 v_rhat">NA</td>
<td class="gt_row gt_right" headers="stub_1_1 v_ess_bulk">NA</td>
<td class="gt_row gt_right" headers="stub_1_1 v_ess_tail">NA</td>
</tr>
<tr class="even">
<th id="stub_1_2" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">2</th>
<td class="gt_row gt_left" headers="stub_1_2 cell_group">DCs</td>
<td class="gt_row gt_left" headers="stub_1_2 parameter">(Intercept)</td>
<td class="gt_row gt_left" headers="stub_1_2 factor">NA</td>
<td class="gt_row gt_right" headers="stub_1_2 c_lower">0.77417609</td>
<td class="gt_row gt_right" headers="stub_1_2 c_effect">0.9424418</td>
<td class="gt_row gt_right" headers="stub_1_2 c_upper">1.116479608</td>
<td class="gt_row gt_right" headers="stub_1_2 c_pH0">0.00000000</td>
<td class="gt_row gt_right" headers="stub_1_2 c_FDR">0.000000000</td>
<td class="gt_row gt_right" headers="stub_1_2 c_rhat">1.011012</td>
<td class="gt_row gt_right" headers="stub_1_2 c_ess_bulk">697.7882</td>
<td class="gt_row gt_right" headers="stub_1_2 c_ess_tail">1552.972</td>
<td class="gt_row gt_right" headers="stub_1_2 v_lower">-3.78283</td>
<td class="gt_row gt_right" headers="stub_1_2 v_effect">-2.957539</td>
<td class="gt_row gt_right" headers="stub_1_2 v_upper">-2.134048</td>
<td class="gt_row gt_right" headers="stub_1_2 v_pH0">0</td>
<td class="gt_row gt_right" headers="stub_1_2 v_FDR">0</td>
<td class="gt_row gt_right" headers="stub_1_2 v_rhat">1.001649</td>
<td class="gt_row gt_right" headers="stub_1_2 v_ess_bulk">4598.984</td>
<td class="gt_row gt_right" headers="stub_1_2 v_ess_tail">2582.758</td>
</tr>
<tr class="odd">
<th id="stub_1_3" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">3</th>
<td class="gt_row gt_left" headers="stub_1_3 cell_group">DCs</td>
<td class="gt_row gt_left" headers="stub_1_3 parameter">ArmC&I:pCRpCR</td>
<td class="gt_row gt_left" headers="stub_1_3 factor">NA</td>
<td class="gt_row gt_right" headers="stub_1_3 c_lower">-0.47084651</td>
<td class="gt_row gt_right" headers="stub_1_3 c_effect">-0.1203059</td>
<td class="gt_row gt_right" headers="stub_1_3 c_upper">0.229162709</td>
<td class="gt_row gt_right" headers="stub_1_3 c_pH0">0.45070070</td>
<td class="gt_row gt_right" headers="stub_1_3 c_FDR">0.295827077</td>
<td class="gt_row gt_right" headers="stub_1_3 c_rhat">1.010088</td>
<td class="gt_row gt_right" headers="stub_1_3 c_ess_bulk">717.1771</td>
<td class="gt_row gt_right" headers="stub_1_3 c_ess_tail">1444.238</td>
<td class="gt_row gt_right" headers="stub_1_3 v_lower">NA</td>
<td class="gt_row gt_right" headers="stub_1_3 v_effect">NA</td>
<td class="gt_row gt_right" headers="stub_1_3 v_upper">NA</td>
<td class="gt_row gt_right" headers="stub_1_3 v_pH0">NA</td>
<td class="gt_row gt_right" headers="stub_1_3 v_FDR">NA</td>
<td class="gt_row gt_right" headers="stub_1_3 v_rhat">NA</td>
<td class="gt_row gt_right" headers="stub_1_3 v_ess_bulk">NA</td>
<td class="gt_row gt_right" headers="stub_1_3 v_ess_tail">NA</td>
</tr>
<tr class="even">
<th id="stub_1_4" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">4</th>
<td class="gt_row gt_left" headers="stub_1_4 cell_group">M2 Mac</td>
<td class="gt_row gt_left" headers="stub_1_4 parameter">ArmC&I</td>
<td class="gt_row gt_left" headers="stub_1_4 factor">Arm</td>
<td class="gt_row gt_right" headers="stub_1_4 c_lower">0.12079694</td>
<td class="gt_row gt_right" headers="stub_1_4 c_effect">0.3597968</td>
<td class="gt_row gt_right" headers="stub_1_4 c_upper">0.601198700</td>
<td class="gt_row gt_right" headers="stub_1_4 c_pH0">0.01701702</td>
<td class="gt_row gt_right" headers="stub_1_4 c_FDR">0.009384384</td>
<td class="gt_row gt_right" headers="stub_1_4 c_rhat">1.019933</td>
<td class="gt_row gt_right" headers="stub_1_4 c_ess_bulk">776.9721</td>
<td class="gt_row gt_right" headers="stub_1_4 c_ess_tail">1483.882</td>
<td class="gt_row gt_right" headers="stub_1_4 v_lower">NA</td>
<td class="gt_row gt_right" headers="stub_1_4 v_effect">NA</td>
<td class="gt_row gt_right" headers="stub_1_4 v_upper">NA</td>
<td class="gt_row gt_right" headers="stub_1_4 v_pH0">NA</td>
<td class="gt_row gt_right" headers="stub_1_4 v_FDR">NA</td>
<td class="gt_row gt_right" headers="stub_1_4 v_rhat">NA</td>
<td class="gt_row gt_right" headers="stub_1_4 v_ess_bulk">NA</td>
<td class="gt_row gt_right" headers="stub_1_4 v_ess_tail">NA</td>
</tr>
<tr class="odd">
<th id="stub_1_5" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">5</th>
<td class="gt_row gt_left" headers="stub_1_5 cell_group">DCs</td>
<td class="gt_row gt_left" headers="stub_1_5 parameter">pCRpCR</td>
<td class="gt_row gt_left" headers="stub_1_5 factor">pCR</td>
<td class="gt_row gt_right" headers="stub_1_5 c_lower">-0.52733358</td>
<td class="gt_row gt_right" headers="stub_1_5 c_effect">-0.2649256</td>
<td class="gt_row gt_right" headers="stub_1_5 c_upper">-0.008725984</td>
<td class="gt_row gt_right" headers="stub_1_5 c_pH0">0.10010010</td>
<td class="gt_row gt_right" headers="stub_1_5 c_FDR">0.057807808</td>
<td class="gt_row gt_right" headers="stub_1_5 c_rhat">1.007276</td>
<td class="gt_row gt_right" headers="stub_1_5 c_ess_bulk">858.1370</td>
<td class="gt_row gt_right" headers="stub_1_5 c_ess_tail">1509.393</td>
<td class="gt_row gt_right" headers="stub_1_5 v_lower">NA</td>
<td class="gt_row gt_right" headers="stub_1_5 v_effect">NA</td>
<td class="gt_row gt_right" headers="stub_1_5 v_upper">NA</td>
<td class="gt_row gt_right" headers="stub_1_5 v_pH0">NA</td>
<td class="gt_row gt_right" headers="stub_1_5 v_FDR">NA</td>
<td class="gt_row gt_right" headers="stub_1_5 v_rhat">NA</td>
<td class="gt_row gt_right" headers="stub_1_5 v_ess_bulk">NA</td>
<td class="gt_row gt_right" headers="stub_1_5 v_ess_tail">NA</td>
</tr>
<tr class="even">
<th id="stub_1_6" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">6</th>
<td class="gt_row gt_left" headers="stub_1_6 cell_group">M2 Mac</td>
<td class="gt_row gt_left" headers="stub_1_6 parameter">ArmC&I:pCRpCR</td>
<td class="gt_row gt_left" headers="stub_1_6 factor">NA</td>
<td class="gt_row gt_right" headers="stub_1_6 c_lower">-0.74712725</td>
<td class="gt_row gt_right" headers="stub_1_6 c_effect">-0.4227944</td>
<td class="gt_row gt_right" headers="stub_1_6 c_upper">-0.071407724</td>
<td class="gt_row gt_right" headers="stub_1_6 c_pH0">0.03678679</td>
<td class="gt_row gt_right" headers="stub_1_6 c_FDR">0.036786787</td>
<td class="gt_row gt_right" headers="stub_1_6 c_rhat">1.008612</td>
<td class="gt_row gt_right" headers="stub_1_6 c_ess_bulk">878.2435</td>
<td class="gt_row gt_right" headers="stub_1_6 c_ess_tail">1583.149</td>
<td class="gt_row gt_right" headers="stub_1_6 v_lower">NA</td>
<td class="gt_row gt_right" headers="stub_1_6 v_effect">NA</td>
<td class="gt_row gt_right" headers="stub_1_6 v_upper">NA</td>
<td class="gt_row gt_right" headers="stub_1_6 v_pH0">NA</td>
<td class="gt_row gt_right" headers="stub_1_6 v_FDR">NA</td>
<td class="gt_row gt_right" headers="stub_1_6 v_rhat">NA</td>
<td class="gt_row gt_right" headers="stub_1_6 v_ess_bulk">NA</td>
<td class="gt_row gt_right" headers="stub_1_6 v_ess_tail">NA</td>
</tr>
</tbody>
</table>

</div>
</div>
</div>
<p><strong>Posterior predictive check.</strong> <code>remove_unwanted_effects = TRUE</code> removes variation other than biopsy timepoint, so the black boxes are adjusted proportions and the blue boxes are posterior predictive replicates. Fill marks a timepoint contrast with FDR below 0.05.</p>
<div class="cell" data-layout-align="center">
<pre>fit |&gt; 
  sccomp_boxplot(
  factor = &quot;BiopsyPhase&quot;,
  significance_statistic = &quot;FDR&quot;,
  remove_unwanted_effects = TRUE
  ) +
  labs(
    title = &quot;Posterior predictive check&quot;,
    subtitle = &quot;Black: adjusted proportions. Blue: replicates from the fitted model.&quot;,
    x = &quot;Biopsy timepoint&quot;
  ) +
  guides(colour = &quot;none&quot;, shape = &quot;none&quot;) </pre>
<div class="cell-output-display">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://i2.wp.com/tidyomics.github.io/tidyomicsBlog/posts/2026-10-23-sccomp-neotrip-compositional-dynamics/index_files/figure-html/repro-ppc-1.png?w=578&#038;ssl=1" class="img-fluid quarto-figure quarto-figure-center figure-img" alt="Boxplots of immune-cell proportions by biopsy timepoint, faceted by cell type, after removing variation other than biopsy timepoint. Black boxes are adjusted proportions and blue boxes are posterior predictive replicates. Filled boxes mark a timepoint contrast with FDR below 0.05." data-recalc-dims="1"/></p>
</figure>
</div>
</div>
</div>
</section>
<section id="hypothesis-testing" class="level2">
<h2 class="anchored" data-anchor-id="hypothesis-testing">Hypothesis testing</h2>
<p>We pass contrasts to <code>sccomp_test()</code> as strings.</p>
<p><code>marginal_CI_post</code> compares responders with residual disease at that visit alone. Any gap the two groups already had at baseline is still in this result.</p>
<p><code>did_CI_post</code> compares how far each group moved from its own baseline. A head start cancels, so the result is the difference in change.</p>
<div class="cell" data-layout-align="center">
<details open="" class="code-fold">
<summary>Test two contrasts</summary>
<pre>contrasts &lt;- c(
  marginal_CI_post = paste(
    &quot;`pCRpCR`&quot;,                                  # pCR versus RD in chemotherapy at baseline
    &quot;`ArmC&#038;I:pCRpCR`&quot;,                           # additional pCR versus RD gap in the C&#038;I arm
    &quot;`BiopsyPhasePost-treatment:pCRpCR`&quot;,        # additional pCR versus RD gap at post-treatment
    &quot;`ArmC&#038;I:BiopsyPhasePost-treatment:pCRpCR`&quot;, # additional pCR versus RD gap for C&#038;I at post-treatment
    sep = &quot; + &quot;
  ),
  did_CI_post = paste(
    &quot;`BiopsyPhasePost-treatment:pCRpCR`&quot;,        # change from baseline in the pCR contrast
    &quot;`ArmC&#038;I:BiopsyPhasePost-treatment:pCRpCR`&quot;, # further change from baseline in the C&#038;I arm
    sep = &quot; + &quot;
  )
)

res &lt;- sccomp_test(fit, contrasts = contrasts)</pre>
</details>
</div>
<p><code>res</code> holds those two contrasts, one row per phenotype. The table is the full set of difference-in-differences, strongest first:</p>
<div class="cell" data-layout-align="center">
<details open="" class="code-fold">
<summary>Preview difference-in-differences contrasts</summary>
<pre>hmc_did |&gt;
  filter(contrast_family == &quot;did&quot;) |&gt;
  arrange(c_FDR) |&gt;
  gt::gt_preview() |&gt;
  gt::tab_caption(&quot;Change from baseline (strongest by FDR)&quot;)</pre>
</details>
<div class="cell-output-display">
<div id="pulrjnnkte" style="padding-left:0px;padding-right:0px;padding-top:10px;padding-bottom:10px;overflow-x:auto;overflow-y:auto;width:auto;height:auto;">
<style>#pulrjnnkte table {
  font-family: system-ui, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

#pulrjnnkte thead, #pulrjnnkte tbody, #pulrjnnkte tfoot, #pulrjnnkte tr, #pulrjnnkte td, #pulrjnnkte th {
  border-style: none;
}

#pulrjnnkte p {
  margin: 0;
  padding: 0;
}

#pulrjnnkte .gt_table {
  display: table;
  border-collapse: collapse;
  line-height: normal;
  margin-left: auto;
  margin-right: auto;
  color: #333333;
  font-size: 16px;
  font-weight: normal;
  font-style: normal;
  background-color: #FFFFFF;
  width: auto;
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #A8A8A8;
  border-right-style: none;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #A8A8A8;
  border-left-style: none;
  border-left-width: 2px;
  border-left-color: #D3D3D3;
}

#pulrjnnkte .gt_caption {
  padding-top: 4px;
  padding-bottom: 4px;
}

#pulrjnnkte .gt_title {
  color: #333333;
  font-size: 125%;
  font-weight: initial;
  padding-top: 4px;
  padding-bottom: 4px;
  padding-left: 5px;
  padding-right: 5px;
  border-bottom-color: #FFFFFF;
  border-bottom-width: 0;
}

#pulrjnnkte .gt_subtitle {
  color: #333333;
  font-size: 85%;
  font-weight: initial;
  padding-top: 3px;
  padding-bottom: 5px;
  padding-left: 5px;
  padding-right: 5px;
  border-top-color: #FFFFFF;
  border-top-width: 0;
}

#pulrjnnkte .gt_heading {
  background-color: #FFFFFF;
  text-align: center;
  border-bottom-color: #FFFFFF;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
}

#pulrjnnkte .gt_bottom_border {
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
}

#pulrjnnkte .gt_col_headings {
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
}

#pulrjnnkte .gt_col_heading {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: normal;
  text-transform: inherit;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
  vertical-align: bottom;
  padding-top: 5px;
  padding-bottom: 6px;
  padding-left: 5px;
  padding-right: 5px;
  overflow-x: hidden;
}

#pulrjnnkte .gt_column_spanner_outer {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: normal;
  text-transform: inherit;
  padding-top: 0;
  padding-bottom: 0;
  padding-left: 4px;
  padding-right: 4px;
}

#pulrjnnkte .gt_column_spanner_outer:first-child {
  padding-left: 0;
}

#pulrjnnkte .gt_column_spanner_outer:last-child {
  padding-right: 0;
}

#pulrjnnkte .gt_column_spanner {
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  vertical-align: bottom;
  padding-top: 5px;
  padding-bottom: 5px;
  overflow-x: hidden;
  display: inline-block;
  width: 100%;
}

#pulrjnnkte .gt_spanner_row {
  border-bottom-style: hidden;
}

#pulrjnnkte .gt_group_heading {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  text-transform: inherit;
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
  vertical-align: middle;
  text-align: left;
}

#pulrjnnkte .gt_empty_group_heading {
  padding: 0.5px;
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  vertical-align: middle;
}

#pulrjnnkte .gt_from_md > :first-child {
  margin-top: 0;
}

#pulrjnnkte .gt_from_md > :last-child {
  margin-bottom: 0;
}

#pulrjnnkte .gt_row {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  margin: 10px;
  border-top-style: solid;
  border-top-width: 1px;
  border-top-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
  vertical-align: middle;
  overflow-x: hidden;
}

#pulrjnnkte .gt_stub {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  text-transform: inherit;
  border-right-style: solid;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
  padding-left: 5px;
  padding-right: 5px;
}

#pulrjnnkte .gt_stub_row_group {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  text-transform: inherit;
  border-right-style: solid;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
  padding-left: 5px;
  padding-right: 5px;
  vertical-align: top;
}

#pulrjnnkte .gt_row_group_first td {
  border-top-width: 2px;
}

#pulrjnnkte .gt_row_group_first th {
  border-top-width: 2px;
}

#pulrjnnkte .gt_summary_row {
  color: #333333;
  background-color: #FFFFFF;
  text-transform: inherit;
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
}

#pulrjnnkte .gt_first_summary_row {
  border-top-style: solid;
  border-top-color: #D3D3D3;
}

#pulrjnnkte .gt_first_summary_row.thick {
  border-top-width: 2px;
}

#pulrjnnkte .gt_last_summary_row {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
}

#pulrjnnkte .gt_grand_summary_row {
  color: #333333;
  background-color: #FFFFFF;
  text-transform: inherit;
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
}

#pulrjnnkte .gt_first_grand_summary_row {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  border-top-style: double;
  border-top-width: 6px;
  border-top-color: #D3D3D3;
}

#pulrjnnkte .gt_last_grand_summary_row_top {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  border-bottom-style: double;
  border-bottom-width: 6px;
  border-bottom-color: #D3D3D3;
}

#pulrjnnkte .gt_striped {
  background-color: rgba(128, 128, 128, 0.05);
}

#pulrjnnkte .gt_table_body {
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
}

#pulrjnnkte .gt_footnotes {
  color: #333333;
  background-color: #FFFFFF;
  border-bottom-style: none;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 2px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
}

#pulrjnnkte .gt_footnote {
  margin: 0px;
  font-size: 90%;
  padding-top: 4px;
  padding-bottom: 4px;
  padding-left: 5px;
  padding-right: 5px;
}

#pulrjnnkte .gt_sourcenotes {
  color: #333333;
  background-color: #FFFFFF;
  border-bottom-style: none;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 2px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
}

#pulrjnnkte .gt_sourcenote {
  font-size: 90%;
  padding-top: 4px;
  padding-bottom: 4px;
  padding-left: 5px;
  padding-right: 5px;
}

#pulrjnnkte .gt_left {
  text-align: left;
}

#pulrjnnkte .gt_center {
  text-align: center;
}

#pulrjnnkte .gt_right {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

#pulrjnnkte .gt_font_normal {
  font-weight: normal;
}

#pulrjnnkte .gt_font_bold {
  font-weight: bold;
}

#pulrjnnkte .gt_font_italic {
  font-style: italic;
}

#pulrjnnkte .gt_super {
  font-size: 65%;
}

#pulrjnnkte .gt_footnote_marks {
  font-size: 75%;
  vertical-align: 0.4em;
  position: initial;
}

#pulrjnnkte .gt_asterisk {
  font-size: 100%;
  vertical-align: 0;
}

#pulrjnnkte .gt_indent_1 {
  text-indent: 5px;
}

#pulrjnnkte .gt_indent_2 {
  text-indent: 10px;
}

#pulrjnnkte .gt_indent_3 {
  text-indent: 15px;
}

#pulrjnnkte .gt_indent_4 {
  text-indent: 20px;
}

#pulrjnnkte .gt_indent_5 {
  text-indent: 25px;
}

#pulrjnnkte .katex-display {
  display: inline-flex !important;
  margin-bottom: 0.75em !important;
}

#pulrjnnkte div.Reactable > div.rt-table > div.rt-thead > div.rt-tr.rt-tr-group-header > div.rt-th-group:after {
  height: 0px !important;
}
</style>

<table class="gt_table caption-top table table-sm table-striped small" data-quarto-bootstrap="false">
<caption>Change from baseline (strongest by FDR)</caption>
<thead>
<tr class="gt_col_headings header">
<th id="a::stub" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col"></th>
<th id="parameter" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">parameter</th>
<th id="cell_group" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">cell_group</th>
<th id="factor" class="gt_col_heading gt_columns_bottom_border gt_center" data-quarto-table-cell-role="th" scope="col">factor</th>
<th id="c_lower" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">c_lower</th>
<th id="c_effect" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">c_effect</th>
<th id="c_upper" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">c_upper</th>
<th id="c_pH0" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">c_pH0</th>
<th id="c_FDR" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">c_FDR</th>
<th id="c_rhat" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">c_rhat</th>
<th id="c_ess_bulk" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">c_ess_bulk</th>
<th id="c_ess_tail" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">c_ess_tail</th>
<th id="contrast_family" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">contrast_family</th>
<th id="Arm" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">Arm</th>
<th id="BiopsyPhase" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">BiopsyPhase</th>
<th id="pCR" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">pCR</th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr class="odd">
<th id="stub_1_1" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">1</th>
<td class="gt_row gt_left" headers="stub_1_1 parameter">did__C&I__Post_treatment</td>
<td class="gt_row gt_left" headers="stub_1_1 cell_group">CD56^+NK</td>
<td class="gt_row gt_center" headers="stub_1_1 factor">NA</td>
<td class="gt_row gt_right" headers="stub_1_1 c_lower">2.4152900</td>
<td class="gt_row gt_right" headers="stub_1_1 c_effect">3.25669670</td>
<td class="gt_row gt_right" headers="stub_1_1 c_upper">4.1111366</td>
<td class="gt_row gt_right" headers="stub_1_1 c_pH0">0.0000000</td>
<td class="gt_row gt_right" headers="stub_1_1 c_FDR">0.0000000</td>
<td class="gt_row gt_right" headers="stub_1_1 c_rhat">1.0010947</td>
<td class="gt_row gt_right" headers="stub_1_1 c_ess_bulk">4247.316</td>
<td class="gt_row gt_right" headers="stub_1_1 c_ess_tail">3275.396</td>
<td class="gt_row gt_left" headers="stub_1_1 contrast_family">did</td>
<td class="gt_row gt_left" headers="stub_1_1 Arm">C&I</td>
<td class="gt_row gt_left" headers="stub_1_1 BiopsyPhase">Post-treatment</td>
<td class="gt_row gt_left" headers="stub_1_1 pCR">pCR_minus_RD</td>
</tr>
<tr class="even">
<th id="stub_1_2" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">2</th>
<td class="gt_row gt_left" headers="stub_1_2 parameter">did__C&I__Post_treatment</td>
<td class="gt_row gt_left" headers="stub_1_2 cell_group">DCs</td>
<td class="gt_row gt_center" headers="stub_1_2 factor">NA</td>
<td class="gt_row gt_right" headers="stub_1_2 c_lower">0.6045030</td>
<td class="gt_row gt_right" headers="stub_1_2 c_effect">1.33237959</td>
<td class="gt_row gt_right" headers="stub_1_2 c_upper">2.0188092</td>
<td class="gt_row gt_right" headers="stub_1_2 c_pH0">0.0000000</td>
<td class="gt_row gt_right" headers="stub_1_2 c_FDR">0.0000000</td>
<td class="gt_row gt_right" headers="stub_1_2 c_rhat">0.9996942</td>
<td class="gt_row gt_right" headers="stub_1_2 c_ess_bulk">5122.662</td>
<td class="gt_row gt_right" headers="stub_1_2 c_ess_tail">3061.238</td>
<td class="gt_row gt_left" headers="stub_1_2 contrast_family">did</td>
<td class="gt_row gt_left" headers="stub_1_2 Arm">C&I</td>
<td class="gt_row gt_left" headers="stub_1_2 BiopsyPhase">Post-treatment</td>
<td class="gt_row gt_left" headers="stub_1_2 pCR">pCR_minus_RD</td>
</tr>
<tr class="odd">
<th id="stub_1_3" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">3</th>
<td class="gt_row gt_left" headers="stub_1_3 parameter">did__C&I__Post_treatment</td>
<td class="gt_row gt_left" headers="stub_1_3 cell_group">Neutrophils</td>
<td class="gt_row gt_center" headers="stub_1_3 factor">NA</td>
<td class="gt_row gt_right" headers="stub_1_3 c_lower">1.6664088</td>
<td class="gt_row gt_right" headers="stub_1_3 c_effect">2.63412173</td>
<td class="gt_row gt_right" headers="stub_1_3 c_upper">3.5857767</td>
<td class="gt_row gt_right" headers="stub_1_3 c_pH0">0.0000000</td>
<td class="gt_row gt_right" headers="stub_1_3 c_FDR">0.0000000</td>
<td class="gt_row gt_right" headers="stub_1_3 c_rhat">1.0017040</td>
<td class="gt_row gt_right" headers="stub_1_3 c_ess_bulk">4446.958</td>
<td class="gt_row gt_right" headers="stub_1_3 c_ess_tail">3336.609</td>
<td class="gt_row gt_left" headers="stub_1_3 contrast_family">did</td>
<td class="gt_row gt_left" headers="stub_1_3 Arm">C&I</td>
<td class="gt_row gt_left" headers="stub_1_3 BiopsyPhase">Post-treatment</td>
<td class="gt_row gt_left" headers="stub_1_3 pCR">pCR_minus_RD</td>
</tr>
<tr class="even">
<th id="stub_1_4" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">4</th>
<td class="gt_row gt_left" headers="stub_1_4 parameter">did__C&I__Post_treatment</td>
<td class="gt_row gt_left" headers="stub_1_4 cell_group">PD-L1^+APCs</td>
<td class="gt_row gt_center" headers="stub_1_4 factor">NA</td>
<td class="gt_row gt_right" headers="stub_1_4 c_lower">-3.9597930</td>
<td class="gt_row gt_right" headers="stub_1_4 c_effect">-2.80065090</td>
<td class="gt_row gt_right" headers="stub_1_4 c_upper">-1.6193615</td>
<td class="gt_row gt_right" headers="stub_1_4 c_pH0">0.0000000</td>
<td class="gt_row gt_right" headers="stub_1_4 c_FDR">0.0000000</td>
<td class="gt_row gt_right" headers="stub_1_4 c_rhat">1.0002327</td>
<td class="gt_row gt_right" headers="stub_1_4 c_ess_bulk">3677.553</td>
<td class="gt_row gt_right" headers="stub_1_4 c_ess_tail">2867.557</td>
<td class="gt_row gt_left" headers="stub_1_4 contrast_family">did</td>
<td class="gt_row gt_left" headers="stub_1_4 Arm">C&I</td>
<td class="gt_row gt_left" headers="stub_1_4 BiopsyPhase">Post-treatment</td>
<td class="gt_row gt_left" headers="stub_1_4 pCR">pCR_minus_RD</td>
</tr>
<tr class="odd">
<th id="stub_1_5" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">5</th>
<td class="gt_row gt_left" headers="stub_1_5 parameter">did__C&I__Post_treatment</td>
<td class="gt_row gt_left" headers="stub_1_5 cell_group">Treg</td>
<td class="gt_row gt_center" headers="stub_1_5 factor">NA</td>
<td class="gt_row gt_right" headers="stub_1_5 c_lower">-2.8820452</td>
<td class="gt_row gt_right" headers="stub_1_5 c_effect">-1.94723055</td>
<td class="gt_row gt_right" headers="stub_1_5 c_upper">-1.0560065</td>
<td class="gt_row gt_right" headers="stub_1_5 c_pH0">0.0000000</td>
<td class="gt_row gt_right" headers="stub_1_5 c_FDR">0.0000000</td>
<td class="gt_row gt_right" headers="stub_1_5 c_rhat">1.0006019</td>
<td class="gt_row gt_right" headers="stub_1_5 c_ess_bulk">2449.355</td>
<td class="gt_row gt_right" headers="stub_1_5 c_ess_tail">2366.402</td>
<td class="gt_row gt_left" headers="stub_1_5 contrast_family">did</td>
<td class="gt_row gt_left" headers="stub_1_5 Arm">C&I</td>
<td class="gt_row gt_left" headers="stub_1_5 BiopsyPhase">Post-treatment</td>
<td class="gt_row gt_left" headers="stub_1_5 pCR">pCR_minus_RD</td>
</tr>
<tr class="even">
<th id="stub_1_6" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier; font-size: x-small; background-color: #E4E4E4">6..59</th>
<td class="gt_row gt_left" headers="stub_1_6 parameter" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_left" headers="stub_1_6 cell_group" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_center" headers="stub_1_6 factor" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_right" headers="stub_1_6 c_lower" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_right" headers="stub_1_6 c_effect" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_right" headers="stub_1_6 c_upper" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_right" headers="stub_1_6 c_pH0" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_right" headers="stub_1_6 c_FDR" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_right" headers="stub_1_6 c_rhat" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_right" headers="stub_1_6 c_ess_bulk" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_right" headers="stub_1_6 c_ess_tail" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_left" headers="stub_1_6 contrast_family" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_left" headers="stub_1_6 Arm" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_left" headers="stub_1_6 BiopsyPhase" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_left" headers="stub_1_6 pCR" style="background-color: #E4E4E4"></td>
</tr>
<tr class="odd">
<th id="stub_1_7" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">60</th>
<td class="gt_row gt_left" headers="stub_1_7 parameter">did__C&I__On_treatment</td>
<td class="gt_row gt_left" headers="stub_1_7 cell_group">CD8^+TCF1^+T</td>
<td class="gt_row gt_center" headers="stub_1_7 factor">NA</td>
<td class="gt_row gt_right" headers="stub_1_7 c_lower">-0.3600534</td>
<td class="gt_row gt_right" headers="stub_1_7 c_effect">0.01963656</td>
<td class="gt_row gt_right" headers="stub_1_7 c_upper">0.4067838</td>
<td class="gt_row gt_right" headers="stub_1_7 c_pH0">0.6599099</td>
<td class="gt_row gt_right" headers="stub_1_7 c_FDR">0.2803971</td>
<td class="gt_row gt_right" headers="stub_1_7 c_rhat">1.0012927</td>
<td class="gt_row gt_right" headers="stub_1_7 c_ess_bulk">5303.619</td>
<td class="gt_row gt_right" headers="stub_1_7 c_ess_tail">3036.666</td>
<td class="gt_row gt_left" headers="stub_1_7 contrast_family">did</td>
<td class="gt_row gt_left" headers="stub_1_7 Arm">C&I</td>
<td class="gt_row gt_left" headers="stub_1_7 BiopsyPhase">On-treatment</td>
<td class="gt_row gt_left" headers="stub_1_7 pCR">pCR_minus_RD</td>
</tr>
</tbody>
</table>

</div>
</div>
</div>
</section>
<section id="population-and-per-patient-predictions" class="level2">
<h2 class="anchored" data-anchor-id="population-and-per-patient-predictions">Population and per-patient predictions</h2>
<div class="cell" data-layout-align="center">
<details class="code-fold">
<summary>Population prediction grid</summary>
<pre># One row per arm, timepoint, and response group.
pop &lt;- tibble::tribble(
  ~Arm,  ~BiopsyPhase,     ~pCR,
  &quot;C&quot;,   &quot;Baseline&quot;,       &quot;RD&quot;,
  &quot;C&quot;,   &quot;Baseline&quot;,       &quot;pCR&quot;,
  &quot;C&quot;,   &quot;On-treatment&quot;,   &quot;RD&quot;,
  &quot;C&quot;,   &quot;On-treatment&quot;,   &quot;pCR&quot;,
  &quot;C&quot;,   &quot;Post-treatment&quot;, &quot;RD&quot;,
  &quot;C&quot;,   &quot;Post-treatment&quot;, &quot;pCR&quot;,
  &quot;C&#038;I&quot;, &quot;Baseline&quot;,       &quot;RD&quot;,
  &quot;C&#038;I&quot;, &quot;Baseline&quot;,       &quot;pCR&quot;,
  &quot;C&#038;I&quot;, &quot;On-treatment&quot;,   &quot;RD&quot;,
  &quot;C&#038;I&quot;, &quot;On-treatment&quot;,   &quot;pCR&quot;,
  &quot;C&#038;I&quot;, &quot;Post-treatment&quot;, &quot;RD&quot;,
  &quot;C&#038;I&quot;, &quot;Post-treatment&quot;, &quot;pCR&quot;
) |&gt;
  mutate(
    Arm = factor(Arm, c(&quot;C&quot;, &quot;C&I&quot;)),
    BiopsyPhase = factor(BiopsyPhase, c(&quot;Baseline&quot;, &quot;On-treatment&quot;, &quot;Post-treatment&quot;)),
    pCR = factor(pCR, c(&quot;RD&quot;, &quot;pCR&quot;)),
    ROI_id = paste0(&quot;pred_&quot;, row_number()),
    PatientID = &quot;NT031&quot;
  )

gt::gt_preview(pop) |&gt;
  gt::tab_caption(&quot;Population grid passed to sccomp_predict()&quot;)</pre>
</details>
<div class="cell-output-display">
<div id="bcmcslsuap" style="padding-left:0px;padding-right:0px;padding-top:10px;padding-bottom:10px;overflow-x:auto;overflow-y:auto;width:auto;height:auto;">
<style>#bcmcslsuap table {
  font-family: system-ui, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

#bcmcslsuap thead, #bcmcslsuap tbody, #bcmcslsuap tfoot, #bcmcslsuap tr, #bcmcslsuap td, #bcmcslsuap th {
  border-style: none;
}

#bcmcslsuap p {
  margin: 0;
  padding: 0;
}

#bcmcslsuap .gt_table {
  display: table;
  border-collapse: collapse;
  line-height: normal;
  margin-left: auto;
  margin-right: auto;
  color: #333333;
  font-size: 16px;
  font-weight: normal;
  font-style: normal;
  background-color: #FFFFFF;
  width: auto;
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #A8A8A8;
  border-right-style: none;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #A8A8A8;
  border-left-style: none;
  border-left-width: 2px;
  border-left-color: #D3D3D3;
}

#bcmcslsuap .gt_caption {
  padding-top: 4px;
  padding-bottom: 4px;
}

#bcmcslsuap .gt_title {
  color: #333333;
  font-size: 125%;
  font-weight: initial;
  padding-top: 4px;
  padding-bottom: 4px;
  padding-left: 5px;
  padding-right: 5px;
  border-bottom-color: #FFFFFF;
  border-bottom-width: 0;
}

#bcmcslsuap .gt_subtitle {
  color: #333333;
  font-size: 85%;
  font-weight: initial;
  padding-top: 3px;
  padding-bottom: 5px;
  padding-left: 5px;
  padding-right: 5px;
  border-top-color: #FFFFFF;
  border-top-width: 0;
}

#bcmcslsuap .gt_heading {
  background-color: #FFFFFF;
  text-align: center;
  border-bottom-color: #FFFFFF;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
}

#bcmcslsuap .gt_bottom_border {
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
}

#bcmcslsuap .gt_col_headings {
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
}

#bcmcslsuap .gt_col_heading {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: normal;
  text-transform: inherit;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
  vertical-align: bottom;
  padding-top: 5px;
  padding-bottom: 6px;
  padding-left: 5px;
  padding-right: 5px;
  overflow-x: hidden;
}

#bcmcslsuap .gt_column_spanner_outer {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: normal;
  text-transform: inherit;
  padding-top: 0;
  padding-bottom: 0;
  padding-left: 4px;
  padding-right: 4px;
}

#bcmcslsuap .gt_column_spanner_outer:first-child {
  padding-left: 0;
}

#bcmcslsuap .gt_column_spanner_outer:last-child {
  padding-right: 0;
}

#bcmcslsuap .gt_column_spanner {
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  vertical-align: bottom;
  padding-top: 5px;
  padding-bottom: 5px;
  overflow-x: hidden;
  display: inline-block;
  width: 100%;
}

#bcmcslsuap .gt_spanner_row {
  border-bottom-style: hidden;
}

#bcmcslsuap .gt_group_heading {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  text-transform: inherit;
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
  vertical-align: middle;
  text-align: left;
}

#bcmcslsuap .gt_empty_group_heading {
  padding: 0.5px;
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  vertical-align: middle;
}

#bcmcslsuap .gt_from_md > :first-child {
  margin-top: 0;
}

#bcmcslsuap .gt_from_md > :last-child {
  margin-bottom: 0;
}

#bcmcslsuap .gt_row {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  margin: 10px;
  border-top-style: solid;
  border-top-width: 1px;
  border-top-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
  vertical-align: middle;
  overflow-x: hidden;
}

#bcmcslsuap .gt_stub {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  text-transform: inherit;
  border-right-style: solid;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
  padding-left: 5px;
  padding-right: 5px;
}

#bcmcslsuap .gt_stub_row_group {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  text-transform: inherit;
  border-right-style: solid;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
  padding-left: 5px;
  padding-right: 5px;
  vertical-align: top;
}

#bcmcslsuap .gt_row_group_first td {
  border-top-width: 2px;
}

#bcmcslsuap .gt_row_group_first th {
  border-top-width: 2px;
}

#bcmcslsuap .gt_summary_row {
  color: #333333;
  background-color: #FFFFFF;
  text-transform: inherit;
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
}

#bcmcslsuap .gt_first_summary_row {
  border-top-style: solid;
  border-top-color: #D3D3D3;
}

#bcmcslsuap .gt_first_summary_row.thick {
  border-top-width: 2px;
}

#bcmcslsuap .gt_last_summary_row {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
}

#bcmcslsuap .gt_grand_summary_row {
  color: #333333;
  background-color: #FFFFFF;
  text-transform: inherit;
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
}

#bcmcslsuap .gt_first_grand_summary_row {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  border-top-style: double;
  border-top-width: 6px;
  border-top-color: #D3D3D3;
}

#bcmcslsuap .gt_last_grand_summary_row_top {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  border-bottom-style: double;
  border-bottom-width: 6px;
  border-bottom-color: #D3D3D3;
}

#bcmcslsuap .gt_striped {
  background-color: rgba(128, 128, 128, 0.05);
}

#bcmcslsuap .gt_table_body {
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
}

#bcmcslsuap .gt_footnotes {
  color: #333333;
  background-color: #FFFFFF;
  border-bottom-style: none;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 2px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
}

#bcmcslsuap .gt_footnote {
  margin: 0px;
  font-size: 90%;
  padding-top: 4px;
  padding-bottom: 4px;
  padding-left: 5px;
  padding-right: 5px;
}

#bcmcslsuap .gt_sourcenotes {
  color: #333333;
  background-color: #FFFFFF;
  border-bottom-style: none;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 2px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
}

#bcmcslsuap .gt_sourcenote {
  font-size: 90%;
  padding-top: 4px;
  padding-bottom: 4px;
  padding-left: 5px;
  padding-right: 5px;
}

#bcmcslsuap .gt_left {
  text-align: left;
}

#bcmcslsuap .gt_center {
  text-align: center;
}

#bcmcslsuap .gt_right {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

#bcmcslsuap .gt_font_normal {
  font-weight: normal;
}

#bcmcslsuap .gt_font_bold {
  font-weight: bold;
}

#bcmcslsuap .gt_font_italic {
  font-style: italic;
}

#bcmcslsuap .gt_super {
  font-size: 65%;
}

#bcmcslsuap .gt_footnote_marks {
  font-size: 75%;
  vertical-align: 0.4em;
  position: initial;
}

#bcmcslsuap .gt_asterisk {
  font-size: 100%;
  vertical-align: 0;
}

#bcmcslsuap .gt_indent_1 {
  text-indent: 5px;
}

#bcmcslsuap .gt_indent_2 {
  text-indent: 10px;
}

#bcmcslsuap .gt_indent_3 {
  text-indent: 15px;
}

#bcmcslsuap .gt_indent_4 {
  text-indent: 20px;
}

#bcmcslsuap .gt_indent_5 {
  text-indent: 25px;
}

#bcmcslsuap .katex-display {
  display: inline-flex !important;
  margin-bottom: 0.75em !important;
}

#bcmcslsuap div.Reactable > div.rt-table > div.rt-thead > div.rt-tr.rt-tr-group-header > div.rt-th-group:after {
  height: 0px !important;
}
</style>

<table class="gt_table caption-top table table-sm table-striped small" data-quarto-bootstrap="false">
<caption>Population grid passed to sccomp_predict()</caption>
<thead>
<tr class="gt_col_headings header">
<th id="a::stub" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col"></th>
<th id="Arm" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">Arm</th>
<th id="BiopsyPhase" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">BiopsyPhase</th>
<th id="pCR" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">pCR</th>
<th id="ROI_id" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">ROI_id</th>
<th id="PatientID" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">PatientID</th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr class="odd">
<th id="stub_1_1" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">1</th>
<td class="gt_row gt_left" headers="stub_1_1 Arm">C</td>
<td class="gt_row gt_left" headers="stub_1_1 BiopsyPhase">Baseline</td>
<td class="gt_row gt_left" headers="stub_1_1 pCR">RD</td>
<td class="gt_row gt_left" headers="stub_1_1 ROI_id">pred_1</td>
<td class="gt_row gt_left" headers="stub_1_1 PatientID">NT031</td>
</tr>
<tr class="even">
<th id="stub_1_2" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">2</th>
<td class="gt_row gt_left" headers="stub_1_2 Arm">C</td>
<td class="gt_row gt_left" headers="stub_1_2 BiopsyPhase">Baseline</td>
<td class="gt_row gt_left" headers="stub_1_2 pCR">pCR</td>
<td class="gt_row gt_left" headers="stub_1_2 ROI_id">pred_2</td>
<td class="gt_row gt_left" headers="stub_1_2 PatientID">NT031</td>
</tr>
<tr class="odd">
<th id="stub_1_3" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">3</th>
<td class="gt_row gt_left" headers="stub_1_3 Arm">C</td>
<td class="gt_row gt_left" headers="stub_1_3 BiopsyPhase">On-treatment</td>
<td class="gt_row gt_left" headers="stub_1_3 pCR">RD</td>
<td class="gt_row gt_left" headers="stub_1_3 ROI_id">pred_3</td>
<td class="gt_row gt_left" headers="stub_1_3 PatientID">NT031</td>
</tr>
<tr class="even">
<th id="stub_1_4" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">4</th>
<td class="gt_row gt_left" headers="stub_1_4 Arm">C</td>
<td class="gt_row gt_left" headers="stub_1_4 BiopsyPhase">On-treatment</td>
<td class="gt_row gt_left" headers="stub_1_4 pCR">pCR</td>
<td class="gt_row gt_left" headers="stub_1_4 ROI_id">pred_4</td>
<td class="gt_row gt_left" headers="stub_1_4 PatientID">NT031</td>
</tr>
<tr class="odd">
<th id="stub_1_5" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">5</th>
<td class="gt_row gt_left" headers="stub_1_5 Arm">C</td>
<td class="gt_row gt_left" headers="stub_1_5 BiopsyPhase">Post-treatment</td>
<td class="gt_row gt_left" headers="stub_1_5 pCR">RD</td>
<td class="gt_row gt_left" headers="stub_1_5 ROI_id">pred_5</td>
<td class="gt_row gt_left" headers="stub_1_5 PatientID">NT031</td>
</tr>
<tr class="even">
<th id="stub_1_6" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier; font-size: x-small; background-color: #E4E4E4">6..11</th>
<td class="gt_row gt_left" headers="stub_1_6 Arm" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_left" headers="stub_1_6 BiopsyPhase" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_left" headers="stub_1_6 pCR" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_left" headers="stub_1_6 ROI_id" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_left" headers="stub_1_6 PatientID" style="background-color: #E4E4E4"></td>
</tr>
<tr class="odd">
<th id="stub_1_7" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">12</th>
<td class="gt_row gt_left" headers="stub_1_7 Arm">C&I</td>
<td class="gt_row gt_left" headers="stub_1_7 BiopsyPhase">Post-treatment</td>
<td class="gt_row gt_left" headers="stub_1_7 pCR">pCR</td>
<td class="gt_row gt_left" headers="stub_1_7 ROI_id">pred_12</td>
<td class="gt_row gt_left" headers="stub_1_7 PatientID">NT031</td>
</tr>
</tbody>
</table>

</div>
</div>
</div>
<div class="cell" data-layout-align="center">
<details open="" class="code-fold">
<summary>Predict population proportions</summary>
<pre>pred_pop &lt;- sccomp_predict(fit, new_data = pop)</pre>
</details>
<div class="cell-output cell-output-stdout">
<pre>Running standalone generated quantities after 1 MCMC chain, with 1 thread(s) per chain...

Chain 1  Elapsed Time: 0.185 seconds (Generated Quantities) 
Chain 1 finished in 0.0 seconds.</pre>
</div>
<details open="" class="code-fold">
<summary>Predict population proportions</summary>
<pre>pred_pop |&gt;
  gt::gt_preview() |&gt;
  gt::tab_caption(&quot;Population predicted proportions&quot;)</pre>
</details>
<div class="cell-output-display">
<div id="zmpayzboox" style="padding-left:0px;padding-right:0px;padding-top:10px;padding-bottom:10px;overflow-x:auto;overflow-y:auto;width:auto;height:auto;">
<style>#zmpayzboox table {
  font-family: system-ui, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

#zmpayzboox thead, #zmpayzboox tbody, #zmpayzboox tfoot, #zmpayzboox tr, #zmpayzboox td, #zmpayzboox th {
  border-style: none;
}

#zmpayzboox p {
  margin: 0;
  padding: 0;
}

#zmpayzboox .gt_table {
  display: table;
  border-collapse: collapse;
  line-height: normal;
  margin-left: auto;
  margin-right: auto;
  color: #333333;
  font-size: 16px;
  font-weight: normal;
  font-style: normal;
  background-color: #FFFFFF;
  width: auto;
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #A8A8A8;
  border-right-style: none;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #A8A8A8;
  border-left-style: none;
  border-left-width: 2px;
  border-left-color: #D3D3D3;
}

#zmpayzboox .gt_caption {
  padding-top: 4px;
  padding-bottom: 4px;
}

#zmpayzboox .gt_title {
  color: #333333;
  font-size: 125%;
  font-weight: initial;
  padding-top: 4px;
  padding-bottom: 4px;
  padding-left: 5px;
  padding-right: 5px;
  border-bottom-color: #FFFFFF;
  border-bottom-width: 0;
}

#zmpayzboox .gt_subtitle {
  color: #333333;
  font-size: 85%;
  font-weight: initial;
  padding-top: 3px;
  padding-bottom: 5px;
  padding-left: 5px;
  padding-right: 5px;
  border-top-color: #FFFFFF;
  border-top-width: 0;
}

#zmpayzboox .gt_heading {
  background-color: #FFFFFF;
  text-align: center;
  border-bottom-color: #FFFFFF;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
}

#zmpayzboox .gt_bottom_border {
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
}

#zmpayzboox .gt_col_headings {
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
}

#zmpayzboox .gt_col_heading {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: normal;
  text-transform: inherit;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
  vertical-align: bottom;
  padding-top: 5px;
  padding-bottom: 6px;
  padding-left: 5px;
  padding-right: 5px;
  overflow-x: hidden;
}

#zmpayzboox .gt_column_spanner_outer {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: normal;
  text-transform: inherit;
  padding-top: 0;
  padding-bottom: 0;
  padding-left: 4px;
  padding-right: 4px;
}

#zmpayzboox .gt_column_spanner_outer:first-child {
  padding-left: 0;
}

#zmpayzboox .gt_column_spanner_outer:last-child {
  padding-right: 0;
}

#zmpayzboox .gt_column_spanner {
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  vertical-align: bottom;
  padding-top: 5px;
  padding-bottom: 5px;
  overflow-x: hidden;
  display: inline-block;
  width: 100%;
}

#zmpayzboox .gt_spanner_row {
  border-bottom-style: hidden;
}

#zmpayzboox .gt_group_heading {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  text-transform: inherit;
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
  vertical-align: middle;
  text-align: left;
}

#zmpayzboox .gt_empty_group_heading {
  padding: 0.5px;
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  vertical-align: middle;
}

#zmpayzboox .gt_from_md > :first-child {
  margin-top: 0;
}

#zmpayzboox .gt_from_md > :last-child {
  margin-bottom: 0;
}

#zmpayzboox .gt_row {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  margin: 10px;
  border-top-style: solid;
  border-top-width: 1px;
  border-top-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
  vertical-align: middle;
  overflow-x: hidden;
}

#zmpayzboox .gt_stub {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  text-transform: inherit;
  border-right-style: solid;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
  padding-left: 5px;
  padding-right: 5px;
}

#zmpayzboox .gt_stub_row_group {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  text-transform: inherit;
  border-right-style: solid;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
  padding-left: 5px;
  padding-right: 5px;
  vertical-align: top;
}

#zmpayzboox .gt_row_group_first td {
  border-top-width: 2px;
}

#zmpayzboox .gt_row_group_first th {
  border-top-width: 2px;
}

#zmpayzboox .gt_summary_row {
  color: #333333;
  background-color: #FFFFFF;
  text-transform: inherit;
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
}

#zmpayzboox .gt_first_summary_row {
  border-top-style: solid;
  border-top-color: #D3D3D3;
}

#zmpayzboox .gt_first_summary_row.thick {
  border-top-width: 2px;
}

#zmpayzboox .gt_last_summary_row {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
}

#zmpayzboox .gt_grand_summary_row {
  color: #333333;
  background-color: #FFFFFF;
  text-transform: inherit;
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
}

#zmpayzboox .gt_first_grand_summary_row {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  border-top-style: double;
  border-top-width: 6px;
  border-top-color: #D3D3D3;
}

#zmpayzboox .gt_last_grand_summary_row_top {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  border-bottom-style: double;
  border-bottom-width: 6px;
  border-bottom-color: #D3D3D3;
}

#zmpayzboox .gt_striped {
  background-color: rgba(128, 128, 128, 0.05);
}

#zmpayzboox .gt_table_body {
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
}

#zmpayzboox .gt_footnotes {
  color: #333333;
  background-color: #FFFFFF;
  border-bottom-style: none;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 2px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
}

#zmpayzboox .gt_footnote {
  margin: 0px;
  font-size: 90%;
  padding-top: 4px;
  padding-bottom: 4px;
  padding-left: 5px;
  padding-right: 5px;
}

#zmpayzboox .gt_sourcenotes {
  color: #333333;
  background-color: #FFFFFF;
  border-bottom-style: none;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 2px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
}

#zmpayzboox .gt_sourcenote {
  font-size: 90%;
  padding-top: 4px;
  padding-bottom: 4px;
  padding-left: 5px;
  padding-right: 5px;
}

#zmpayzboox .gt_left {
  text-align: left;
}

#zmpayzboox .gt_center {
  text-align: center;
}

#zmpayzboox .gt_right {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

#zmpayzboox .gt_font_normal {
  font-weight: normal;
}

#zmpayzboox .gt_font_bold {
  font-weight: bold;
}

#zmpayzboox .gt_font_italic {
  font-style: italic;
}

#zmpayzboox .gt_super {
  font-size: 65%;
}

#zmpayzboox .gt_footnote_marks {
  font-size: 75%;
  vertical-align: 0.4em;
  position: initial;
}

#zmpayzboox .gt_asterisk {
  font-size: 100%;
  vertical-align: 0;
}

#zmpayzboox .gt_indent_1 {
  text-indent: 5px;
}

#zmpayzboox .gt_indent_2 {
  text-indent: 10px;
}

#zmpayzboox .gt_indent_3 {
  text-indent: 15px;
}

#zmpayzboox .gt_indent_4 {
  text-indent: 20px;
}

#zmpayzboox .gt_indent_5 {
  text-indent: 25px;
}

#zmpayzboox .katex-display {
  display: inline-flex !important;
  margin-bottom: 0.75em !important;
}

#zmpayzboox div.Reactable > div.rt-table > div.rt-thead > div.rt-tr.rt-tr-group-header > div.rt-th-group:after {
  height: 0px !important;
}
</style>

<table class="gt_table caption-top table table-sm table-striped small" data-quarto-bootstrap="false">
<caption>Population predicted proportions</caption>
<thead>
<tr class="gt_col_headings header">
<th id="a::stub" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col"></th>
<th id="Arm" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">Arm</th>
<th id="BiopsyPhase" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">BiopsyPhase</th>
<th id="pCR" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">pCR</th>
<th id="ROI_id" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">ROI_id</th>
<th id="PatientID" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">PatientID</th>
<th id="cell_group" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">cell_group</th>
<th id="proportion_mean" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">proportion_mean</th>
<th id="proportion_lower" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">proportion_lower</th>
<th id="proportion_upper" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">proportion_upper</th>
<th id="unconstrained_mean" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">unconstrained_mean</th>
<th id="unconstrained_lower" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">unconstrained_lower</th>
<th id="unconstrained_upper" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">unconstrained_upper</th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr class="odd">
<th id="stub_1_1" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">1</th>
<td class="gt_row gt_left" headers="stub_1_1 Arm">C</td>
<td class="gt_row gt_left" headers="stub_1_1 BiopsyPhase">Baseline</td>
<td class="gt_row gt_left" headers="stub_1_1 pCR">RD</td>
<td class="gt_row gt_left" headers="stub_1_1 ROI_id">pred_1</td>
<td class="gt_row gt_left" headers="stub_1_1 PatientID">NT031</td>
<td class="gt_row gt_left" headers="stub_1_1 cell_group">CD20^+B</td>
<td class="gt_row gt_right" headers="stub_1_1 proportion_mean">0.025475434</td>
<td class="gt_row gt_right" headers="stub_1_1 proportion_lower">0.011849041</td>
<td class="gt_row gt_right" headers="stub_1_1 proportion_upper">0.04656918</td>
<td class="gt_row gt_right" headers="stub_1_1 unconstrained_mean">-0.78916098</td>
<td class="gt_row gt_right" headers="stub_1_1 unconstrained_lower">-1.44397950</td>
<td class="gt_row gt_right" headers="stub_1_1 unconstrained_upper">-0.18577897</td>
</tr>
<tr class="even">
<th id="stub_1_2" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">2</th>
<td class="gt_row gt_left" headers="stub_1_2 Arm">C</td>
<td class="gt_row gt_left" headers="stub_1_2 BiopsyPhase">Baseline</td>
<td class="gt_row gt_left" headers="stub_1_2 pCR">RD</td>
<td class="gt_row gt_left" headers="stub_1_2 ROI_id">pred_1</td>
<td class="gt_row gt_left" headers="stub_1_2 PatientID">NT031</td>
<td class="gt_row gt_left" headers="stub_1_2 cell_group">CD4^+PD1^+T</td>
<td class="gt_row gt_right" headers="stub_1_2 proportion_mean">0.050589704</td>
<td class="gt_row gt_right" headers="stub_1_2 proportion_lower">0.032262354</td>
<td class="gt_row gt_right" headers="stub_1_2 proportion_upper">0.07256136</td>
<td class="gt_row gt_right" headers="stub_1_2 unconstrained_mean">-0.07387369</td>
<td class="gt_row gt_right" headers="stub_1_2 unconstrained_lower">-0.48652249</td>
<td class="gt_row gt_right" headers="stub_1_2 unconstrained_upper">0.30169921</td>
</tr>
<tr class="odd">
<th id="stub_1_3" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">3</th>
<td class="gt_row gt_left" headers="stub_1_3 Arm">C</td>
<td class="gt_row gt_left" headers="stub_1_3 BiopsyPhase">Baseline</td>
<td class="gt_row gt_left" headers="stub_1_3 pCR">RD</td>
<td class="gt_row gt_left" headers="stub_1_3 ROI_id">pred_1</td>
<td class="gt_row gt_left" headers="stub_1_3 PatientID">NT031</td>
<td class="gt_row gt_left" headers="stub_1_3 cell_group">CD4^+TCF1^+T</td>
<td class="gt_row gt_right" headers="stub_1_3 proportion_mean">0.081006649</td>
<td class="gt_row gt_right" headers="stub_1_3 proportion_lower">0.051677388</td>
<td class="gt_row gt_right" headers="stub_1_3 proportion_upper">0.11175873</td>
<td class="gt_row gt_right" headers="stub_1_3 unconstrained_mean">0.39988733</td>
<td class="gt_row gt_right" headers="stub_1_3 unconstrained_lower">-0.02541777</td>
<td class="gt_row gt_right" headers="stub_1_3 unconstrained_upper">0.76499292</td>
</tr>
<tr class="even">
<th id="stub_1_4" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">4</th>
<td class="gt_row gt_left" headers="stub_1_4 Arm">C</td>
<td class="gt_row gt_left" headers="stub_1_4 BiopsyPhase">Baseline</td>
<td class="gt_row gt_left" headers="stub_1_4 pCR">RD</td>
<td class="gt_row gt_left" headers="stub_1_4 ROI_id">pred_1</td>
<td class="gt_row gt_left" headers="stub_1_4 PatientID">NT031</td>
<td class="gt_row gt_left" headers="stub_1_4 cell_group">CD56^+NK</td>
<td class="gt_row gt_right" headers="stub_1_4 proportion_mean">0.032822597</td>
<td class="gt_row gt_right" headers="stub_1_4 proportion_lower">0.017883680</td>
<td class="gt_row gt_right" headers="stub_1_4 proportion_upper">0.05537661</td>
<td class="gt_row gt_right" headers="stub_1_4 unconstrained_mean">-0.52534173</td>
<td class="gt_row gt_right" headers="stub_1_4 unconstrained_lower">-1.06513952</td>
<td class="gt_row gt_right" headers="stub_1_4 unconstrained_upper">0.01087662</td>
</tr>
<tr class="odd">
<th id="stub_1_5" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">5</th>
<td class="gt_row gt_left" headers="stub_1_5 Arm">C</td>
<td class="gt_row gt_left" headers="stub_1_5 BiopsyPhase">Baseline</td>
<td class="gt_row gt_left" headers="stub_1_5 pCR">RD</td>
<td class="gt_row gt_left" headers="stub_1_5 ROI_id">pred_1</td>
<td class="gt_row gt_left" headers="stub_1_5 PatientID">NT031</td>
<td class="gt_row gt_left" headers="stub_1_5 cell_group">CD79a^+Plasma</td>
<td class="gt_row gt_right" headers="stub_1_5 proportion_mean">0.158685350</td>
<td class="gt_row gt_right" headers="stub_1_5 proportion_lower">0.102258774</td>
<td class="gt_row gt_right" headers="stub_1_5 proportion_upper">0.22280513</td>
<td class="gt_row gt_right" headers="stub_1_5 unconstrained_mean">1.07227269</td>
<td class="gt_row gt_right" headers="stub_1_5 unconstrained_lower">0.63309480</td>
<td class="gt_row gt_right" headers="stub_1_5 unconstrained_upper">1.48362000</td>
</tr>
<tr class="even">
<th id="stub_1_6" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier; font-size: x-small; background-color: #E4E4E4">6..179</th>
<td class="gt_row gt_left" headers="stub_1_6 Arm" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_left" headers="stub_1_6 BiopsyPhase" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_left" headers="stub_1_6 pCR" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_left" headers="stub_1_6 ROI_id" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_left" headers="stub_1_6 PatientID" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_left" headers="stub_1_6 cell_group" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_right" headers="stub_1_6 proportion_mean" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_right" headers="stub_1_6 proportion_lower" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_right" headers="stub_1_6 proportion_upper" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_right" headers="stub_1_6 unconstrained_mean" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_right" headers="stub_1_6 unconstrained_lower" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_right" headers="stub_1_6 unconstrained_upper" style="background-color: #E4E4E4"></td>
</tr>
<tr class="odd">
<th id="stub_1_7" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">180</th>
<td class="gt_row gt_left" headers="stub_1_7 Arm">C&I</td>
<td class="gt_row gt_left" headers="stub_1_7 BiopsyPhase">Post-treatment</td>
<td class="gt_row gt_left" headers="stub_1_7 pCR">pCR</td>
<td class="gt_row gt_left" headers="stub_1_7 ROI_id">pred_12</td>
<td class="gt_row gt_left" headers="stub_1_7 PatientID">NT031</td>
<td class="gt_row gt_left" headers="stub_1_7 cell_group">Treg</td>
<td class="gt_row gt_right" headers="stub_1_7 proportion_mean">0.008723248</td>
<td class="gt_row gt_right" headers="stub_1_7 proportion_lower">0.005836047</td>
<td class="gt_row gt_right" headers="stub_1_7 proportion_upper">0.01223340</td>
<td class="gt_row gt_right" headers="stub_1_7 unconstrained_mean">-1.53308238</td>
<td class="gt_row gt_right" headers="stub_1_7 unconstrained_lower">-1.88980277</td>
<td class="gt_row gt_right" headers="stub_1_7 unconstrained_upper">-1.19372516</td>
</tr>
</tbody>
</table>

</div>
</div>
</div>
<div class="cell" data-layout-align="center">
<details class="code-fold">
<summary>Per-patient prediction grid</summary>
<pre># Per-patient grid: one row per patient and timepoint (uses the patient random intercept)
pat &lt;- long |&gt;
  distinct(PatientID, BiopsyPhase, Arm, pCR) |&gt;
  mutate(ROI_id = paste0(PatientID, &quot;_&quot;, BiopsyPhase))

pat |&gt;
  gt::gt_preview() |&gt;
  gt::tab_caption(&quot;Per-patient prediction grid (624 rows)&quot;)</pre>
</details>
<div class="cell-output-display">
<div id="heutamzyau" style="padding-left:0px;padding-right:0px;padding-top:10px;padding-bottom:10px;overflow-x:auto;overflow-y:auto;width:auto;height:auto;">
<style>#heutamzyau table {
  font-family: system-ui, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

#heutamzyau thead, #heutamzyau tbody, #heutamzyau tfoot, #heutamzyau tr, #heutamzyau td, #heutamzyau th {
  border-style: none;
}

#heutamzyau p {
  margin: 0;
  padding: 0;
}

#heutamzyau .gt_table {
  display: table;
  border-collapse: collapse;
  line-height: normal;
  margin-left: auto;
  margin-right: auto;
  color: #333333;
  font-size: 16px;
  font-weight: normal;
  font-style: normal;
  background-color: #FFFFFF;
  width: auto;
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #A8A8A8;
  border-right-style: none;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #A8A8A8;
  border-left-style: none;
  border-left-width: 2px;
  border-left-color: #D3D3D3;
}

#heutamzyau .gt_caption {
  padding-top: 4px;
  padding-bottom: 4px;
}

#heutamzyau .gt_title {
  color: #333333;
  font-size: 125%;
  font-weight: initial;
  padding-top: 4px;
  padding-bottom: 4px;
  padding-left: 5px;
  padding-right: 5px;
  border-bottom-color: #FFFFFF;
  border-bottom-width: 0;
}

#heutamzyau .gt_subtitle {
  color: #333333;
  font-size: 85%;
  font-weight: initial;
  padding-top: 3px;
  padding-bottom: 5px;
  padding-left: 5px;
  padding-right: 5px;
  border-top-color: #FFFFFF;
  border-top-width: 0;
}

#heutamzyau .gt_heading {
  background-color: #FFFFFF;
  text-align: center;
  border-bottom-color: #FFFFFF;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
}

#heutamzyau .gt_bottom_border {
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
}

#heutamzyau .gt_col_headings {
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
}

#heutamzyau .gt_col_heading {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: normal;
  text-transform: inherit;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
  vertical-align: bottom;
  padding-top: 5px;
  padding-bottom: 6px;
  padding-left: 5px;
  padding-right: 5px;
  overflow-x: hidden;
}

#heutamzyau .gt_column_spanner_outer {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: normal;
  text-transform: inherit;
  padding-top: 0;
  padding-bottom: 0;
  padding-left: 4px;
  padding-right: 4px;
}

#heutamzyau .gt_column_spanner_outer:first-child {
  padding-left: 0;
}

#heutamzyau .gt_column_spanner_outer:last-child {
  padding-right: 0;
}

#heutamzyau .gt_column_spanner {
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  vertical-align: bottom;
  padding-top: 5px;
  padding-bottom: 5px;
  overflow-x: hidden;
  display: inline-block;
  width: 100%;
}

#heutamzyau .gt_spanner_row {
  border-bottom-style: hidden;
}

#heutamzyau .gt_group_heading {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  text-transform: inherit;
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
  vertical-align: middle;
  text-align: left;
}

#heutamzyau .gt_empty_group_heading {
  padding: 0.5px;
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  vertical-align: middle;
}

#heutamzyau .gt_from_md > :first-child {
  margin-top: 0;
}

#heutamzyau .gt_from_md > :last-child {
  margin-bottom: 0;
}

#heutamzyau .gt_row {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  margin: 10px;
  border-top-style: solid;
  border-top-width: 1px;
  border-top-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 1px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 1px;
  border-right-color: #D3D3D3;
  vertical-align: middle;
  overflow-x: hidden;
}

#heutamzyau .gt_stub {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  text-transform: inherit;
  border-right-style: solid;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
  padding-left: 5px;
  padding-right: 5px;
}

#heutamzyau .gt_stub_row_group {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: initial;
  text-transform: inherit;
  border-right-style: solid;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
  padding-left: 5px;
  padding-right: 5px;
  vertical-align: top;
}

#heutamzyau .gt_row_group_first td {
  border-top-width: 2px;
}

#heutamzyau .gt_row_group_first th {
  border-top-width: 2px;
}

#heutamzyau .gt_summary_row {
  color: #333333;
  background-color: #FFFFFF;
  text-transform: inherit;
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
}

#heutamzyau .gt_first_summary_row {
  border-top-style: solid;
  border-top-color: #D3D3D3;
}

#heutamzyau .gt_first_summary_row.thick {
  border-top-width: 2px;
}

#heutamzyau .gt_last_summary_row {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
}

#heutamzyau .gt_grand_summary_row {
  color: #333333;
  background-color: #FFFFFF;
  text-transform: inherit;
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
}

#heutamzyau .gt_first_grand_summary_row {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  border-top-style: double;
  border-top-width: 6px;
  border-top-color: #D3D3D3;
}

#heutamzyau .gt_last_grand_summary_row_top {
  padding-top: 8px;
  padding-bottom: 8px;
  padding-left: 5px;
  padding-right: 5px;
  border-bottom-style: double;
  border-bottom-width: 6px;
  border-bottom-color: #D3D3D3;
}

#heutamzyau .gt_striped {
  background-color: rgba(128, 128, 128, 0.05);
}

#heutamzyau .gt_table_body {
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
}

#heutamzyau .gt_footnotes {
  color: #333333;
  background-color: #FFFFFF;
  border-bottom-style: none;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 2px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
}

#heutamzyau .gt_footnote {
  margin: 0px;
  font-size: 90%;
  padding-top: 4px;
  padding-bottom: 4px;
  padding-left: 5px;
  padding-right: 5px;
}

#heutamzyau .gt_sourcenotes {
  color: #333333;
  background-color: #FFFFFF;
  border-bottom-style: none;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-left-width: 2px;
  border-left-color: #D3D3D3;
  border-right-style: none;
  border-right-width: 2px;
  border-right-color: #D3D3D3;
}

#heutamzyau .gt_sourcenote {
  font-size: 90%;
  padding-top: 4px;
  padding-bottom: 4px;
  padding-left: 5px;
  padding-right: 5px;
}

#heutamzyau .gt_left {
  text-align: left;
}

#heutamzyau .gt_center {
  text-align: center;
}

#heutamzyau .gt_right {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

#heutamzyau .gt_font_normal {
  font-weight: normal;
}

#heutamzyau .gt_font_bold {
  font-weight: bold;
}

#heutamzyau .gt_font_italic {
  font-style: italic;
}

#heutamzyau .gt_super {
  font-size: 65%;
}

#heutamzyau .gt_footnote_marks {
  font-size: 75%;
  vertical-align: 0.4em;
  position: initial;
}

#heutamzyau .gt_asterisk {
  font-size: 100%;
  vertical-align: 0;
}

#heutamzyau .gt_indent_1 {
  text-indent: 5px;
}

#heutamzyau .gt_indent_2 {
  text-indent: 10px;
}

#heutamzyau .gt_indent_3 {
  text-indent: 15px;
}

#heutamzyau .gt_indent_4 {
  text-indent: 20px;
}

#heutamzyau .gt_indent_5 {
  text-indent: 25px;
}

#heutamzyau .katex-display {
  display: inline-flex !important;
  margin-bottom: 0.75em !important;
}

#heutamzyau div.Reactable > div.rt-table > div.rt-thead > div.rt-tr.rt-tr-group-header > div.rt-th-group:after {
  height: 0px !important;
}
</style>

<table class="gt_table caption-top table table-sm table-striped small" data-quarto-bootstrap="false">
<caption>Per-patient prediction grid (624 rows)</caption>
<thead>
<tr class="gt_col_headings header">
<th id="a::stub" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col"></th>
<th id="PatientID" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">PatientID</th>
<th id="BiopsyPhase" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">BiopsyPhase</th>
<th id="Arm" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">Arm</th>
<th id="pCR" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">pCR</th>
<th id="ROI_id" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">ROI_id</th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr class="odd">
<th id="stub_1_1" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">1</th>
<td class="gt_row gt_left" headers="stub_1_1 PatientID">NT031</td>
<td class="gt_row gt_left" headers="stub_1_1 BiopsyPhase">Baseline</td>
<td class="gt_row gt_left" headers="stub_1_1 Arm">C&I</td>
<td class="gt_row gt_left" headers="stub_1_1 pCR">RD</td>
<td class="gt_row gt_left" headers="stub_1_1 ROI_id">NT031_Baseline</td>
</tr>
<tr class="even">
<th id="stub_1_2" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">2</th>
<td class="gt_row gt_left" headers="stub_1_2 PatientID">NT277</td>
<td class="gt_row gt_left" headers="stub_1_2 BiopsyPhase">Baseline</td>
<td class="gt_row gt_left" headers="stub_1_2 Arm">C&I</td>
<td class="gt_row gt_left" headers="stub_1_2 pCR">RD</td>
<td class="gt_row gt_left" headers="stub_1_2 ROI_id">NT277_Baseline</td>
</tr>
<tr class="odd">
<th id="stub_1_3" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">3</th>
<td class="gt_row gt_left" headers="stub_1_3 PatientID">NT277</td>
<td class="gt_row gt_left" headers="stub_1_3 BiopsyPhase">On-treatment</td>
<td class="gt_row gt_left" headers="stub_1_3 Arm">C&I</td>
<td class="gt_row gt_left" headers="stub_1_3 pCR">RD</td>
<td class="gt_row gt_left" headers="stub_1_3 ROI_id">NT277_On-treatment</td>
</tr>
<tr class="even">
<th id="stub_1_4" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">4</th>
<td class="gt_row gt_left" headers="stub_1_4 PatientID">NT238</td>
<td class="gt_row gt_left" headers="stub_1_4 BiopsyPhase">Baseline</td>
<td class="gt_row gt_left" headers="stub_1_4 Arm">C&I</td>
<td class="gt_row gt_left" headers="stub_1_4 pCR">RD</td>
<td class="gt_row gt_left" headers="stub_1_4 ROI_id">NT238_Baseline</td>
</tr>
<tr class="odd">
<th id="stub_1_5" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">5</th>
<td class="gt_row gt_left" headers="stub_1_5 PatientID">NT238</td>
<td class="gt_row gt_left" headers="stub_1_5 BiopsyPhase">On-treatment</td>
<td class="gt_row gt_left" headers="stub_1_5 Arm">C&I</td>
<td class="gt_row gt_left" headers="stub_1_5 pCR">RD</td>
<td class="gt_row gt_left" headers="stub_1_5 ROI_id">NT238_On-treatment</td>
</tr>
<tr class="even">
<th id="stub_1_6" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier; font-size: x-small; background-color: #E4E4E4">6..623</th>
<td class="gt_row gt_left" headers="stub_1_6 PatientID" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_left" headers="stub_1_6 BiopsyPhase" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_left" headers="stub_1_6 Arm" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_left" headers="stub_1_6 pCR" style="background-color: #E4E4E4"></td>
<td class="gt_row gt_left" headers="stub_1_6 ROI_id" style="background-color: #E4E4E4"></td>
</tr>
<tr class="odd">
<th id="stub_1_7" class="gt_row gt_right gt_stub" data-quarto-table-cell-role="th" scope="row" style="font-family: Courier">624</th>
<td class="gt_row gt_left" headers="stub_1_7 PatientID">NT091</td>
<td class="gt_row gt_left" headers="stub_1_7 BiopsyPhase">On-treatment</td>
<td class="gt_row gt_left" headers="stub_1_7 Arm">C&I</td>
<td class="gt_row gt_left" headers="stub_1_7 pCR">RD</td>
<td class="gt_row gt_left" headers="stub_1_7 ROI_id">NT091_On-treatment</td>
</tr>
</tbody>
</table>

</div>
</div>
</div>
<div class="cell" data-layout-align="center">
<details open="" class="code-fold">
<summary>Predict per-patient proportions</summary>
<pre>pred_pat &lt;- sccomp_predict(fit, new_data = pat)</pre>
</details>
<div class="cell-output cell-output-stdout">
<pre>Running standalone generated quantities after 1 MCMC chain, with 1 thread(s) per chain...

Chain 1  Elapsed Time: 5.094 seconds (Generated Quantities) 
Chain 1 finished in 0.0 seconds.</pre>
</div>
<details open="" class="code-fold">
<summary>Predict per-patient proportions</summary>
<pre>pred_pat</pre>
</details>
<div class="cell-output cell-output-stdout">
<pre># A tibble: 9,360 × 12
   PatientID BiopsyPhase Arm   pCR   ROI_id         cell_group   proportion_mean
   &lt;chr&gt;     &lt;fct&gt;       &lt;fct&gt; &lt;fct&gt; &lt;chr&gt;          &lt;chr&gt;                  &lt;dbl&gt;
 1 NT031     Baseline    C&I   RD    NT031_Baseline CD20^+B               0.0175
 2 NT031     Baseline    C&I   RD    NT031_Baseline CD4^+PD1^+T           0.0427
 3 NT031     Baseline    C&I   RD    NT031_Baseline CD4^+TCF1^+T          0.0848
 4 NT031     Baseline    C&I   RD    NT031_Baseline CD56^+NK              0.0353
 5 NT031     Baseline    C&I   RD    NT031_Baseline CD79a^+Plas…          0.139 
 6 NT031     Baseline    C&I   RD    NT031_Baseline CD8^+GZMB^+T          0.0272
 7 NT031     Baseline    C&I   RD    NT031_Baseline CD8^+PD1^+T…          0.0420
 8 NT031     Baseline    C&I   RD    NT031_Baseline CD8^+T                0.0972
 9 NT031     Baseline    C&I   RD    NT031_Baseline CD8^+TCF1^+T          0.0477
10 NT031     Baseline    C&I   RD    NT031_Baseline DCs                   0.123 
# &#x2139; 9,350 more rows
# &#x2139; 5 more variables: proportion_lower &lt;dbl&gt;, proportion_upper &lt;dbl&gt;,
#   unconstrained_mean &lt;dbl&gt;, unconstrained_lower &lt;dbl&gt;,
#   unconstrained_upper &lt;dbl&gt;</pre>
</div>
</div>
</section>

<section id="results" class="level1">
<h1>Results</h1>
</section><section id="predicted-immune-compositional-trajectories" class="level2">
<h2 class="anchored" data-anchor-id="predicted-immune-compositional-trajectories">Predicted immune compositional trajectories</h2>
<p><strong>Figure 1</strong> shows baseline-aligned predicted proportions (95% credible intervals) for all 15 immune TME phenotypes. Within each arm × cell-type facet, both response groups are shifted to the mean of their baseline predictions. Panels are split by treatment arm (columns); colour denotes pathological response. Asterisks mark arm–time points with difference-in-differences FDR < 0.05.</p>
<div class="cell" data-layout-align="center">
<details class="code-fold">
<summary>Align groups to a common baseline</summary>
<pre># Plot shift. Within each arm and cell type, move pCR and RD to the mean of
# their baseline predictions, on the posterior draws.
draws &lt;- sccomp_predict(
  fit, new_data = pop, number_of_draws = 500, summary_instead_of_draws = FALSE
) |&gt;
  as_tibble() |&gt;
  mutate(across(c(Arm, BiopsyPhase, pCR), as.character))</pre>
</details>
<div class="cell-output cell-output-stdout">
<pre>Running standalone generated quantities after 1 MCMC chain, with 1 thread(s) per chain...

Chain 1  Elapsed Time: 0.183 seconds (Generated Quantities) 
Chain 1 finished in 0.0 seconds.</pre>
</div>
<details class="code-fold">
<summary>Align groups to a common baseline</summary>
<pre># Shift pCR and RD to their average baseline within each Arm x cell_group, per draw
shifts &lt;- draws |&gt;
  filter(BiopsyPhase == &quot;Baseline&quot;) |&gt;
  select(Arm, cell_group, .draw, pCR, proportion) |&gt;
  pivot_wider(names_from = pCR, values_from = proportion) |&gt;
  mutate(shift_pCR = (pCR + RD) / 2 - pCR,
         shift_RD  = (pCR + RD) / 2 - RD) |&gt;
  select(Arm, cell_group, .draw, shift_pCR, shift_RD)

aligned_summary &lt;- draws |&gt;
  left_join(shifts, by = c(&quot;Arm&quot;, &quot;cell_group&quot;, &quot;.draw&quot;)) |&gt;
  mutate(aligned_prop = proportion + if_else(pCR == &quot;pCR&quot;, shift_pCR, shift_RD)) |&gt;
  summarise(
    aligned_mean  = mean(aligned_prop),
    aligned_lower = quantile(aligned_prop, 0.025),
    aligned_upper = quantile(aligned_prop, 0.975),
    .by = c(Arm, BiopsyPhase, pCR, cell_group)
  )</pre>
</details>
</div>
<div class="cell" data-layout-align="center">
<details class="code-fold">
<summary>Baseline-aligned immune trajectories</summary>
<pre>cell_order &lt;- c(
  &quot;CD8^+T&quot;, &quot;CD8^+GZMB^+T&quot;, &quot;CD8^+TCF1^+T&quot;, &quot;CD8^+PD1^+T_{Ex}&quot;,
  &quot;CD4^+TCF1^+T&quot;, &quot;CD4^+PD1^+T&quot;, &quot;Treg&quot;, &quot;CD20^+B&quot;, &quot;CD79a^+Plasma&quot;,
  &quot;CD56^+NK&quot;, &quot;M2 Mac&quot;, &quot;DCs&quot;, &quot;Neutrophils&quot;, &quot;PD-L1^+APCs&quot;, &quot;PD-L1^+IDO^+APCs&quot;
)
cols_response &lt;- c(RD = &quot;#377EB8&quot;, pCR = &quot;#E41A1C&quot;)

pred_main &lt;- aligned_summary |&gt;
  mutate(
    BiopsyPhase = factor(BiopsyPhase, phase_levels),
    Arm = factor(Arm, arm_levels),
    pCR = factor(pCR, c(&quot;RD&quot;, &quot;pCR&quot;)),
    cell_group = factor(cell_group, levels = cell_order),
    # A few intervals sit just below zero after the baseline shift.
    aligned_mean = pmax(aligned_mean, 0),
    aligned_lower = pmax(aligned_lower, 0)
  )

# Asterisk just above the ribbon where the DiD FDR is below 0.05.
did_sig_fig1 &lt;- hmc_did |&gt;
  filter(contrast_family == &quot;did&quot;, is.finite(c_FDR), c_FDR &lt; 0.05) |&gt;
  distinct(cell_group, Arm, BiopsyPhase) |&gt;
  mutate(cell_group = factor(cell_group, levels = cell_order)) |&gt;
  inner_join(
    pred_main |&gt;
      summarise(ymax = max(aligned_upper), .by = c(cell_group, Arm, BiopsyPhase)),
    by = c(&quot;cell_group&quot;, &quot;Arm&quot;, &quot;BiopsyPhase&quot;)
  )

ggplot(pred_main, aes(x = BiopsyPhase, y = aligned_mean, colour = pCR, group = pCR)) +
  geom_ribbon(
    aes(ymin = aligned_lower, ymax = aligned_upper, fill = pCR),
    alpha = 0.10, colour = NA
  ) +
  geom_line(linewidth = 0.75) +
  geom_point(size = 1.2) +
  geom_text(
    data = did_sig_fig1,
    aes(x = BiopsyPhase, y = ymax, label = &quot;*&quot;),
    inherit.aes = FALSE,
    size = 4.5, colour = &quot;black&quot;, fontface = &quot;bold&quot;, vjust = -0.2
  ) +
  facet_grid(cell_group ~ Arm, scales = &quot;free_y&quot;) +
  scale_y_continuous(labels = scales::label_number(accuracy = 0.01)) +
  coord_cartesian(clip = &quot;off&quot;) +
  scale_colour_manual(values = cols_response, name = &quot;Response&quot;) +
  scale_fill_manual(values = cols_response, name = &quot;Response&quot;) +
  labs(
    title = &quot;Baseline-aligned immune compositional trajectories&quot;,
    x = NULL,
    y = &quot;Baseline-aligned predicted proportion&quot;
  ) +
  theme_pub() +
  theme(strip.text.y = element_text(size = 7))</pre>
</details>
<div class="cell-output-display">
<div id="fig-main-trajectories" class="quarto-float quarto-figure quarto-figure-center anchored" data-fig-align="center" alt="Line plots of baseline-aligned immune proportions across biopsy timepoints, faceted by cell type and treatment arm, with pathological complete response and residual disease in separate colours and asterisks where the difference-in-differences FDR is below 0.05.">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-main-trajectories-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<img src="https://i0.wp.com/tidyomics.github.io/tidyomicsBlog/posts/2026-10-23-sccomp-neotrip-compositional-dynamics/index_files/figure-html/fig-main-trajectories-1.png?w=578&#038;ssl=1" id="fig-main-trajectories" class="img-fluid quarto-figure quarto-figure-center anchored figure-img" alt="Line plots of baseline-aligned immune proportions across biopsy timepoints, faceted by cell type and treatment arm, with pathological complete response and residual disease in separate colours and asterisks where the difference-in-differences FDR is below 0.05." data-recalc-dims="1"/>
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig quarto-uncaptioned" id="fig-main-trajectories-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure 2
</figcaption>
</figure>
</div>
</div>
</div>
</section>
<section id="change-from-baseline" class="level2">
<h2 class="anchored" data-anchor-id="change-from-baseline">Change from baseline</h2>
<p>For each cell type and treatment arm, we test whether responders and patients with residual disease changed by different amounts from baseline. <strong>Figure 2</strong> shows that difference at the on-treatment and post-treatment visits. Bordered tiles have FDR < 0.05.</p>
<div class="cell" data-layout-align="center">
<details class="code-fold">
<summary>Difference-in-differences heatmap</summary>
<pre>did_hm &lt;- hmc_did |&gt;
  filter(contrast_family == &quot;did&quot;) |&gt;
  mutate(
    cell_group = factor(cell_group, levels = rev(cell_order)),
    BiopsyPhase = factor(BiopsyPhase, levels = c(&quot;On-treatment&quot;, &quot;Post-treatment&quot;)),
    Arm = factor(Arm, arm_levels)
  ) |&gt;
  filter(!is.na(cell_group)) |&gt;
  mutate(
    sig = is.finite(c_FDR) & c_FDR &lt; 0.05,
    facet_lab = factor(
      paste(Arm, BiopsyPhase, sep = &quot; · &quot;),
      levels = c(
        &quot;C · On-treatment&quot;, &quot;C · Post-treatment&quot;,
        &quot;C&#038;I · On-treatment&quot;, &quot;C&#038;I · Post-treatment&quot;
      )
    )
  )

lim &lt;- max(abs(did_hm$c_effect), na.rm = TRUE)

ggplot(did_hm, aes(x = facet_lab, y = cell_group, fill = c_effect)) +
  geom_tile(colour = &quot;grey90&quot;, linewidth = 0.3) +
  geom_tile(
    data = filter(did_hm, sig),
    colour = &quot;black&quot;, linewidth = 0.7, fill = NA
  ) +
  geom_text(
    aes(label = if_else(sig, sprintf(&quot;%.2f&quot;, c_effect), &quot;&quot;)),
    size = 2.6
  ) +
  scale_fill_gradient2(
    low = cols_response[[&quot;RD&quot;]], mid = &quot;white&quot;, high = cols_response[[&quot;pCR&quot;]],
    midpoint = 0, limits = c(-lim, lim), name = &quot;center_log_ratio&quot;
  ) +
  labs(
    title = &quot;Response-associated change from baseline (difference-in-differences)&quot;,
    x = NULL, y = NULL
  ) +
  theme_pub() +
  theme(
    axis.text.x = element_text(angle = 30, hjust = 1),
    panel.grid = element_blank()
  )</pre>
</details>
<div class="cell-output-display">
<div id="fig-did-heatmap" class="quarto-float quarto-figure quarto-figure-center anchored" alt="Heatmap of difference-in-differences effects, on the centered log-ratio scale, for each immune phenotype, treatment arm, and post-baseline visit. Black borders mark FDR below 0.05." data-fig-align="center" width="450">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-did-heatmap-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<img src="https://i2.wp.com/tidyomics.github.io/tidyomicsBlog/posts/2026-10-23-sccomp-neotrip-compositional-dynamics/index_files/figure-html/fig-did-heatmap-1.png?w=450&#038;ssl=1" id="fig-did-heatmap" class="img-fluid quarto-figure quarto-figure-center anchored figure-img" alt="Heatmap of difference-in-differences effects, on the centered log-ratio scale, for each immune phenotype, treatment arm, and post-baseline visit. Black borders mark FDR below 0.05." data-recalc-dims="1" />
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig quarto-uncaptioned" id="fig-did-heatmap-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure 3
</figcaption>
</figure>
</div>
</div>
</div>
<p><strong>Most of the signal is at post-treatment:</strong> 14 immune phenotypes have a significant DiD at post-treatment, and 9 on-treatment. For M2 Mac in the C&I arm, the on-treatment DiD FDR is 0.21 and the post-treatment DiD FDR is 0.007. The on-treatment marginal contrast largely repeats a baseline difference.</p>
</section>
<section id="m2-macrophage-changes-by-response" class="level2">
<h2 class="anchored" data-anchor-id="m2-macrophage-changes-by-response">M2 macrophage changes by response</h2>
<p><strong>Figure 3</strong> compares pCR versus residual disease at each visit with the change from baseline. Points above the dotted line are FDR < 0.05.</p>
<div class="cell" data-layout-align="center">
<details class="code-fold">
<summary>M2 macrophage changes by response</summary>
<pre>m2_volcano &lt;- bind_rows(
  hmc_marginal_pcr |&gt;
    filter(cell_group == &quot;M2 Mac&quot;) |&gt;
    mutate(Arm, BiopsyPhase, c_effect, c_FDR, family = &quot;Marginal pCR vs RD&quot;),
  hmc_did |&gt;
    filter(contrast_family == &quot;did&quot;, cell_group == &quot;M2 Mac&quot;) |&gt;
    mutate(Arm, BiopsyPhase, c_effect, c_FDR, family = &quot;DiD (change from T0)&quot;)
) |&gt;
  mutate(label = paste(Arm, BiopsyPhase, sep = &quot; · &quot;))

ggplot(m2_volcano, aes(x = c_effect, y = -log10(pmax(c_FDR, 1e-12)))) +
  geom_vline(xintercept = 0, linetype = 2, colour = &quot;grey70&quot;) +
  geom_hline(yintercept = -log10(0.05), linetype = 2, colour = &quot;grey70&quot;) +
  geom_point(aes(colour = family, shape = BiopsyPhase), size = 2.8) +
  geom_text(aes(label = label), size = 2.3, vjust = -0.9, colour = &quot;grey25&quot;) +
  scale_colour_manual(values = c(
    &quot;Marginal pCR vs RD&quot; = &quot;#999999&quot;,
    &quot;DiD (change from T0)&quot; = &quot;#d73027&quot;
  )) +
  labs(
    title = &quot;M2 Mac: visit contrast and change from baseline&quot;,
    x = &quot;center_log_ratio&quot;,
    y = expression(-log[10](FDR)),
    colour = NULL, shape = NULL
  ) +
  theme_pub()</pre>
</details>
<div class="cell-output-display">
<div id="fig-volcano" class="quarto-float quarto-figure quarto-figure-center anchored" alt="Scatter plot of M2 macrophage response contrasts, with the centered log-ratio effect on the x-axis and minus log10 FDR on the y-axis, comparing pCR versus residual disease at each visit with the change from baseline." data-fig-align="center" width="450">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-volcano-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<img src="https://i1.wp.com/tidyomics.github.io/tidyomicsBlog/posts/2026-10-23-sccomp-neotrip-compositional-dynamics/index_files/figure-html/fig-volcano-1.png?w=450&#038;ssl=1" id="fig-volcano" class="img-fluid quarto-figure quarto-figure-center anchored figure-img" alt="Scatter plot of M2 macrophage response contrasts, with the centered log-ratio effect on the x-axis and minus log10 FDR on the y-axis, comparing pCR versus residual disease at each visit with the change from baseline." data-recalc-dims="1" />
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig quarto-uncaptioned" id="fig-volcano-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure 4
</figcaption>
</figure>
</div>
</div>
</div>
</section>
<section id="between-patient-heterogeneity-in-m2-mac" class="level2">
<h2 class="anchored" data-anchor-id="between-patient-heterogeneity-in-m2-mac">Between-patient heterogeneity in M2 Mac</h2>
<p>The fixed effect is the population estimate. Patient random intercepts describe individual baselines and trajectories. <strong>Figure 4</strong> shows the per-patient predicted M2 Mac trajectories.</p>
<div class="cell" data-layout-align="center">
<details class="code-fold">
<summary>Between-patient M2 trajectories</summary>
<pre>pred_pat &lt;- hmc_pred_patient |&gt;
  filter(cell_group == &quot;M2 Mac&quot;) |&gt;
  mutate(cell_group = factor(cell_group))

pcr_cols &lt;- c(&quot;RD&quot; = &quot;#4575b4&quot;, &quot;pCR&quot; = &quot;#d73027&quot;)

ggplot(pred_pat, aes(x = BiopsyPhase, y = proportion_mean, group = PatientID, colour = pCR)) +
  geom_line(linewidth = 0.4, alpha = 0.5) +
  facet_grid(cell_group ~ Arm + pCR, scales = &quot;free_y&quot;) +
  scale_colour_manual(values = pcr_cols, name = &quot;Response&quot;) +
  labs(
    title = &quot;Between-patient variation in predicted M2 Mac proportion&quot;,
    x = NULL,
    y = &quot;Predicted proportion&quot;
  ) +
  theme_pub() +
  theme(legend.position = &quot;bottom&quot;, strip.text = element_text(size = 8))</pre>
</details>
<div class="cell-output-display">
<div id="fig-patient-heterogeneity" class="quarto-float quarto-figure quarto-figure-center anchored" alt="Line plots of predicted M2 macrophage proportion for each patient across biopsy timepoints, faceted by treatment arm and pathological response." data-fig-align="center" width="450">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-patient-heterogeneity-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<img src="https://i0.wp.com/tidyomics.github.io/tidyomicsBlog/posts/2026-10-23-sccomp-neotrip-compositional-dynamics/index_files/figure-html/fig-patient-heterogeneity-1.png?w=450&#038;ssl=1" id="fig-patient-heterogeneity" class="img-fluid quarto-figure quarto-figure-center anchored figure-img" alt="Line plots of predicted M2 macrophage proportion for each patient across biopsy timepoints, faceted by treatment arm and pathological response." data-recalc-dims="1" />
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig quarto-uncaptioned" id="fig-patient-heterogeneity-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure 5
</figcaption>
</figure>
</div>
</div>
</div>
</section>
<section id="visualise-results-in-spatial-context" class="level2">
<h2 class="anchored" data-anchor-id="visualise-results-in-spatial-context">Visualise results in spatial context</h2>
<p><strong>Figure 2</strong> identifies two C&I post-treatment phenotypes that move in opposite directions by response: M2 Mac increases more from baseline in pCR (DiD FDR 0.007), whereas PD-L1+APCs decreases (DiD FDR 0.000). <strong>Figure 5</strong> plots both markers on the tissue for 20 representative patients per response group (those closest to each group’s median post-treatment M2 proportion), showing all post-treatment ROIs per patient (pCR left, RD right; magenta = M2 Mac, yellow = PD-L1+APCs, green = other immune TME).</p>
<div class="cell" data-layout-align="center">
<details class="code-fold">
<summary>Spatial maps of post-treatment cores</summary>
<pre>cores &lt;- read_csv(file.path(post_dir, &quot;spatial-cores.csv&quot;), show_col_types = FALSE)
cells &lt;- read_fst(file.path(post_dir, &quot;spatial-cells.fst&quot;)) |&gt; as_tibble()

mk_roi &lt;- function(image_id) {
  if (is.na(image_id)) return(patchwork::plot_spacer())
  mk_spatial_tile(filter(cells, ImageID == image_id))
}

mk_row &lt;- function(pid) {
  image_ids &lt;- cores |&gt;
    filter(PatientID == pid) |&gt;
    arrange(roi) |&gt;
    pull(ImageID)
  ids &lt;- c(image_ids, rep(NA_character_, 3L - length(image_ids)))
  patchwork::wrap_plots(
    ggplot() +
      annotate(&quot;text&quot;, x = 0.5, y = 0.5, label = pid, fontface = &quot;bold&quot;, size = 2.8) +
      scale_x_continuous(limits = c(0, 1), expand = c(0, 0)) +
      scale_y_continuous(limits = c(0, 1), expand = c(0, 0)) +
      theme_void(),
    patchwork::wrap_plots(lapply(ids, mk_roi), nrow = 1, ncol = 3),
    ncol = 1,
    heights = c(0.16, 1)
  )
}

mk_block &lt;- function(response, title) {
  one_column &lt;- function(which_side) {
    pids &lt;- cores |&gt;
      filter(pCR == response, side == which_side) |&gt;
      arrange(patient) |&gt;
      distinct(PatientID) |&gt;
      pull(PatientID)
    patchwork::wrap_plots(lapply(pids, mk_row), ncol = 1)
  }
  patchwork::wrap_plots(one_column(&quot;left&quot;), one_column(&quot;right&quot;), ncol = 2) +
    patchwork::plot_annotation(
      title = title,
      theme = theme(
        plot.title = element_text(face = &quot;bold&quot;, size = 12, hjust = 0.5, margin = margin(b = 4))
      )
    )
}

divider &lt;- ggplot() +
  annotate(&quot;segment&...</pre></details></div></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://tidyomics.github.io/tidyomicsBlog/posts/2026-10-23-sccomp-neotrip-compositional-dynamics/"> tidyomicsBlog</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/modelling-cell-type-proportions-in-repeated-measures-designs-with-sccomp-a-tma-case-study/">Modelling cell-type proportions in repeated-measures designs with sccomp: a TMA case study</a>]]></content:encoded>
					
		
		<enclosure url="https://tidyomics.github.io/tidyomicsBlog/posts/2026-10-23-sccomp-neotrip-compositional-dynamics/tma-cores.png" length="0" type="image/png" />

		<post-id xmlns="com-wordpress:feed-additions:1">404594</post-id>	</item>
		<item>
		<title>Agents in Production Webinar: From a Chatbot to a Tool You Can Rely On</title>
		<link>https://www.r-bloggers.com/2026/10/agents-in-production-webinar-from-a-chatbot-to-a-tool-you-can-rely-on/</link>
		
		<dc:creator><![CDATA[The Jumping Rivers Blog]]></dc:creator>
		<pubDate>Tue, 27 Oct 2026 23:59:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://www.jumpingrivers.com/blog/agents-in-production-webinar/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>Ask a large language model a question about your data, and you’ll probably get a confident answer.<br />
Ask it again, and you might get a different one.<br />
Sometimes, neither answer is correct.<br />
That’s a problem if you’re building an AI ...</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/agents-in-production-webinar-from-a-chatbot-to-a-tool-you-can-rely-on/">Agents in Production Webinar: From a Chatbot to a Tool You Can Rely On</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/agents-in-production-webinar/"> 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/agents-in-production-webinar/">
<img src="https://i1.wp.com/www.jumpingrivers.com/blog/agents-in-production-webinar/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>Ask a large language model a question about your data, and you’ll probably get a confident answer.
Ask it again, and you might get a different one.
Sometimes, neither answer is correct.</p>
<p>That’s a problem if you’re building an AI agent other people need to use.
How do you get dependable results from it, and what does it take to make it ready for production?</p>
<p>On <strong>19 November 2026</strong>, join <a href="https://www.jumpingrivers.com/authors/pedro-silva/" rel="nofollow" target="_blank">Pedro Silva</a>, Principal Data Scientist at Jumping Rivers, for this free webinar.</p>
<blockquote>
<p><a href="https://www.jumpingrivers.com/webinars/" rel="nofollow" target="_blank"><strong>Register for the webinar</strong></a></p>
</blockquote>
<h2 id="from-a-working-agent-to-production">From a Working Agent to Production</h2>
<p>One way to make an agent more reliable is to keep the language model away from the arithmetic.
Pedro will show how to build agents in R where the model decides which function to call and R does the calculations.
The agent answers questions about your data with existing R functions, so the numbers come from R, not from the model.</p>
<p>Getting an agent to work in a demo is the easy part.
Once other people start using it, new questions come up.
How do you manage separate conversations for different users?
How do you test an agent whose responses may vary between runs?
How do you keep a record of its actions, and which safeguards can you rely on?</p>
<p>Pedro will work through each of these and show what it takes to turn a working prototype into a tool other people can depend on.</p>
<h2 id="who-should-attend">Who Should Attend?</h2>
<p>The webinar is for data scientists, R developers and anyone looking at building and deploying AI agents.</p>
<p>It will be useful whether you’re trying out agents for the first time or have an application you want to open up to more users.</p>
<h2 id="continued-learning-benefits">Continued Learning Benefits</h2>
<p>If you join more of our webinars, you can also get discounts:</p>
<ul>
<li>Attend two webinars and receive 20% off tickets to the <a href="https://ai-in-production.jumpingrivers.com/" rel="nofollow" target="_blank">AI in Production 2027 conference</a></li>
<li>Attend more than two webinars and receive 20% off any <a href="https://www.jumpingrivers.com/training/public/" rel="nofollow" target="_blank">Jumping Rivers public training course</a></li>
</ul>
<h2 id="webinar-details">Webinar Details</h2>
<ul>
<li><strong>Topic:</strong> Agents in Production: From a Chatbot to a Tool You Can Rely On</li>
<li><strong>Speaker:</strong> Pedro Silva</li>
<li><strong>Date:</strong> Thursday, 19 November 2026</li>
<li><strong>Time:</strong> 1:15pm UK time</li>
<li><strong>Location:</strong> Online</li>
<li><strong>Cost:</strong> Free</li>
</ul>
<blockquote>
<p><a href="https://www.jumpingrivers.com/webinars/" rel="nofollow" target="_blank"><strong>Register for the webinar</strong></a></p>
</blockquote>
<p>
For updates and revisions to this article, see the <a href = "https://www.jumpingrivers.com/blog/agents-in-production-webinar/">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/agents-in-production-webinar/"> 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/agents-in-production-webinar-from-a-chatbot-to-a-tool-you-can-rely-on/">Agents in Production Webinar: From a Chatbot to a Tool You Can Rely On</a>]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">404488</post-id>	</item>
	</channel>
</rss>
