<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://www.ronanlaker.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.ronanlaker.com/" rel="alternate" type="text/html" /><updated>2026-07-13T09:41:59+00:00</updated><id>https://www.ronanlaker.com/feed.xml</id><title type="html">Ronan Laker</title><subtitle>Data scientist with a space physics PhD</subtitle><author><name>Ronan Laker</name></author><entry><title type="html">Modelling time series as a language</title><link href="https://www.ronanlaker.com/chronos-timeseries-model/" rel="alternate" type="text/html" title="Modelling time series as a language" /><published>2026-07-07T00:00:00+00:00</published><updated>2026-07-07T00:00:00+00:00</updated><id>https://www.ronanlaker.com/chronos-timeseries-model</id><content type="html" xml:base="https://www.ronanlaker.com/chronos-timeseries-model/"><![CDATA[<style>
.breakout {
  width: min(95vw, 1400px);
  position: relative;
  left: 50%;
  transform: translateX(-50%);
}
.breakout table {
  display: table;
  width: auto;
  margin-left: auto;
  margin-right: auto;
}
</style>

<p>Time series models normally require the designing of specific features to capture something about the underlying patterns:</p>
<ul>
  <li>Fourier terms, Radial Basis functions etc in old <a href="/linear-models-demystified">blog post about linear models</a></li>
  <li>holiday events</li>
  <li>Calendar features (hour, day of week, weekend)</li>
</ul>

<p>However, with the recent success of modelling lanugages with enormous amounts of data and processing power, a team at AWS asked</p>

<blockquote>
  <p>what are the fundamental differences between a language model that predicts the next token, and a time series forecasting model that predicts the next values? … Both endeavors fundamentally aim to model the sequential structure of the data to predict future patterns. Shouldn’t good language models “just work” on time series?</p>
</blockquote>

<p>Previous attempts at applying LLMs to timeseries forecasting have consisted of clever fine-tuning of existing models (<a href="https://arxiv.org/pdf/2302.11939">GPT4TS</a>, <a href="https://arxiv.org/abs/2102.06828">Time-LLM</a>) or just asking the LLM nicely (<a href="https://arxiv.org/abs/2210.08964">Promptcast</a>). As with all the best ideas, it seems obvious in hindsight that a model trained on language is clearly not going to beat a LLM-type model trained on timeseries data. While it sounds easy on paper, the two major challenges are:</p>
<ul>
  <li>there is much much more structured text data to learn from when compared to timeseries</li>
  <li>how do we define a “vocabulary” for our timeseries?</li>
</ul>

<p>The first problem can be solved by taking the clean timeseries data that already exists, like the <a href="https://forecasters.org/resources/time-series-data/">M competition</a>, and applying a bit of mathematical pick-n-mix to create a wide corpus of training data</p>

<p><img src="/files/chronos/TSMixup.png" alt="Fig. 2 from the Chronos paper" /></p>

<p>As for vocabulary, the authors scaled and quantized the data into 4096 buckets that represent the tokens.</p>

<p><img src="/files/chronos/timeseries_to_tokens.png" alt="Excerpt of Fig 1. from the Chronos paper" /></p>

<p><strong>That’s it.</strong></p>

<p>Now all we have to do is borrow the transformer architecture that worked so well for modelling language and applying to the newly generated dataset of timeseries.</p>

<p>The results are frankly ridiculous for zero-shot forecasting with no special timeseries features.</p>

<p><img src="/files/chronos/chronos_results.png" alt="" /></p>

<h1 id="how-does-it-work">How does it work?</h1>

<h2 id="sparse-autoencoders">Sparse autoencoders</h2>

<p>Just as we have borrowed the architecture used to train LLMs, we can also apply the same “mechanistic interpretability” techniques used to understand how the model learns “concepts”. These papers are a bit dense, but I understood it much more clearly after reading <a href="https://www.welchlabs.com/store/illustrated-guide-to-ai">Illustrated guide to AI</a>. You can see the video version of the chapter <a href="https://youtube.com/watch?v=UGO_Ehywuxc&amp;is=imtGB8tahD_9I3MD">here</a></p>

<p><img src="https://youtube.com/watch?v=UGO_Ehywuxc&amp;is=imtGB8tahD_9I3MD" alt="" /></p>

<p>One of the main take aways is that these neural networks represent more features than they have dimensions for encoding them, known as <a href="https://arxiv.org/abs/2209.10652">superposition hypothesis</a>. For any given model, we can train another “sparse autoencoder” to map the neuron outputs at any given layer to concepts. Once we know which combinations of neurons lead to a certain concept, we can find examples in the training text that maximise these neuron patterns. Some legend set up <a href="https://www.neuronpedia.org/search-explanations/">Neuronpedia</a> to easily visualise the findings of 400 different sparse autoencoders on the Gemini models.</p>

<p>While these techniques are now commonplace for language models, they are fairly new for the timeseries equivalents. In March 2026, <a href="https://arxiv.org/pdf/2603.10071">Mishra 2026</a> dissected Chronos and discovered that different layers of the network focussed on different classes of features (seasonality, level shifts, change-detection). The jury is still out however, as another <a href="https://arxiv.org/html/2605.05151">paper</a> claims the model predictions did not change much when they purposefully set high neuron activations to zero. They think this could still explain why linear models continue to perform well.</p>

<h2 id="occlusion">Occlusion</h2>

<p>Another way to peek inside the model’s brain is to purposefully occlude data from the input and measure how much worse this makes the forecast. Since Chronos gives us a probability of each token, we can use “Continuous Ranked Probability Score” (CRPS), like they do in <a href="https://arxiv.org/pdf/2603.10071">Mishra 2026</a>, to estimate the accuracy and precision of the forecast. We can then interpret this, with a pinch of salt, as where the model is “looking” when it makes its predictions.</p>

<p>I knocked up this quick <a href="https://github.com/rlaker/chronos-activation-viz">demo</a> to demonstrate the above for a few example datasets, which turn out to be fascinating! (Warning: this is not meant to be scientific, just a bit curious)</p>

<p>For a seasonal pattern, you can see that Chronos is “looking” at the peaks of the pattern at several points in the past:</p>

<iframe src="/files/chronos/seasonal_attribution.html" width="1200px" height="400px" style="border:none; display:block; margin:0 auto;"></iframe>

<p>It’s even more interesting when we add a linear trend term. I interpret the red dots as Chronos trying to find the amplitude for its future predictions</p>

<iframe src="/files/chronos/trend_seasonal_attribution.html" width="1200px" height="400px" style="border:none; display:block; margin:0 auto;"></iframe>

<p>Whereas for a linear trend, no single point is relied on. There is no real change is we occlude a single point (hence all blue)</p>

<iframe src="/files/chronos/trend_attribution.html" width="1200px" height="400px" style="border:none; display:block; margin:0 auto;"></iframe>

<p>As <a href="https://arxiv.org/pdf/2603.10071">Mishra 2026</a> claimed, Chronos clearly ignores the data before the regime change and also focusses more on recent data</p>

<iframe src="/files/chronos/regime_attribution.html" width="1200px" height="400px" style="border:none; display:block; margin:0 auto;"></iframe>

<h2 id="chronos2">Chronos2</h2>

<p>Turns out I’m quite late to the party, as the AWS team has already released a huge improvement with <a href="https://arxiv.org/pdf/2510.15821">Chronos2</a> that adds the ability to use exogenous variables through a “group attention” mechanism. I’ll leave that for another time after I have tried these models on my own data at work.</p>]]></content><author><name>Ronan Laker</name></author><category term="code" /><category term="data" /><category term="til" /><summary type="html"><![CDATA[and why it sort of just *works*]]></summary></entry><entry><title type="html">Publishing my Obsidian cooking vault as a static site</title><link href="https://www.ronanlaker.com/cooking-website-export/" rel="alternate" type="text/html" title="Publishing my Obsidian cooking vault as a static site" /><published>2026-06-19T00:00:00+00:00</published><updated>2026-06-19T00:00:00+00:00</updated><id>https://www.ronanlaker.com/cooking-website-export</id><content type="html" xml:base="https://www.ronanlaker.com/cooking-website-export/"><![CDATA[<p>I have an Obsidian vault to store everything I know about cooking: recipes, ingredients, cookbooks and restaurant notes. Each has a YAML frontmatter and <code class="language-plaintext highlighter-rouge">[[wikilinks]]</code> that allow me to query the notes like a database. I publish a public version of these notes at <a href="https://www.ronanlaker.com/cooking">ronanlaker.com/cooking</a>, but the result was always buggy.</p>

<p>The old workflow required several convoluted steps:</p>
<ol>
  <li>A Python script to move only the relevant notes to a new folder and strip out some private content</li>
  <li>Open the new folder with Obsidian and use the <a href="https://community.obsidian.md/plugins/webpage-html-export">Webpage HTML Export</a> plugin to export HTML files to yet another folder</li>
  <li>Push the final folder to Github as a repo named “cooking”, so that it will appear at the right url when published with GitHub pages</li>
</ol>

<p>The main problem is buggy rendering of <a href="https://community.obsidian.md/plugins/dataview">dataview</a> plugin, which underpins the functionality of the vault. This workflow also needed a GUI to complete and was extremely slow for my 1000+ pages.</p>

<h2 id="the-new-architecture">The new architecture</h2>

<p>I knew the exported website wasn’t up to scratch for a while but had no idea how to get around the dataview issue, apart from waiting for the plugin authors to update their code. I laid out this predicament for Claude who then diligently prepared a new pipeline based on Python and Quartz v5.</p>

<p>Claude was able to write a full Python interpreter for the dataview queries, like:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>table length, healthy, course
from "100 Recipes"
where icontains(cuisine, "french")
sort course,length asc
</code></pre></div></div>

<p>It’s not the most complex query, but I was very pleasantly surprised that this worked straight away. I think the biggest improvement in AI is the fact that it writes tests at each stage of the development. After the Python code has converted the dataview queries to Markdown lists and tables, Quartz v5 then builds a fully functional static website with backlinks, interactive graph etc all thanks to greater <a href="https://quartz.jzhao.xyz/getting-started/whats-new#improved-obsidian-compatibility">Obsidan compatability</a>.</p>

<p>The final result is that I can now just run <code class="language-plaintext highlighter-rouge">make deploy</code> to build the website from scratch and push to GitHub.</p>]]></content><author><name>Ronan Laker</name></author><category term="code" /><category term="food" /><summary type="html"><![CDATA[Another thing Claude has solved for me]]></summary></entry><entry><title type="html">Showing my Unsplash gallery on my blog</title><link href="https://www.ronanlaker.com/showing-unsplash-on-blog/" rel="alternate" type="text/html" title="Showing my Unsplash gallery on my blog" /><published>2026-06-05T00:00:00+00:00</published><updated>2026-06-05T00:00:00+00:00</updated><id>https://www.ronanlaker.com/showing-unsplash-on-blog</id><content type="html" xml:base="https://www.ronanlaker.com/showing-unsplash-on-blog/"><![CDATA[<p>I wanted to show my <a href="https://unsplash.com/@rlaker">Unsplash photos</a> directly on my <a href="/photos/">photography page</a> instead of just showing a text link. Since Unsplash doesn’t have an embed widget, I needed to use their API.</p>

<h2 id="the-naive-approach-and-why-i-didnt-use-it">The naive approach (and why I didn’t use it)</h2>

<p>The simplest option is to call the Unsplash API from JavaScript on the client side:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://api.unsplash.com/users/rlaker/photos</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
  <span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">'</span><span class="s1">Authorization</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Client-ID YOUR_ACCESS_KEY</span><span class="dl">'</span> <span class="p">}</span>
<span class="p">})</span>
</code></pre></div></div>

<p>This works, but it has two problems:</p>

<ol>
  <li><strong>Your API key is visible in the page source.</strong> Unsplash’s demo keys are read-only and rate-limited, so it’s not catastrophic, but it’s not great either.</li>
  <li><strong>Rate limiting.</strong> The demo tier allows 50 requests per hour. Each page view is one request that collects the metadata of all my images on unsplash (the photos themselves are served from Unsplash’s CDN and don’t count as requests).</li>
</ol>

<h2 id="the-build-time-approach">The build-time approach</h2>

<p>Instead, with the help of Claude, I fetch the photos at build time using a GitHub Actions workflow. The workflow runs weekly (and can be triggered manually), calls the Unsplash API, and saves the results to <code class="language-plaintext highlighter-rouge">_data/unsplash.json</code>. Jekyll reads this file as <code class="language-plaintext highlighter-rouge">site.data.unsplash</code> during the build, so the photos are baked into the HTML meaning no API calls when someone views the page.</p>

<h3 id="the-github-action">The GitHub Action</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Fetch Unsplash photos</span>

<span class="na">on</span><span class="pi">:</span>
  <span class="na">schedule</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">cron</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0</span><span class="nv"> </span><span class="s">6</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">1"</span>  <span class="c1"># every Monday at 6am</span>
  <span class="na">workflow_dispatch</span><span class="pi">:</span>       <span class="c1"># manual trigger</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">fetch</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Fetch photos from Unsplash</span>
        <span class="na">env</span><span class="pi">:</span>
          <span class="na">UNSPLASH_ACCESS_KEY</span><span class="pi">:</span> <span class="s">${{ secrets.UNSPLASH_ACCESS_KEY }}</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s"># fetch all photos, paginating through results</span>
          <span class="s"># save to _data/unsplash.json</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Commit if changed</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">git add _data/unsplash.json</span>
          <span class="s">git diff --cached --quiet &amp;&amp; exit 0</span>
          <span class="s">git commit -m "update Unsplash photo data"</span>
          <span class="s">git push</span>
</code></pre></div></div>

<p>The API key lives in a GitHub secret and never touches the repo or the client.</p>

<h3 id="the-template">The template</h3>

<p>The photos page loops over the data file using Liquid:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">photo</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site.data.unsplash</span><span class="w"> </span><span class="p">%}</span>
  &lt;a href="<span class="p">{{</span><span class="w"> </span><span class="nv">photo</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">}}</span>"&gt;
    &lt;img src="<span class="p">{{</span><span class="w"> </span><span class="nv">photo</span><span class="p">.</span><span class="nv">src</span><span class="w"> </span><span class="p">}}</span>" alt="<span class="p">{{</span><span class="w"> </span><span class="nv">photo</span><span class="p">.</span><span class="nv">alt</span><span class="w"> </span><span class="p">}}</span>" loading="lazy"&gt;
  &lt;/a&gt;
<span class="p">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="p">%}</span>
</code></pre></div></div>

<p>I show the first 12 photos and hide the rest behind a “Load more” button that just removes <code class="language-plaintext highlighter-rouge">display:none</code> — no JavaScript fetch needed.</p>

<h3 id="the-data-file">The data file</h3>

<p>Each entry in <code class="language-plaintext highlighter-rouge">unsplash.json</code> looks like:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://unsplash.com/photos/..."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"src"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://images.unsplash.com/...?w=400"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"alt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"photo description"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"longer description"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">src</code> field uses Unsplash’s <code class="language-plaintext highlighter-rouge">small</code> size (400px wide), which is plenty for a thumbnail grid and keeps the page fast.</p>

<h2 id="setup-steps">Setup steps</h2>

<ol>
  <li>Create an app on <a href="https://unsplash.com/developers">unsplash.com/developers</a> and grab the Access Key</li>
  <li>Add it as a GitHub secret called <code class="language-plaintext highlighter-rouge">UNSPLASH_ACCESS_KEY</code> (repo Settings &gt; Secrets and variables &gt; Actions)</li>
  <li>Run the workflow manually once to generate the initial <code class="language-plaintext highlighter-rouge">_data/unsplash.json</code></li>
  <li>After that, the workflow refreshes it weekly</li>
</ol>

<h2 id="the-result">The result</h2>

<p>My <a href="/photos/">photos page</a> now shows my latest Unsplash uploads at the top, automatically refreshed every week, with no API key exposed and no client-side rate limiting to worry about.</p>]]></content><author><name>Ronan Laker</name></author><category term="code" /><category term="til" /><category term="photo" /><summary type="html"><![CDATA[How I embedded my Unsplash photos on my Jekyll site without exposing an API key]]></summary></entry><entry><title type="html">TIL: Hidden Markov Model</title><link href="https://www.ronanlaker.com/HMM/" rel="alternate" type="text/html" title="TIL: Hidden Markov Model" /><published>2026-06-01T00:00:00+00:00</published><updated>2026-06-01T00:00:00+00:00</updated><id>https://www.ronanlaker.com/HMM</id><content type="html" xml:base="https://www.ronanlaker.com/HMM/"><![CDATA[<style>
.breakout {
  width: min(95vw, 1400px);
  position: relative;
  left: 50%;
  transform: translateX(-50%);
}
.breakout table {
  display: table;
  width: auto;
  margin-left: auto;
  margin-right: auto;
}
</style>

<p>While doing research for a consulting project that sadly went nowhere (classifying rock type by core samples if you’re asking), I discovered Hidden Markov Models (HMMs).</p>

<h1 id="weather-example">Weather example</h1>

<p>Imagine we want to classify the state of weather, as us British are wont to do, based on the Temperature (C) and Humidity (%).</p>

<p>We might design a naive model that could classify the three different states (Sunny, Cloudy and Rainy) based on thresholds in the temperature and humidity variables. The scatter points in the plot below represent the observed temperature and humidity measurements for 60 time steps, which clearly have some noise as they have a good amount of overlap. The dotted black lines show the decision boundaries of the Naive classifier, which does an OK job of classifying states, but noisy data could lead to the predicted state flipping back and forth in an un-natural way.</p>

<iframe src="/files/HMM_example/naive_decision_boundaries.html" width="800px" height="800px" style="border:none; display:block; margin:0 auto;"></iframe>

<p>We can avoid this with the genius of a Hidden Markov Model that allows us to define both:</p>
<ul>
  <li>How likely the transition between states is</li>
  <li>What is the probability of seeing parameters T and H, given a state $s$?</li>
</ul>

<p>In technical lingo, the HMM allows us to encode our special British knowledge into the “transition matrix” and “emission probabilities”.</p>

<h2 id="transition-matrix">Transition matrix</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>TRANSITION = np.array([
    # Sunny  Cloudy  Rainy
    [0.75,   0.20,   0.05],   # from Sunny
    [0.25,   0.55,   0.20],   # from Cloudy
    [0.05,   0.35,   0.60],   # from Rainy
])
</code></pre></div></div>

<p>The transition probabilities can be see above. As an example how to read the matrix, we are saying if it is currently Sunny there is a 75% chance it will remain Sunny, 20% it will become cloudy and only 5% it will transition straight to rainy.</p>

<p>This is a really elegant design, since we could even update our knowledge. Maybe we think the chance of transitioning from state Sunny -&gt; Rainy is more probable in April over August.</p>

<h2 id="emission-probabilities">Emission probabilities</h2>

<p>The emission probabilities capture how likely a certain measurement is, given we are in the titular “hidden markov state”.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>EMISSION_MEANS = np.array([
    [24.0, 45.0],   # Sunny
    [19.0, 65.0],   # Cloudy
    [14.0, 80.0],   # Rainy
])

EMISSION_COVS = np.array([
    [[4.5**2, 0.0   ], [0.0, 14.0**2]],   # Sunny
    [[4.5**2, 0.0   ], [0.0, 14.0**2]],   # Cloudy
    [[4.0**2, 0.0   ], [0.0, 12.0**2]],   # Rainy
])
</code></pre></div></div>

<p>In the example above, the probability of observing a temperature T when it is Sunny follows a Gaussian with $\mu = 24C$ and a $\sigma = 4.5C$</p>

<blockquote>
  <p>Note: I am just using Gaussian probabilities because it makes things easy here. The model can work with any distribution</p>
</blockquote>

<p>This can be visualised by plotting the same scatter points as above, but this time with the shaded regions showing the 65% confidence interval of each state</p>

<iframe src="/files/HMM_example/emissions_2d_gaussian.html" width="800px" height="800px" style="border:none; display:block; margin:0 auto;"></iframe>

<h1>Viterbi algorithm</h1>

<p>So how do we find the most probable path through all of these states? There are a few options, but the most ingenious is the Viterbi algorithm which goes a little something like this…</p>

<p>At the first time step, we see the emission, $o_1$.  Therefore, we can represent the probability of the first state $s_1$ with the following product:</p>

\[P(s_1{=}s,\; o_1) \;=\; \underbrace{\pi_s}_{\text{start in } s} \;\times\; \underbrace{f(o_1 \mid s)}_{\text{which then emits } o_1}\]

<p>where $\pi_s$ is the starting probabilities of each state and $f$ is our Gaussian function from earlier.</p>

<p>Using the Markov property that “each state is only depends on the one before it”, we can create a recursive equation for the score of the best path ending in state $s$ at each step (i.e. the joint probability of that path having produced all observations $o_1,\ldots,o_t$).</p>

\[D_t(s) \;=\; \max_{s'} \Big[ \, D_{t-1}(s') \cdot A_{s' \to s} \, \Big] \cdot f(o_t \mid s)\]

<ul>
  <li>$D_{t-1}(s’)$ is the “probability of the best path ending in state $s’$ at the previous step, having explained $o_1, \ldots, o_{t-1}$.” This is the running joint probability of one specific path through the states.</li>
  <li>$\times \; A_{s’ \to s}$ represents the transition from $s’$ to $s$. Given the Markov property, we just read this from the transition matrix</li>
  <li>$\max_{s’}\;[\ldots]$ keeps the winning path at each step.</li>
  <li>$\cdot \; f(o_t \mid s)$ captures the probability of this state, $s$, emitting observation $o_t$.”</li>
</ul>

<p>However, before we try and compute this we need to realise that we are going to be multiplying many small numbers together. If we are multiply probabilities like 0.05*0.03 (~$10^{-4}$) at each time step we end up trying to compute numbers like $10^{-240}$ for our 60-step sequence. Obviously this is madness, so instead we will take the log-probabilities so we end up with the following equation:</p>

\[\delta_t(s) = \max_{s'}\big[\delta_{t-1}(s') + \log A_{s' \to s}\big] + \log f(o_t \mid s)\]

<p>For each destination state, we can recored which previous state $s’$ led to the maximum score at that step.</p>

\[\psi_t(s) \;=\; \arg\max_{s'} \Big[ \delta_{t-1}(s') + \log A_{s' \to s} \Big]\]

<p>i.e. “for each destination state $s$, $\psi_t(s)$ records which previous state $s’$ achieved the max.”</p>

<p>At the end of the sequence we pick the highest-scoring final cell:
\(\hat s_T \;=\; \arg\max_s \, \delta_T(s)\)
and then walk the stored backpointers right-to-left to reconstruct the full path:
\(\hat s_{t-1} \;=\; \psi_t(\hat s_t), \quad t = T, T-1, \ldots, 2\)</p>

<blockquote>
  <p>Better explanation <a href="https://inst.eecs.berkeley.edu/~cs188/textbook/hmms/viterbi.html">here</a></p>
</blockquote>

<h2 id="run-through">Run through</h2>

<p>Let’s walk through the first two steps of this algorithm, with the temperature and humidity readings, along with their true state, in the table below:</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>$T$ (°C)</th>
      <th>$H$ (%)</th>
      <th>true state</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>$o_1$</td>
      <td>17.77</td>
      <td>69.18</td>
      <td>Cloudy</td>
    </tr>
    <tr>
      <td>$o_2$</td>
      <td>19.54</td>
      <td>38.63</td>
      <td>Sunny</td>
    </tr>
  </tbody>
</table>

<p>At step 1 there is no previous state to transition from, so we initialise with just the prior on the starting state and the likelihood of the first observation</p>

<table>
  <thead>
    <tr>
      <th>State</th>
      <th>$\log \pi_s$</th>
      <th>$\log f(o_1 \mid s)$</th>
      <th>$\delta_1(s)$</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Sunny</td>
      <td>$-0.69$</td>
      <td>$-8.43$</td>
      <td>$-9.13$</td>
    </tr>
    <tr>
      <td>Cloudy</td>
      <td>$-1.20$</td>
      <td>$-6.06$</td>
      <td>$\mathbf{-7.27}$</td>
    </tr>
    <tr>
      <td>Rainy</td>
      <td>$-1.61$</td>
      <td>$-6.56$</td>
      <td>$-8.17$</td>
    </tr>
  </tbody>
</table>

<p>For step 2, we compute three maxes (one per destination state), each giving us 3 new scores $\delta_2(s)$. Alongside each score we store a <strong>backpointer</strong> $\psi_2(s)$ that remembers which previous state achieved that max:</p>

\[\psi_2(s) \;=\; \arg\max_{s'} \Big[ \delta_1(s') + \log A_{s' \to s} \Big]\]

<div style="width: min(95vw, 1400px); position: relative; left: 50%; transform: translateX(-50%);">
$$
\begin{aligned}
\delta_2(\text{Sunny})  &amp;= \max\!\Big[\, \delta_1(\text{Sunny}) + \log A_{S \to S},\; \delta_1(\text{Cloudy}) + \log A_{C \to S},\; \delta_1(\text{Rainy}) + \log A_{R \to S} \,\Big] + \log f(o_2 \mid \text{Sunny}) \\[4pt]
\delta_2(\text{Cloudy}) &amp;= \max\!\Big[\, \delta_1(\text{Sunny}) + \log A_{S \to C},\; \delta_1(\text{Cloudy}) + \log A_{C \to C},\; \delta_1(\text{Rainy}) + \log A_{R \to C} \,\Big] + \log f(o_2 \mid \text{Cloudy}) \\[4pt]
\delta_2(\text{Rainy})  &amp;= \max\!\Big[\, \delta_1(\text{Sunny}) + \log A_{S \to R},\; \delta_1(\text{Cloudy}) + \log A_{C \to R},\; \delta_1(\text{Rainy}) + \log A_{R \to R} \,\Big] + \log f(o_2 \mid \text{Rainy})
\end{aligned}
$$
</div>

<h3 id="working-table--every-candidate-delta_1s--log-a_sto-s">Working table — every candidate $\delta_1(s’) + \log A_{s’\to s}$</h3>

<p>Using the transition matrix:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>A = np.array([
    # Sunny  Cloudy  Rainy
    [0.75,   0.20,   0.05],   # from Sunny
    [0.25,   0.55,   0.20],   # from Cloudy
    [0.05,   0.35,   0.60],   # from Rainy
])
</code></pre></div></div>

<p>For each destination, the <strong>bold</strong> entry wins. The two right-hand columns record the winning score and which previous state achieved it:</p>

<div class="breakout">

  <table>
    <thead>
      <tr>
        <th>destination $s$</th>
        <th>from Sunny</th>
        <th>from Cloudy</th>
        <th>from Rainy</th>
        <th>winning score</th>
        <th>$\psi_2(s)$</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td><strong>Sunny</strong></td>
        <td>$-9.13 + (-0.29) = -9.42$</td>
        <td>$\mathbf{-7.27 + (-1.39) = -8.65}$</td>
        <td>$-8.17 + (-3.00) = -11.16$</td>
        <td>$\mathbf{-8.65}$</td>
        <td><strong>Cloudy</strong></td>
      </tr>
      <tr>
        <td><strong>Cloudy</strong></td>
        <td>$-9.13 + (-1.61) = -10.73$</td>
        <td>$\mathbf{-7.27 + (-0.60) = -7.87}$</td>
        <td>$-8.17 + (-1.05) = -9.22$</td>
        <td>$\mathbf{-7.87}$</td>
        <td><strong>Cloudy</strong></td>
      </tr>
      <tr>
        <td><strong>Rainy</strong></td>
        <td>$-9.13 + (-3.00) = -12.12$</td>
        <td>$-7.27 + (-1.61) = -8.88$</td>
        <td>$\mathbf{-8.17 + (-0.51) = -8.68}$</td>
        <td>$\mathbf{-8.68}$</td>
        <td><strong>Rainy</strong></td>
      </tr>
    </tbody>
  </table>

</div>

<p>So the backpointers are split between Cloudy (if step 2’s state is Sunny or Cloudy) and Rainy (if step 2 is Rainy).</p>

<h3 id="finishing-delta_2s">Finishing $\delta_2(s)$</h3>

<p>To get to $\delta_2(s)$, we need to add the emission term for today’s observation:</p>

\[\delta_2(s) \;=\; \underbrace{\max_{s'}\big[\delta_1(s') + \log A_{s'\to s}\big]}_{\text{winning score above}} \;+\; \underbrace{\log f(o_2 \mid s)}_{\text{how well } s \text{ explains } o_2}\]

<p>So the emission can change which destination is the overall best at this step, but it never changes the backpointers $\psi_2(s)$.</p>

<table>
  <thead>
    <tr>
      <th>$s$</th>
      <th>winning score</th>
      <th>$\log f(o_2 \mid s)$</th>
      <th>$\delta_2(s)$</th>
      <th>$\psi_2(s)$</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Sunny</td>
      <td>$-8.65$</td>
      <td>$-6.58$</td>
      <td>$\mathbf{-15.23}$</td>
      <td>Cloudy</td>
    </tr>
    <tr>
      <td>Cloudy</td>
      <td>$-7.87$</td>
      <td>$-7.76$</td>
      <td>$-15.63$</td>
      <td>Cloudy</td>
    </tr>
    <tr>
      <td>Rainy</td>
      <td>$-8.68$</td>
      <td>$-12.61$</td>
      <td>$-21.29$</td>
      <td>Rainy</td>
    </tr>
  </tbody>
</table>

<p>If we were to end the algorithm here, we would see that step = 2 has the highest score for Sunny. We can look back at $\psi_2(s)$ to see that if Sunny was step 2, then Cloudy would be step 1.</p>

<p>However, its very important to note that if we continue moving through the observations we could see a different path emerge as the global best score. In this case, the full sequence can be seen in the plot below, where it turns out the best path starts with the same Cloudy → Sunny as in our tabular example above</p>

<div class="breakout">
  <iframe src="/files/HMM_example/HMM_full_demo.html" width="100%" height="1080px" style="border:none; display:block;"></iframe>
</div>

<p>Rather than flipping between states like the naive model, the HMM through Viterbi’s algorithm is able to smoothly transition between states in a natural way and does very well when compared to the true states.</p>

<h2 id="modern-uses">Modern uses</h2>

<p>So there you go, its always fun to stumble across an algorithm that has been quietly <a href="https://ieeexplore.ieee.org/document/7943748">decoding digital communications</a> and <a href="https://link.springer.com/article/10.1007/s40846-025-00928-5">tracking my sleep cycles</a> and <a href="https://ieeexplore.ieee.org/abstract/document/1657823">more</a>.</p>]]></content><author><name>Ronan Laker</name></author><category term="code" /><category term="data" /><category term="til" /></entry><entry><title type="html">In London you are always within 878m of a pub</title><link href="https://www.ronanlaker.com/london-pub-desert/" rel="alternate" type="text/html" title="In London you are always within 878m of a pub" /><published>2026-04-12T00:00:00+00:00</published><updated>2026-04-12T00:00:00+00:00</updated><id>https://www.ronanlaker.com/london-pub-desert</id><content type="html" xml:base="https://www.ronanlaker.com/london-pub-desert/"><![CDATA[<p>I went to a PyData meetup the other day and heard Ben Guerin tell the story behind his viral website <a href="https://www.ismypubfucked.com/">ismypubfucked.com</a>, which does what it says on the tin (his joke). That got me thinking, at any given time in London, how far away from a pub am I?</p>

<p>Well, to answer that we need to create a fine grid of query points across London and then find the closest pub at each point by comparing distances. With a 5km radius around Buckingham palace I ended up with around 63,000 points to check against all 3631 pubs I could find on OpenStreetMap. That’s way too many distance calculations, if only there was a better way to find the nearest neighbouring pub…</p>

<p>Obviously this is a solved problem. We just need to create a binary tree to represent our points and then traverse the tree in a clever way to find the nearest neighbour. After converting the lat/lon of the pubs into radians we also need to tell the BallTree that we want to calculate the distance with the Haversine formula since we are all living on a big ball.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>from sklearn.neighbours import BallTree

tree  = BallTree(pubs, metric = 'haversine')

nearest_neighbour = tree.query(query_point, k=1)
</code></pre></div></div>

<p>Normally I would leave it at that, but since I just got Claude code I thought I might as well create some cool diagrams to prove how much of a speed improvement we can get by using this data structure. By creating a BallTree we have our 3631 pubs as the leaf nodes of the tree, and 3630 internal nodes that represent circles that enclose all the pubs in the leaves at deeper levels. All the nearest neighbour algorithm has to do is progress down the tree, checking the internal nodes at each level. For an internal node with centre $C$ and radius $R$, the distance:</p>

\[\textrm{distance}(\textrm{query}, C) - R\]

<p>represents the minimum distance to the query point for the pubs within this circle. If this distance is larger than the closest distance found so far, we can ignore this entire branch of the tree!</p>

<p>For our example, we only checked 41 out of 7261 nodes of the tree, which is visualised in the plot below. Interestingly, we actually had to check two completely separate branches of the tree all the way down to the leaf node.</p>

<iframe src="/files/pubs_london/tree.html" width="800px" height="600px" style="border:none; display:block; margin:0 auto;"></iframe>

<p>Now we have a quick way to find the closest pub, we can create our grid within 5 km of the palace and create a map like this. You can clearly see the effects of Hyde park and Regent’s park, but because of the many pubs looking over the Thames this is not visible at all!</p>

<iframe src="/files/pubs_london/pub_desert.html" width="800px" height="600px" style="border:none; display:block; margin:0 auto;"></iframe>

<p>We have our answer! You are always within 878m of a pub in London, with the furthest point from any pubs being the football pitches in Regent’s park.</p>

<blockquote>
  <p>You can find the code in this <a href="https://github.com/rlaker/london_pub_desert">repository</a></p>
</blockquote>]]></content><author><name>Ronan Laker</name></author><category term="visualisation" /></entry><entry><title type="html">First world Python environment problem</title><link href="https://www.ronanlaker.com/new-mac/" rel="alternate" type="text/html" title="First world Python environment problem" /><published>2026-03-17T00:00:00+00:00</published><updated>2026-03-17T00:00:00+00:00</updated><id>https://www.ronanlaker.com/new-mac</id><content type="html" xml:base="https://www.ronanlaker.com/new-mac/"><![CDATA[<p>TD;LR: Migrating an Intel Mac to one with an M chip messes up the Python environments</p>

<p>I haven’t been writing much lately. I’ve convinced myself it’s nothing to do with my own motivation, but because my 2018 Macbook was so painful to use. It geniunely took 10 minutes before the cursor wouldn’t act drunk.</p>

<blockquote>
  <p>Your Macbook is so old that it still has Intel inside</p>
</blockquote>

<p>I decided to upgrade to the M4 Air with 256Gb storage, placing my order on Feb 17th. Obviously I don’t pay attention to Apple’s release cycle, because less than 2 weeks later they announced the M5 Air with a minimum storage of 512Gb and the new M5 chip…ffs</p>

<p>Thankfully, Apple anticipated my annoyance and automatically upgraded me - turns out its quite a common <a href="https://www.reddit.com/r/macbookpro/comments/1rjuhqu/auto_upgraded_to_m5_pro_chip_on_an_existing_order/">thing</a>. It probably makes perfect sense from their side (handling costs of returns, factory production rates etc) but it just gives you the same warm feeling as when Flat Iron gives you a free icecream.</p>

<p>After transferring my data to the new Mac I ran into an interesting problem</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ImportError: 
dlopen<span class="o">(</span>.venv/lib/python3.9/site-packages/rpds/rpds.cpython-39-darwin.so, 0x0002<span class="o">)</span>: 
tried: <span class="s1">'.venv/lib/python3.9/site-packages/rpds/rpds.cpython-39-darwin.so'</span> 
mach-o file, but is an incompatible architecture 
have <span class="s1">'x86_64'</span>, need <span class="s1">'arm64e'</span> or <span class="s1">'arm64e.v1'</span> or <span class="s1">'arm64'</span> or <span class="s1">'arm64'</span>
</code></pre></div></div>

<p>My old environment had x86-compiled Python packages, which don’t work when loaded by a native arm Python interpreter. The fix was simple enough, I ended up having to reinstall uv so that it would build environments with the new arm architecture. I just thought it was a fun issue everyone <a href="https://imperialcollegelondon.github.io/RSEBlog/2023/04/20/python-development-on-m1-macs/">solved</a> years ago.</p>]]></content><author><name>Ronan Laker</name></author><category term="til" /><summary type="html"><![CDATA[and a thinly veiled humble brag]]></summary></entry><entry><title type="html">A year of eating like the French</title><link href="https://www.ronanlaker.com/year-of-french-food/" rel="alternate" type="text/html" title="A year of eating like the French" /><published>2026-01-15T00:00:00+00:00</published><updated>2026-01-15T00:00:00+00:00</updated><id>https://www.ronanlaker.com/year-of-french-food</id><content type="html" xml:base="https://www.ronanlaker.com/year-of-french-food/"><![CDATA[<p>I made French food my culinary focus of last year. Each time I wanted a “project” to cook at the weekend, I made it my mission to recreate French classics I saw on YouTube. While I didn’t quite make it to recreating Pierre Koffman’s pig trotters, which two of my favourite cooking channels did within a week of each other (<a href="https://youtu.be/f9kJNrOqYQE?si=IQygS8W_PQzRfgOH">Fallow</a> and <a href="https://youtu.be/ipqFwCtPOgA?si=CuAdyZ2BdfFmFjpz">Adam Byatt</a>), I am pretty proud of the things I managed to make.</p>

<p>An essential quote for exploring French food</p>

<p><img src="/images/photos/french_food/Between Meals(1).png" alt="" /></p>

<h2 id="mains">Mains</h2>

<h3 id="sautéed-chicken-with-tarragon">Sautéed chicken with Tarragon</h3>

<p>This was the first dish that showed me the power of classical technique and I just love how a whole chicken can be transformed into this elegant final product. After scoring Larousse Gastronomique for £4 in a car boot sale, I followed its terse instructions to sauté chicken “à brun” then add shallots and deglaze with white wine. In the meantime, create a browned chicken stock from the leftover carcass and use this thickened sauce to finish off the sauce and get that shine. It’s such a good dish I created it four times within the year.</p>

<p><img src="/images/photos/french_food/IMG_2987.jpeg" alt="" /></p>

<h3 id="coq-au-vin">Coq au vin</h3>

<p>This <a href="https://youtu.be/3c6b18C5TpY?si=nJNNK31RRDb4B5zV">coq au vin</a> was what finally convinced me there was no such thing as cooking wine. I sacrificed two glasses of a nice Bordeaux as an experiment to marinate some old tough chicken legs. The result had that same zing I tasted in Paris and was much thinner than a classic British/Irish stew I was used to.</p>

<p>While I managed to make an excellent coq au vin, I was not so lucky with its cousin the beef Bourguignon. I was plagued by poor choice of beef cut, a reduced-price rib of beef disintegrated into flakes and another cheap cut from the local butchers was so chalky Dom Pérignon asked for their soil back.</p>

<h3 id="pommes-sardalaise">Pommes Sardalaise</h3>

<p>Being half Irish, I already had a pretty decent grasp on potatoes with mashed potato genuinely being my favourite food. So I was on the look out for other potato dishes to satiate my starchy appetite, and what’s the only way to beat butter? Duck fat.</p>

<p>Fair warning, this is an insane amount of duck fat so be prepared for the consequences the next day 💨.</p>

<h3 id="pan-bagnat">Pan Bagnat</h3>

<p>It’s all been a bit high brow up till now, so I’ll bring it down a level. One of my favourite guilty pleasure lunches is the Gregg’s tuna baguette. I can only assume they were inspired by this French picnic classic. This <a href="https://youtu.be/8GxzGND1NnA?si=YtAN6wOcOTWAZsM9">video</a> is the over-the-top version of this dish but has a really infectious energy.</p>

<p><img src="/images/photos/french_food/French_food - 1 (1).jpeg" alt="" /></p>

<h3 id="blanquette-de-poulet-couldnt-find-veau">Blanquette de poulet (couldn’t find veau)</h3>

<p>Another dish that we tried in Paris and reminded me of one of my favourite dishes growing up, Mum’s creamy chicken with rice. Instead of cream, this <a href="https://youtu.be/xn1BTnRmEb8?si=P9PFqdlqYY40iqy1">recipe</a> is thickened with a roux to make a velouté. Using the stock from poaching the chicken and vegetables is such a genius technique and gives the sauce an amazing flavour. Its also mind blowing how much each element retains its own flavour, making every mouthful different.</p>

<p><img src="/images/photos/french_food/French_food - 2.jpeg" alt="" /></p>

<h3 id="french-onion-soup">French Onion soup</h3>

<p>More of a recipe to prove I’ve done it than something I would make regularly. Needs even more cheese than you think. Slices worked much better than grating it on.</p>

<p><img src="/images/photos/french_food/French_food - 42.jpeg" alt="" /></p>

<h3 id="consommé">Consommé</h3>

<p>The <a href="https://youtu.be/YBZXdQwQEfA?si=Pt-mRnxXquFD3NfH">technique</a> was like magic. I think I got the ratios of vegetables slightly off so the taste was a bit too carroty but it was still a cool thing to create.</p>

<p><img src="/images/photos/french_food/French_food - 32.jpeg" alt="" /></p>

<h3 id="onion-tart-tatin">Onion tart tatin</h3>

<p>We definitely managed to capture the taste but our onions fell apart a bit compared to immaculate YouTube videos, but I am not willing to spend over £10 on Roscoff onions.</p>

<p><img src="/images/photos/french_food/Onion_tarte_tatin - 1.jpeg" alt="" /></p>

<p><img src="/images/photos/french_food/Onion_tarte_tatin - 2.jpeg" alt="" /></p>

<p><img src="/images/photos/french_food/Onion_tarte_tatin - 3.jpeg" alt="" /></p>

<h3 id="ratatouille">Ratatouille</h3>

<p>I tried the fancy movie version of this dish a few years ago and it all just turned into mush… So I was happier with this fine dining <a href="https://youtube.com/shorts/ctL5ZZN1KYA?si=mrx-RhqXuVjdCZ2O">version</a>, but I still think the dicing is slightly overkill.</p>

<p><img src="/images/photos/french_food/image 6.jpg" alt="" /></p>

<h2 id="dessert">Dessert</h2>

<p>I never thought I would be that guy, but a stand mixer purchase really improved my pâtisserie game towards the end of the year. Yes it did cost several hundreds of pounds, but I can now spend all day creating Lemon meringue tarts instead of spending £5.50 per tart at <a href="https://www.lechoux.com/products/copy-of-lemon-meringue-tart">Choux</a> so who’s laughing now… In my defense, it is the most fun I’ve ever had making a dessert.</p>

<h3 id="lemon-meringue-tart">Lemon meringue tart</h3>

<p><img src="/images/photos/french_food/French_food - 64.jpeg" alt="" /></p>

<p><img src="/images/photos/french_food/French_food - 66.jpeg" alt="" /></p>

<p>Just look how cute these are! This dessert also unlocks lemon curd, sweet pastry and Italian meringue from the bucket list.</p>

<h3 id="madeleines">Madeleines</h3>

<p>I bought the madeleine tray in Paris so I kind of had to learn how to make these. My humps can’t rival <a href="https://youtu.be/GIwkqVp54Nk?si=oKDXUDYpWasapuce">Aya’s</a>, but at least I could tut at The Great British Bake Off for giving the contestants silicon madeleine moulds in the final. Don’t they know its the contrast of hot oven and cold tray that creates the bump?!</p>

<p><img src="/images/photos/french_food/French_food - 36.jpeg" alt="" /></p>

<p><img src="/images/photos/french_food/French_food - 35.jpeg" alt="" /></p>

<h3 id="profiteroles">Profiteroles</h3>

<p>Éclairs are another one of my favourite desserts that costs £5.50 a pop at <a href="https://www.lechoux.com/products/chocolate-cream-eclairs">Le Choux</a>, which obviously justifies making too many to eat myself. I went for profiteroles instead after trying them at Paulette (<a href="#paulette">below</a>). Much like doing a physics degree is now only useful for answering University Challenge questions, I probably won’t make these more than once a year but at least I know what crème diplomat is.</p>

<p>Start with making the pastry cream, used this <a href="https://youtu.be/Ud_VZOHv6mY?si=zD7eUsWZinc3mqqW">video</a> for the choux pastry and crème diplomat but swapped to this <a href="https://youtu.be/659K-yU9B9M?si=dmuVnpfBEHx4zbhX">video</a> for the chocolate topping.</p>

<p>Somehow I forgot to take a picture!</p>

<h3 id="crème-brûlée">Crème Brûlée</h3>

<p>Actually much easier than it seems, I already had a blow torch in my endless quest to perfect egg fried rice. Be careful not to burn the sugar as it gets very bitter.</p>

<p><img src="/images/photos/french_food/French_food - 13.jpeg" alt="" /></p>

<h3 id="chocolate-mousse">Chocolate Mousse</h3>

<p>We tried so many recipes but we couldn’t get it quite right before the sight of mousse became sickening. I think the “French classics” (<a href="#sources">below</a>) version is probably the best, but I just hate the idea of using an uneven number of egg whites and yolks.</p>

<p><img src="/images/photos/french_food/French_food - 1.jpeg" alt="" /></p>

<p><img src="/images/photos/french_food/French_food - 31.jpeg" alt="" /></p>

<h2 id="restaurants">Restaurants</h2>

<h3 id="bouillon-chartier">Bouillon Chartier</h3>

<p>Bouillon Chartier was just so perfectly French. A terse conversation with the waiter yielded all the classics we could fit into one sitting. I’m sure you can tell from the scribbles on our table, but we ordered 1/2 a Bordeaux, 12 escargot, pork terrine, leeks vinaigrette, beef Bourguignon, boudin noir and even tête de veau. Judging from their wandering eyes and tutting heads, this was far too much food according to the elderly French couple sharing our table. Thankfully the quality of the food meant I didn’t need any more encouragment from our geriatric neighbours to clean the plates.</p>

<p><img src="/images/photos/french_food/Chartier%20-%205.jpeg" alt="" /></p>

<p>The Bourguignon had huge chunks of tender beef and serving it with Macaroni rather than mashed potato was something new to me.</p>

<p><img src="/images/photos/french_food/Chartier%20-%203.jpeg" alt="" /></p>

<p>Thankfully, I still got my mash fix with the boudin noir, essentially a black pudding sausage. The taste was excellent, but the lack of oats in the sausage meant its texture was too similar to the mash.</p>

<p><img src="/images/photos/french_food/Chartier%20-%201.jpeg" alt="" /></p>

<p>While the picture doesn’t make the tête de veau look particularly appetising, the meat was exceptionally tender and the fat/gelatin had an amazingly chewy texture reminiscent of a beef tendon we had in another fancier restaurant.</p>

<p><img src="/images/photos/french_food/Chartier%20-%202.jpeg" alt="" /></p>

<p>We absolutely loved this place, it was just so grand yet down to earth at the same time. Like the horrible tourists we were, we ended up buying 4 wine glasses and two side plates with the Bouillon Chartier logo on (I’m sure you’ve noticed from some of the other pictures in this page).</p>

<h3 id="brasserie-zédel">Brasserie Zédel</h3>

<p>Still can’t believe this place exists in London. The online menu makes it look like any other French bistro with inexplicably higher prices, but once you step into the dining room it all makes sense. The food was so good I didn’t even mind the live Jazz.</p>

<p>This special of Asparagus with Bearnaise and a poached duck egg was just perfectly creamy and fresh. It was gone before we knew it.</p>

<p><img src="/images/photos/french_food/French_food - 18.jpeg" alt="" /></p>

<p>I had the Choucroute Zédel, the biggest of three choucroute options, as I wanted to try as many assorted meats as possible. It turns out to be a mountain of food which would normally be impossible to get through without the choucroute (sauerkraut) providing a fresh, tangy offset to the meat. My favourite was the smoked ham hock, which was reminiscent of those thick cut BBQ ribs. My only criticism is that the potatoes were surplus to requirement and were just a bit bland.</p>

<p><img src="/images/photos/french_food/French_food - 19.jpeg" alt="" /></p>

<p>We also had the dressed crab, which was easily the best I’ve ever had.</p>

<p><img src="/images/photos/french_food/French_food - 20.jpeg" alt="" /></p>

<h3 id="paulette">Paulette</h3>

<p>We went to this small French bistro for our 1st wedding anniversary after hearing about through work colleagues. If it wasn’t such a special event I would have paid more attention to the prices, but leaving that aside the food was really very good indeed.</p>

<p>For starters we tried three classics.</p>

<p>We wanted to see a real onion tarte tatin in the wild after trying our own version (<a href="#onion-tart-tatin">above</a>). This had a great taste but was served cold and therefore became a bit soggy. Obviously, we ordered Escargot Bourgogne but these were very different to normal garlic-y ones. Much more herby taste which wasn’t as good as normal in my opinion. The real star was the moules à la crème au piment d’Espelette - the best mussel sauce I’ve ever had. It tasted great, the little hint of heat in the background and strong mussel flavour. The thickness, as well as taste, of the sauce practically forced you to order another round of bread in spite of the £6 fee.</p>

<p><img src="/images/photos/french_food/French_food - 44.jpeg" alt="" /></p>

<p>Confit duck leg with tarbais beans (the ones used in Cassoulet) was also very good. Simple but perfectly balanced, again a background spice to keep it lingering between mouthfuls and herby punch on top. Went really well with the garlic creamy spinach.</p>

<p>Just look at it. Beautiful.</p>

<p><img src="/images/photos/french_food/French_food - 48.jpeg" alt="" /></p>

<p>I’ve tried making beurre blanc at home but I keep thinking “it can’t possibly be that much butter”. I think I was proved wrong with this Cod in buerre blanc sauce. The sauce was really mild and smooth, mainly tasted the chives with just a subtle butter in the background. Again, a beautiful presentation that’s not out of reach at home.</p>

<p><img src="/images/photos/french_food/French_food - 47.jpeg" alt="" /></p>

<p>The Tart Bordaloue was a pleasant surprise, the chocolate layer must have used the exact same chocolate as a Pain au Chocolat.</p>

<p><img src="/images/photos/french_food/French_food - 50 1.jpeg" alt="" /></p>

<p>The finale was profiteroles. The chocolate sauce was so fun, but the ice cream was actually a bit too cold. I would have preferred whipped cream or crème diplomat (<a href="#profiteroles">above</a>).</p>

<p><img src="/images/photos/french_food/French_food - 51.jpeg" alt="" /></p>

<h2 id="summary">Summary</h2>

<p>So what have I gained from eating like a Frenchman for a year? Well, from a technique perspective I think I’ve come a long way. The other day I made the <a href="#blanquette-de-poulet-couldnt-find-veau">blanquette the poulet</a> almost a year after the first attempt. It tasted so much fuller than the first try, probably because of little things like salting in each step and not letting it boil too ferociously. Another example of the learning the “little things” is with the <a href="#sautéed-chicken-with-tarragon">sautéed chicken and tarragon</a>. Having made it so many times I can now instinctively judge when the shallots have been sweated down enough, and I know how to evaporate the wine until the reduction looks a bit jammy. These skills are applicable in so much other cooking, like a risotto or gravy, which is why I chose French as the cuisine to focus on first. The downside is that I do now use double the amount of pans for the same recipe, which is a real killer when we don’t have a dishwasher.</p>

<p>I have also learnt that you can find much better recipes when you search in the native language, something I plan on continuing when I take on Italian cuisine in the coming year. I already have a much better foundation in Italian than I did in French, but I am determined to learn how to make good fresh pasta. Although, the only Italian dessert I really enjoy is Tiramisu, so I think I will continue with French patisserie and even take on the dreaded Macaron.</p>

<p>So I’ll bid French cuisine <em>adieu</em> for now and start practicing my 🤌.</p>

<h2 id="sources">Sources</h2>

<h3 id="youtube">Youtube</h3>

<ul>
  <li><a href="https://www.youtube.com/@PastryLivingwithAya">Pastry living with Aya</a> for all things baking.</li>
  <li><a href="https://www.youtube.com/@ChefAdamByatt">Adam Byatt</a> makes things look effortless</li>
  <li><a href="https://www.youtube.com/@MatthewJRyle/shorts">Matthew Ryle</a> is great for inspiration but only does shorts. See his book “French Classics” below</li>
  <li><a href="https://www.youtube.com/@FallowChefs">Fallow</a> are great when they cover French food</li>
</ul>

<h3 id="books">Books</h3>

<ul>
  <li><a href="https://www.matthewryle.com/book-french-classics-matthew-ryle">French Classics</a> looks great and tastes great</li>
  <li><a href="https://en.wikipedia.org/wiki/Mastering_the_Art_of_French_Cooking">Mastering the Art of French Cooking</a> is fun to look through but not massively useful to cook from</li>
  <li><a href="https://en.wikipedia.org/wiki/Larousse_Gastronomique">Larouse Gastronomique</a> is really fun to look through for inspiration but very few instructions. I managed to find my copy for £4 in a car boot sale when its worth £60 new</li>
  <li><a href="https://www.penguin.co.uk/books/456094/between-meals-by-liebling-a-j/9780241637975">Between Meals</a> and <a href="https://www.penguinrandomhouse.com/books/157092/the-food-of-france-by-waverley-root/">The Food of France</a> are both guides to French food written in the early 20th century and still make for fascinating reading</li>
</ul>]]></content><author><name>Ronan Laker</name></author><category term="food" /><category term="meta" /></entry><entry><title type="html">Logs to the rescue</title><link href="https://www.ronanlaker.com/logging/" rel="alternate" type="text/html" title="Logs to the rescue" /><published>2025-11-17T00:00:00+00:00</published><updated>2025-11-17T00:00:00+00:00</updated><id>https://www.ronanlaker.com/logging</id><content type="html" xml:base="https://www.ronanlaker.com/logging/"><![CDATA[<p>I’ve always been partial to a few print statements when debugging my personal coding projects. Now, having worked on code behind an API, I have come to appreciate the benefit of switching <code class="language-plaintext highlighter-rouge">print()</code> for <code class="language-plaintext highlighter-rouge">logger.info</code>. It only takes a few lines to set the root logger configuration, which adds useful metadata to every debugging message:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>import logging
import jsonlogger

logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)

# StreamHandler to stdout
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.DEBUG)

# Define a simple message format
log_format = "%(asctime)s [%(module)s &gt; %(funcName)] : %(message)s"
formatter = jsonlogger.JsonFormatter(log_format)
handler.setFormatter(formatter)

logger.addHandler(handler)
</code></pre></div></div>

<blockquote>
  <p>Of course, there is an excellent tutorial at <a href="https://calmcode.io/course/logging/introduction">calmcode.io</a></p>
</blockquote>

<p>This has become very handy when I recently took over a machine learning pipeline that was churning out thousands of logs, with Pytorch printing every update in a progress bar and cmdstanpy insisting on telling me about its chains.</p>

<p>In this same logging configuration file, we can turn these annoying logs off. There could be a more consistent way to do this, but this is what worked for me.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>import cmdstanpy
cmdstanpy.disable_logging()

noisy_libs = ["neuralforecast", "pandas", "snowflake.connector"]
for lib in noise_libs:
    logging.getLogger(lib).setLevel(logging.WARNING)
</code></pre></div></div>

<p>Finally, when looking at the logs in DataDog I could see that any logs not generated by me, but by other packages like pandas, were mising the lovely context data (i.e. environment, market, model). We can fix this by adding a filter to the base logger, which then adds our context to all logs in the stdout output.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>class ContextFilter(logging.Filter):
    def __init__(self, context):
        super().__init__()
        self.context = context
    
    def filter(self, record):
        for key, value in self.context.items():
            setattr(record, key, value)
        return True

root_logger = logging.getLogger()
logHandler = logging.StreamHandler(stream = sys.stdout)

logHandler.addFilter(ContextFilter(context))
</code></pre></div></div>]]></content><author><name>Ronan Laker</name></author><category term="code" /></entry><entry><title type="html">What the hell is happening on the internet?</title><link href="https://www.ronanlaker.com/critical-thinking-for-social-media/" rel="alternate" type="text/html" title="What the hell is happening on the internet?" /><published>2025-09-02T00:00:00+00:00</published><updated>2025-09-02T00:00:00+00:00</updated><id>https://www.ronanlaker.com/critical-thinking-for-social-media</id><content type="html" xml:base="https://www.ronanlaker.com/critical-thinking-for-social-media/"><![CDATA[<p>As this viral <a href="https://x.com/molochofficial/status/1858940244567146823?s=12&amp;t=GArJOEJ41SKT7sLfzFsugQ&amp;utm_source=www.garbageday.email&amp;utm_medium=referral&amp;utm_campaign=the-internet-s-most-vocal-freaks">tweet</a> posits, without an apex media predator in Ellen DeGeneres, internet characters are sticking around long enough to become media figures in their own right. Being on the cusp of Milliennials and Gen Z, I have stubbonly refused to install TikTok which means I have no idea who the hell characters like “Beavo” or the “Schooner scorer” are.</p>

<p>Luckily, like anything on the internet, <strong>there is a podcast for that</strong>.</p>

<p><a href="https://youtube.com/@thescreenrotpodcast?si=EMNs-g2NxWkEmy4F">The screen rot podcast</a> is a sort of media literacy for the social media age. While anyone could poke fun at some internet figure for an hour, these guys are amazing at finding that one that killer insight. When analysing the enigma of Beavo, they presented a clip of Beavo saying “I will do anything” for views. In the process of throwing “trends” on the wall and seeing what sticks, we see how Beavo started eating food on camera and accidently became famous because it turns out he doesn’t chew food before swallowing it…</p>

<p>My favourite “internet” newsletter, <a href="https://www.garbageday.email/c/about">Garbage Day</a>, had a similar take on Mr Beast:</p>

<blockquote>
  <p>“His success is less of a reflection of his own entertainment knowhow and more a reflection of what YouTube’s algorithm looks like when stripped raw.”</p>
</blockquote>

<p>I love this newsletter because it traces how memes and stories make it into the mainstream, which is increasingly important since memes can now directly influence White House policies. As Garbage day <a href="https://www.garbageday.email/p/the-uncanny-valley-between-meme-and-law-9650f12b480c7005">explains</a>, “DOGE was cutting federal funding in <a href="https://www.chalkbeat.org/2025/02/20/chris-rufo-social-media-posts-driving-education-department-cuts/">direct response</a> to posts from right-wing activist Chris Rufo […] Even the name, ‘Department of Government Efficiency,’ <a href="https://x.com/elonmusk/status/1825713824785379477">came from</a> an Elon Musk reply guy going by ‘Sir Doge Of The Coin.’”</p>

<p>With this kind of stuff happening in the world, it’s probably better off reading this newsletter than paying for the Economist.</p>

<p>A more traditionally “serious” source of analysis is the excellent <a href="https://www.youtube.com/playlist?list=PLDTPrMoGHssAfgMMS3L5LpLNFMNp1U_Nq">If you’re listening</a> show on YouTube. This handles larger geopolitical issues, and does a really good job of painting the overall picture in an entertaining format. They also also picked up on the idea that politics is now being influence by memes and tweets. They traced how an AI video of a resort in Gaza was designed specifically to <a href="https://youtu.be/DdUGdSEXkQU?si=rEedSkyZ-5aDeLKP">influence Trump’s policy</a> in the Middle East.</p>

<p>Maybe all this analysis is futile, but at least it’s entertaining.</p>]]></content><author><name>Ronan Laker</name></author><category term="essay" /></entry><entry><title type="html">Learning languages in 2025</title><link href="https://www.ronanlaker.com/learning-languages/" rel="alternate" type="text/html" title="Learning languages in 2025" /><published>2025-07-14T00:00:00+00:00</published><updated>2025-07-14T00:00:00+00:00</updated><id>https://www.ronanlaker.com/learning-languages</id><content type="html" xml:base="https://www.ronanlaker.com/learning-languages/"><![CDATA[<p>Despite being the proud owner of an A* in GCSE French, <em>je parle pas un mot</em>. Since I have joined a predominately French company I’ve decided to try learning it again, even if I just to understand the back of a wine bottle.</p>

<p>This aim has coincided with a requirement at work to 10x the speed of our Python API, which will probably require rewriting parts in another programming language.</p>

<p>Given its been &gt;10 years since I attempted to learn any language, programming or natural, are there better ways to teach myself in 2025?</p>

<h1 id="french">French</h1>

<h2 id="duolingo">Duolingo</h2>

<p>Duolingo isn’t the greatest app in the world, but my god is it good at peer pressure. Having to do at least <em>some</em> French every day is a good way to keep up my motivation and build vocabulary.</p>

<p>Luckily for me, <a href="https://youtube.com/watch?v=TUL29y0vJ8Q&amp;si=EXkpFPl6jOWpNNbz">30% of English vocabulary is based on French</a> so I can bluff my way past the easy levels. However, this app isn’t very helpful for learning about grammar rules or how French people actually speak.</p>

<h2 id="youtube">Youtube</h2>

<p>For that, Youtube is an absolute gold mine. I can watch:</p>
<ul>
  <li><a href="https://youtu.be/SApp5pEtCB4?si=OPeOpvtQjZT2-SZF">grammar explanations</a></li>
  <li><a href="https://youtu.be/YCuU4SjcS2A?si=QDmyp-w6hplCKcmI">people being interviewed in the street</a></li>
  <li><a href="https://youtu.be/q3gxfqXTj_E?si=eha6TPlH925axXpX">Jokes about the language</a></li>
</ul>

<p>Youtube’s auto-translation is so good that it opens up the chance to watch <em>unbelievably</em> French videos like:</p>
<ul>
  <li><a href="https://youtu.be/DW5DXfCF3hE?si=w4mHtGtyOMzoiuGE">A man in a beret who kills his rooster to make coq au vin</a></li>
  <li><a href="https://youtu.be/hnlijylU8Q4?si=8N-qzYtVEHK0pDx-">A different man in a beret showing me how Mont D’or is made</a></li>
  <li><a href="https://www.youtube.com/watch?v=urIwhx3uHug">a dude wearing a questionable number of knives</a></li>
</ul>

<h1 id="rust">Rust</h1>

<p>I’ve been using Python for 10+ years and have learnt a lot of general programming concepts through it, e.g. Object oriented, iterators, threading. However, I think I need to learn a low-level language that I can turn to when I need performance.</p>

<p>For example, I was working on some code that merged two dataframes and then exploded the columns to something like 5 millions rows. When written in pure Python it would take 4.4 seconds compared to a ridiculous 0.15s in <a href="https://www.rust-lang.org/">Rust</a>. It has become a bit of a meme to “re-write this in Rust”, so it was definitely on my radar but I couldn’t tell why it was so different.</p>

<h2 id="the-rust-book">The Rust Book</h2>

<p><a href="https://rust-book.cs.brown.edu/ch00-00-introduction.html">The rust book</a> is an incredible resource. Not only does it show you the syntax, but it has diagrams like this to explain concepts like <a href="https://rust-book.cs.brown.edu/ch04-02-references-and-borrowing.html">ownership and borrowing</a></p>

<p><img src="/images/the_rust_book.png" alt="" /></p>

<p>So, I’m not only learning about one new language but I’m being guided through concepts like heap and stack memory, pointers and everything else Python abstracts away.</p>

<p>I will probably still use Python for 90% of what I do, but its nice to be able to see how another language handles common pain points in Python like error messages and packaging.</p>

<h2 id="rustlings">Rustlings</h2>

<p><a href="https://rustlings.rust-lang.org/">Rustlings</a> is the perfect partner to the Rust book, as it gives you 94 example scripts with compiler errors that you need to fix. This also demonstrates just how incredible the compiler is at helping you fix mistakes.</p>

<h2 id="youtube-1">Youtube</h2>

<p>There is a lot of nonsense about “which programming language is the best”, but I found this gem of a channel that explains why Rust is great</p>

<p><img src="https://youtu.be/2hXNd6x9sZs?si=qhvfe5Pw_0HSSNkN" alt="" /></p>]]></content><author><name>Ronan Laker</name></author><category term="code" /></entry></feed>