<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Metrics · Jared Lynskey</title>
    <link>https://jared.lynskey.co.nz/en/tags/metrics/</link>
    <description>Short stories of value on software engineering and engineering leadership — performance reviews, hiring and scaling teams, CI/CD, DevOps monitoring and agentic AI. Written from Seoul.</description>
    <generator>Hugo</generator>
    <language>en</language>
    <copyright>© 2026 Jared Lynskey</copyright>
    <lastBuildDate>Wed, 28 Oct 2026 00:00:00 +1300</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/en/tags/metrics/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Content Ranking Algorithms: Signals Beyond Views and Likes</title>
      <link>https://jared.lynskey.co.nz/en/posts/2023/2023-10-06-reinenforcement-learning/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/en/posts/2023/2023-10-06-reinenforcement-learning/</guid>
      <pubDate>Fri, 06 Oct 2023 00:30:03 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>content ranking</category>
      <category>metrics</category>
      <category>SEO</category>
      <category>machine learning</category>
      <category>user engagement</category>
      <category>content strategy</category>
      <description>What a content ranking algorithm does, why ranking on views and likes alone makes the popular more popular, and the quieter signals worth blending into one score.</description>
      <content:encoded><![CDATA[<p>A content ranking algorithm decides the order a feed, a search page or a &ldquo;top posts&rdquo; list shows things in. It gives every item a score built from several signals (how people engage with it, what the content itself is like, how fresh and relevant it is) and sorts by that score. Rank on views and likes alone, though, and you get a rich-get-richer loop: whatever sits at the top gets seen, so it collects more views and likes, so it stays at the top.</p>
<p>That&rsquo;s the obvious flaw in views and upvotes: they reward whatever&rsquo;s already popular. Fresh content starves at the bottom of the feed, and the platform slowly turns into a rerun channel. Ranking a feed well is one of the quietly hard problems where I work, so I&rsquo;ve spent a fair bit of time thinking about which other signals are worth trusting.</p>

<h3 class="relative group">Watch what people do, not what they tap
    <div id="watch-what-people-do-not-what-they-tap" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#watch-what-people-do-not-what-they-tap" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Explicit feedback is scarce and biased; behaviour is plentiful. Time on page and scroll depth tell you whether someone actually read the thing or bailed after the first paragraph. Revisits are even better — coming back to a piece is a stronger endorsement than any upvote. And a fast exit is a useful negative signal: the content promised something the reader didn&rsquo;t get.</p>

<h3 class="relative group">Signals baked into the content itself
    <div id="signals-baked-into-the-content-itself" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#signals-baked-into-the-content-itself" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Good content doesn&rsquo;t always collect early likes, but it usually shows its quality in other ways. Novelty is one — does the piece cover ground the platform hasn&rsquo;t seen? Structure is another; clean formatting, decent grammar and clarity correlate with quality more than we&rsquo;d like to admit. An author&rsquo;s track record is a fair prior too, judged on how their past work held readers rather than on raw like counts. Early comments help as well: three genuinely positive comments beat thirty reflexive upvotes, and a bit of semantic analysis can flag content that fills a gap the platform has been missing.</p>

<h3 class="relative group">Borrow signals from outside
    <div id="borrow-signals-from-outside" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#borrow-signals-from-outside" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>The wider internet tells you what&rsquo;s resonating right now. Content that lines up with trending search keywords has built-in relevance, and quick publishing on a rising topic deserves a boost. What&rsquo;s buzzing on Twitter or Reddit is often a preview of what your own users will want next — with a little prediction you can promote matching fresh content before the wave instead of after it. Your own search box is underrated too: if users keep searching for a topic, or a piece pulls steady traffic in from search engines, that&rsquo;s relevance you didn&rsquo;t have to guess at.</p>

<h3 class="relative group">Blending the signals into one score
    <div id="blending-the-signals-into-one-score" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#blending-the-signals-into-one-score" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Here&rsquo;s the idea in a few lines of Python. It&rsquo;s an illustration, not a production system. Each signal is normalised to a 0 to 1 range so that one with big raw numbers can&rsquo;t drown the rest, a weighted sum turns them into a single score, and a time decay gives a new post a fair shot against last month&rsquo;s hit.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">WEIGHTS</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;read_depth&#34;</span><span class="p">:</span> <span class="mf">0.3</span><span class="p">,</span> <span class="s2">&#34;revisits&#34;</span><span class="p">:</span> <span class="mf">0.25</span><span class="p">,</span> <span class="s2">&#34;quality&#34;</span><span class="p">:</span> <span class="mf">0.25</span><span class="p">,</span> <span class="s2">&#34;early_comments&#34;</span><span class="p">:</span> <span class="mf">0.2</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="n">HALF_LIFE_HOURS</span> <span class="o">=</span> <span class="mi">48</span>  <span class="c1"># a post loses half its score every two days</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">normalise</span><span class="p">(</span><span class="n">values</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">lo</span><span class="p">,</span> <span class="n">hi</span> <span class="o">=</span> <span class="nb">min</span><span class="p">(</span><span class="n">values</span><span class="p">),</span> <span class="nb">max</span><span class="p">(</span><span class="n">values</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">[(</span><span class="n">v</span> <span class="o">-</span> <span class="n">lo</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">hi</span> <span class="o">-</span> <span class="n">lo</span><span class="p">)</span> <span class="k">if</span> <span class="n">hi</span> <span class="o">&gt;</span> <span class="n">lo</span> <span class="k">else</span> <span class="mf">0.0</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">rank</span><span class="p">(</span><span class="n">items</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;items: dicts holding raw signal values plus &#34;age_hours&#34;.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">norm</span> <span class="o">=</span> <span class="p">{</span><span class="n">name</span><span class="p">:</span> <span class="n">normalise</span><span class="p">([</span><span class="n">item</span><span class="p">[</span><span class="n">name</span><span class="p">]</span> <span class="k">for</span> <span class="n">item</span> <span class="ow">in</span> <span class="n">items</span><span class="p">])</span> <span class="k">for</span> <span class="n">name</span> <span class="ow">in</span> <span class="n">WEIGHTS</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">scores</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">item</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">items</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">base</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="n">weight</span> <span class="o">*</span> <span class="n">norm</span><span class="p">[</span><span class="n">name</span><span class="p">][</span><span class="n">i</span><span class="p">]</span> <span class="k">for</span> <span class="n">name</span><span class="p">,</span> <span class="n">weight</span> <span class="ow">in</span> <span class="n">WEIGHTS</span><span class="o">.</span><span class="n">items</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="n">scores</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">base</span> <span class="o">*</span> <span class="mf">0.5</span> <span class="o">**</span> <span class="p">(</span><span class="n">item</span><span class="p">[</span><span class="s2">&#34;age_hours&#34;</span><span class="p">]</span> <span class="o">/</span> <span class="n">HALF_LIFE_HOURS</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">order</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">items</span><span class="p">)),</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">i</span><span class="p">:</span> <span class="n">scores</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">reverse</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">[</span><span class="n">items</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">order</span><span class="p">]</span></span></span></code></pre></div></div>
<p>The weights are guesses until the tests below say otherwise. Views and likes can still go in; they just become one input among several instead of the whole score. The half-life decides how quickly old posts sink, which is a topic of its own: I went into it in <a href="https://jared.lynskey.co.nz/en/posts/2023/2023-09-27-simulating-views/" >how feed ranking algorithms decide when to bury old posts</a>. Real systems usually learn the weights with a model rather than picking them by hand, but the shape is the same: many signals in, one number out, sort.</p>

<h3 class="relative group">Then test it honestly
    <div id="then-test-it-honestly" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#then-test-it-honestly" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>However clever the signals, A/B test them — show new content to a slice of users and compare its engagement against the incumbents. And watch how things spread: shares, mentions and embeds, on and off the platform, say more about value than a view counter ever will.</p>
<p>No single metric survives contact with reality. The rankings that actually work blend behaviour, intrinsic quality, timeliness and honest experiments, and no one signal gets the final word.</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2023/2023-10-06-reinenforcement-learning/featured.jpeg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
