<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rune Skarsfjor</title>
    <description>The latest articles on DEV Community by Rune Skarsfjor (@sprettball).</description>
    <link>https://dev.to/sprettball</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4109209%2F9d2dc73a-4ebe-4571-b817-9139092683ce.png</url>
      <title>DEV Community: Rune Skarsfjor</title>
      <link>https://dev.to/sprettball</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sprettball"/>
    <language>en</language>
    <item>
      <title>Three ways your dashboard can be correct and still lie</title>
      <dc:creator>Rune Skarsfjor</dc:creator>
      <pubDate>Fri, 04 Sep 2026 06:56:56 +0000</pubDate>
      <link>https://dev.to/sprettball/three-ways-your-dashboard-can-be-correct-and-still-lie-2npc</link>
      <guid>https://dev.to/sprettball/three-ways-your-dashboard-can-be-correct-and-still-lie-2npc</guid>
      <description>&lt;p&gt;Our dataset said the average loan was 2.3 million kroner. The number that actually mattered was 255,000. Both were correct. Only one of them was true.&lt;/p&gt;

&lt;p&gt;This is a writeup of three ways a dashboard can be arithmetically perfect and still lie, using real figures from an analysis of 1,000 Norwegian debt consolidation applications. If you build reporting for anyone, you have probably shipped at least one of these.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Summing a field that contains two different things
&lt;/h2&gt;

&lt;p&gt;A debt consolidation loan pays off your expensive credit card debt. It also, if you own property, rolls your existing mortgage into the same new loan. Same column in the database. Same &lt;code&gt;loan_amount&lt;/code&gt;. Utterly different meaning.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loan_amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;applications&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- 2,300,000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That query is right and the answer is useless. Of that 2.3 million, roughly 1.9 million is an existing mortgage being moved from one lender to another. The expensive debt, the part the customer actually has a problem with, averages &lt;strong&gt;255,000&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So the headline figure overstates the thing you care about by a factor of nine.&lt;/p&gt;

&lt;p&gt;Nothing in the schema warns you. &lt;code&gt;loan_amount&lt;/code&gt; is a number, &lt;code&gt;AVG&lt;/code&gt; is a function, the result renders fine. The bug is that one column is holding two concepts and only a human who understands the domain will notice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- what you actually wanted&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unsecured_debt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;applications&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- 255,000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a column can mean two things depending on another column, split it. Every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Reporting the mean when the distribution has a tail
&lt;/h2&gt;

&lt;p&gt;Income in this dataset runs from ordinary salaries up to about five million kroner. A handful of very high earners drag the mean upward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mean income:&lt;/strong&gt; ~635,000&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Median income:&lt;/strong&gt; 647,000 for homeowners, 550,000 for renters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Look at what happens there. The mean sits &lt;em&gt;between&lt;/em&gt; the two medians and describes neither group. Someone reading only the mean concludes the typical applicant earns 635,000. Nobody earns 635,000. It is an artefact.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;housing&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;income&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mean&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;median&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cheap rule that has never failed me: if &lt;code&gt;mean&lt;/code&gt; and &lt;code&gt;median&lt;/code&gt; differ by more than a few percent, the mean is not describing anybody and should not be the number on the card. Show the median, and show the spread.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Averaging across segments that behave differently
&lt;/h2&gt;

&lt;p&gt;This is the one that cost us the most rework.&lt;/p&gt;

&lt;p&gt;Applicants split into two groups with genuinely different outcomes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Rate before&lt;/th&gt;
&lt;th&gt;Rate after&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Owns property (300 applicants)&lt;/td&gt;
&lt;td&gt;12.5%&lt;/td&gt;
&lt;td&gt;7.6%&lt;/td&gt;
&lt;td&gt;−4.9pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No property (679 applicants)&lt;/td&gt;
&lt;td&gt;15.2%&lt;/td&gt;
&lt;td&gt;12.6%&lt;/td&gt;
&lt;td&gt;−2.6pp&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A single blended "average rate reduction" across all applicants produces a number that is true of the population and false of every individual in it. Worse, it is actively misleading in opposite directions for the two groups: it undersells the result for one and oversells it for the other.&lt;/p&gt;

&lt;p&gt;The fix is not statistical, it is editorial. Decide what question the reader is asking. They are asking &lt;em&gt;what happens to someone like me&lt;/em&gt;. That question has two answers here, so the chart needs two rows and no total.&lt;/p&gt;

&lt;p&gt;I have started treating a total row as something you justify rather than something you add by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing that ties all three together
&lt;/h2&gt;

&lt;p&gt;Each of these is a case where the aggregation is correct and the framing is wrong. Your tests will not catch them, because nothing is broken. &lt;code&gt;AVG&lt;/code&gt; returns the average. The column contains what it contains.&lt;/p&gt;

&lt;p&gt;The only defence I have found is to ask, for every number on a page: &lt;em&gt;if a reader acted on this alone, what would they get wrong?&lt;/em&gt; For the 2.3 million figure, they would conclude Norwegian households carry nine times more expensive debt than they do. That question takes ten seconds and it has caught more real problems for me than any amount of test coverage.&lt;/p&gt;




&lt;p&gt;The figures above come from &lt;a href="https://samlegjeld.no/gjeldanalyse-2026/" rel="noopener noreferrer"&gt;an analysis of 1,000 debt consolidation applications&lt;/a&gt; published by &lt;a href="https://samlegjeld.no/gjeldanalyse-2026/" rel="noopener noreferrer"&gt;samlegjeld.no&lt;/a&gt;. The full breakdown is there.&lt;/p&gt;

</description>
      <category>data</category>
      <category>sql</category>
      <category>webdev</category>
      <category>analytics</category>
    </item>
  </channel>
</rss>
