<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"
     xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
  <title>Pick Heart — Build Notes</title>
  <link>https://blog.pickheart.com/</link>
  <atom:link href="https://blog.pickheart.com/feed.xml" rel="self" type="application/rss+xml"/>
  <description>Notes from building games and web apps: what I measured, what surprised me, and what I got wrong.</description>
  <language>en</language>
  <lastBuildDate>Sun, 16 Aug 2026 00:00:00 +0000</lastBuildDate>
  <item>
    <title>Write it down</title>
    <link>https://blog.pickheart.com/posts/write-it-down/</link>
    <guid isPermaLink="true">https://blog.pickheart.com/posts/write-it-down/</guid>
    <pubDate>Sun, 16 Aug 2026 00:00:00 +0000</pubDate>
    <description>Memory is shot. Time to write things down so I have something to reflect on.</description>
    <content:encoded><![CDATA[<h2 id="welcome">Welcome! こんにちは❕</h2>
<p>Thank you for stopping by :clap: 
It has been about two months, to the day, when I started this new venture of mine.  Putting so many hours of my time
towards something that may ultimately go nowhere felt pointless.  It was important for me going into this that I had
a solid plan around what exactly I hoped to get out of all of this.  I wrote it down early and I am sharing it now:</p>
<table>
<thead>
<tr>
<th>Experience</th>
<th>Community</th>
<th>Opportunity</th>
</tr>
</thead>
<tbody>
<tr>
<td>Experience building actual software and products</td>
<td>Connecting with other games, developers, and people</td>
<td>Opportunities to do more of what I truly love</td>
</tr>
</tbody>
</table>
<h2 id="what-is-this-venture">What is this venture?</h2>
<p>Originally I thought I would use this as a platform to focus all of my energy thinking about how I would do &ldquo;xyz&rdquo; differently
&ldquo;if only it were my game.&rdquo;  Flash forward about 5 years of head in the books grinding in Python and Visual Analytics for 50-60
hours a week before I finally felt confident enough in myself to start this venture.</p>
<h2 id="who-am-i">Who am I?</h2>
<p>I am Eli.  A Husband and a Father, I put a lot of pressure on myself to ensure I am doing everything I can for those I love.
Behind all of that is a nerdy obsession with games, data, and new things.  </p>
<h2 id="why-now">Why now?</h2>
<p>Like, just about, everyone I have seen how AI has shifted the dynamics of development.  Even in the few years I have 
been working in analytics I have seen the shift.  This clip from Star Trek really sums up my feelings around AI as a whole.</p>
<div class="video">
  <iframe src="https://www.youtube-nocookie.com/embed/qi1Zuni57Rc?start=215&amp;end=222"
          title="The bit at the end"
          loading="lazy"
          referrerpolicy="strict-origin-when-cross-origin"
          allow="accelerometer; clipboard-write; encrypted-media; picture-in-picture"
          allowfullscreen></iframe>
</div>

<p class="video-caption">
<a href="https://www.youtube.com/watch?v=qi1Zuni57Rc">Watch</a>.</p>

<p>To that end, AI has helped me immeasurably when I think of the number of times I would have simply given up on a task without
the right &ldquo;next step&rdquo;.  This is that next step as I finally channel all of the things I have learned from bootcamps, self-dev,
work, and curiosity.  Can any of that be turned into something purposeful? I would like to find out.</p>]]></content:encoded>
  </item>
  <item>
    <title>Sentiment scores wording, not subject</title>
    <link>https://blog.pickheart.com/posts/sentiment-scores-wording-not-subject/</link>
    <guid isPermaLink="true">https://blog.pickheart.com/posts/sentiment-scores-wording-not-subject/</guid>
    <pubDate>Thu, 13 Aug 2026 00:00:00 +0000</pubDate>
    <description>I built a news aggregator that scores how positively each outlet words a story. Then I checked what the score was actually measuring, and cut a feature over the answer.</description>
    <content:encoded><![CDATA[<p>I run a thing called <a href="https://pickheart.com/headlines/">Headlines</a>. It reads about
thirty-five RSS feeds every hour, clusters the headlines into stories by shared
vocabulary, and ranks them by how many independent newsrooms ran each one. No
summarising, no generated text — every line is a real headline, linked to its
source.</p>
<p>Each headline also gets a sentiment score, from a small hand-written word list.
The obvious feature to build on top of that is &ldquo;show me the good news.&rdquo; I built
it. Then I looked at what it returned.</p>
<h2 id="the-most-positive-headlines-were-a-war-and-a-fraud">The most positive headlines were a war and a fraud</h2>
<p>Two of the highest-scoring headlines on the day I checked:</p>
<ul>
<li>&ldquo;Trump says Hamas agreed to next phase of Gaza peace deal&rdquo; — <strong>+0.65</strong></li>
<li>&ldquo;AI scammers outperform humans when it comes to building trust&rdquo; — <strong>+0.69</strong></li>
</ul>
<p>Nothing is broken. <em>Agreed</em>, <em>peace</em>, <em>deal</em>, <em>outperform</em> and <em>trust</em> are
positive words, and the scorer is doing exactly what it says on the tin. The
problem is what I assumed it meant.</p>
<blockquote>
<p>Sentiment scores <strong>wording</strong>. It does not score subject. A war reported in the
language of diplomacy scores like diplomacy.</p>
</blockquote>
<p>Anyone filtering on that gets positively-phrased hard news, which is not what
&ldquo;show me the good news&rdquo; promises. So I cut the feature rather than ship a
promise the number could not keep.</p>
<h2 id="most-headlines-score-nothing-at-all">Most headlines score nothing at all</h2>
<p>The second thing I found was quieter, and it changes how you read any average
built on this:</p>
<table>
<thead>
<tr>
<th>headlines scored</th>
<th>222</th>
</tr>
</thead>
<tbody>
<tr>
<td>exactly 0.0</td>
<td>68.5%</td>
</tr>
<tr>
<td>negative</td>
<td>22.1%</td>
</tr>
<tr>
<td>positive</td>
<td>9.5%</td>
</tr>
</tbody>
</table>
<p>Two thirds of headlines contain no word the list recognises. They score zero —
not &ldquo;neutral&rdquo; in any considered sense, just unmatched. Every daily mean is
therefore diluted toward zero by construction, and a mean of <code>-0.09</code> is not
&ldquo;a mildly bad day&rdquo;, it&rsquo;s &ldquo;the third of headlines that scored at all leaned
negative.&rdquo;</p>
<p>That is fine as long as it&rsquo;s labelled. It stops being fine the moment somebody
calls the chart &ldquo;mood&rdquo;.</p>
<h2 id="the-mistake-i-nearly-published">The mistake I nearly published</h2>
<p>Here is the one I&rsquo;m most glad I checked. The interesting question is not &ldquo;which
outlet is negative&rdquo; but &ldquo;which outlet <em>words the same story</em> more warmly than
its peers.&rdquo; Those sound similar. They are not.</p>
<p>Average every outlet&rsquo;s raw sentiment and you don&rsquo;t measure framing, you measure
<strong>beat</strong>. Al Jazeera covers conflict, so it scores negative. TechCrunch covers
product launches, so it scores positive. Nobody is spinning anything.</p>
<p>Controlling for the story means scoring each outlet against the mean of every
outlet that covered <em>that same event</em>:</p>
<div class="code"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">deltas</span><span class="p">(</span><span class="n">stories</span><span class="p">,</span> <span class="n">min_outlets</span><span class="o">=</span><span class="mi">3</span><span class="p">):</span>
<span class="w">    </span><span class="sd">&quot;&quot;&quot;{outlet: (sum_of_deltas, count)} for one run&#39;s stories.&quot;&quot;&quot;</span>
    <span class="n">acc</span> <span class="o">=</span> <span class="n">defaultdict</span><span class="p">(</span><span class="k">lambda</span><span class="p">:</span> <span class="p">[</span><span class="mf">0.0</span><span class="p">,</span> <span class="mi">0</span><span class="p">])</span>
    <span class="k">for</span> <span class="n">s</span> <span class="ow">in</span> <span class="n">stories</span><span class="p">:</span>
        <span class="n">cov</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;coverage&quot;</span><span class="p">)</span> <span class="ow">or</span> <span class="p">[]</span>
        <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">cov</span><span class="p">)</span> <span class="o">&lt;</span> <span class="n">min_outlets</span><span class="p">:</span>
            <span class="k">continue</span>
        <span class="n">mean</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="n">c</span><span class="p">[</span><span class="s2">&quot;sentiment&quot;</span><span class="p">]</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">cov</span><span class="p">)</span> <span class="o">/</span> <span class="nb">len</span><span class="p">(</span><span class="n">cov</span><span class="p">)</span>
        <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">cov</span><span class="p">:</span>
            <span class="n">a</span> <span class="o">=</span> <span class="n">acc</span><span class="p">[</span><span class="n">c</span><span class="p">[</span><span class="s2">&quot;publisher&quot;</span><span class="p">]]</span>
            <span class="n">a</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">+=</span> <span class="n">c</span><span class="p">[</span><span class="s2">&quot;sentiment&quot;</span><span class="p">]</span> <span class="o">-</span> <span class="n">mean</span>
            <span class="n">a</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span>
    <span class="k">return</span> <span class="n">acc</span>
</code></pre></div>

<p>Measured on one day, the naive average called Al Jazeera the most negative
outlet at <strong>−0.244</strong>. Controlled for the story, it was <strong>−0.066</strong>. Most of that
number was the subject, not the wording.</p>
<p>The <code>min_outlets=3</code> is not arbitrary either. With two outlets, whatever one says
the other is defined as its exact opposite — that&rsquo;s arithmetic, not evidence.
Three is the smallest number where the mean isn&rsquo;t just the other outlet&rsquo;s
opinion.</p>
<h2 id="what-i-did-instead">What I did instead</h2>
<p>I stopped publishing the number and started recording it.</p>
<p>One run is nowhere near enough data. Within-story headline spread runs about
0.18, so resolving a 0.05 difference between two outlets needs roughly 47
observations each. A single run gives an outlet 5 to 16, and every confidence
interval I computed on one run spanned zero — not one outlet was
distinguishable from &ldquo;no effect at all.&rdquo;</p>
<p>Published on one run, that table would be noise with real newsrooms&rsquo; names on
it, reordering itself every hour. Accumulated, an outlet appearing in ten shared
stories a day clears 47 inside a week.</p>
<p>So it writes a row a day and says nothing until it has earned the right to. The
summary function returns a confidence interval and a <code>ranked</code> flag rather than a
bare average, specifically so nothing downstream can quietly present a number
that hasn&rsquo;t.</p>
<h2 id="the-actual-lesson">The actual lesson</h2>
<p>I&rsquo;ve been a data analyst for five years and I still nearly shipped this. The
scorer worked. The code was correct. The tests, had I written them, would have
passed. Everything was green and the conclusion was wrong, because I never
checked what the number <em>meant</em> — only that it was computed.</p>
<p>The check that caught it took ten minutes: sort by the metric, read the top
five, ask whether they look like what you promised.</p>]]></content:encoded>
  </item>
</channel>
</rss>
