<?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: Subeg Singh</title>
    <description>The latest articles on DEV Community by Subeg Singh (@subeg_singh_kapoor).</description>
    <link>https://dev.to/subeg_singh_kapoor</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%2F3990629%2F2c3f773e-ce22-46c4-a4d0-0703bc1855f6.png</url>
      <title>DEV Community: Subeg Singh</title>
      <link>https://dev.to/subeg_singh_kapoor</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/subeg_singh_kapoor"/>
    <language>en</language>
    <item>
      <title>Why 99% Fast Can Still Feel Completely Broken. That's where averages lie and percentile dont.</title>
      <dc:creator>Subeg Singh</dc:creator>
      <pubDate>Fri, 07 Aug 2026 19:14:36 +0000</pubDate>
      <link>https://dev.to/subeg_singh_kapoor/why-99-fast-can-still-feel-completely-broken-thats-where-averages-lie-and-percentile-dont-3fcb</link>
      <guid>https://dev.to/subeg_singh_kapoor/why-99-fast-can-still-feel-completely-broken-thats-where-averages-lie-and-percentile-dont-3fcb</guid>
      <description>&lt;p&gt;Ten people load the same webpage. Nine of them see it in about 12 milliseconds, basically instant. The tenth person hits a slow server and waits 4 full seconds.&lt;/p&gt;

&lt;p&gt;Now someone asks: "So, how fast is our page?"&lt;/p&gt;

&lt;p&gt;You do what feels obvious. You average the ten numbers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;410.6 milliseconds.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Write that in a report and it looks fine, even respectable. But sit with it for a second. Whose experience does 410ms actually describe? Not the nine people who got 12ms. Not the one person who got 4000ms. It describes a person who doesn't exist. One outlier dragged a "typical" number to 30 to 40 times higher than what 90% of your users actually felt.&lt;/p&gt;

&lt;p&gt;That's the problem I want to talk about, and it's hiding in plain sight in most dashboards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the average betrays you
&lt;/h2&gt;

&lt;p&gt;The average works fine when data is evenly spread out, things Latency almost never looks like that. It's skewed: a big, dense pile of fast requests, then a long thin tail of slow ones stretching out to the right. A server hiccups, a connection pool runs dry, a cache misses, and now you've got outliers that are 100x, 300x, sometimes 1000x worse than normal.&lt;/p&gt;

&lt;p&gt;The average lets those outliers hijack the whole summary. One bad request can outweigh a hundred good ones in the final number. If you're making decisions off the average, you're making decisions based on something that isn't real.&lt;/p&gt;

&lt;p&gt;So what's the honest alternative?&lt;/p&gt;

&lt;h2&gt;
  
  
  Percentiles: sort it, then ask "where does X% end?"
&lt;/h2&gt;

&lt;p&gt;A percentile asks a different question than an average does. Instead of "what's the mean of all these numbers," it asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If I sort every result from fastest to slowest, what value sits X% of the way through?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;P50 (the median): the middle value. Half your requests were&lt;/li&gt;
&lt;li&gt;P90: 90% of requests were at or below this. Only the worst 10% were slower.&lt;/li&gt;
&lt;li&gt;P99: 99% were this fast or faster. Only the unluckiest 1% saw worse.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The math is simple enough that you don't need a stats course for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rank = (P / 100) × n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you take the value sitting at that rank in your sorted list.&lt;/p&gt;

&lt;p&gt;Let's run it on our ten webpage loads, sorted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="mf"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;4000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;P50: rank = 0.50 × 10 = 5, so average the 5th and 6th values: (12+12)/2 = 12ms&lt;/li&gt;
&lt;li&gt;P90: rank = 0.90 × 10 = 9, so the 9th value: 15ms&lt;/li&gt;
&lt;li&gt;P99: rank = 0.99 × 10 = 9.9, which lands basically on the la&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Look at what just happened. Instead of one number papering over the situation, you now get two honest statements. A typical user sees 12ms, and 90% of users see under 15ms: the experience is genuinely good. Meanwhile, the worst 1% of requests are getting hit with 4-second waits: there's a real problem to go fix.&lt;/p&gt;

&lt;p&gt;Nothing gets hidden and nothing gets averaged away. The good news and the bad news stay as two separate, true statements instead of blending into one misleading one.&lt;/p&gt;

&lt;p&gt;Here's roughly what that distribution looks like if you tried to sketch it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Latency distribution, 10 requests

0-20ms      █████████  (9 requests)   what "typical" actually feels like
   .              (a long, empty gap, nothing happens here)
   .
3980-4000ms █          (1 request)    the tail: small in count
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gap in the middle is the whole story. The average pretends it isn't there.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Percentage" alone doesn't mean anything
&lt;/h2&gt;

&lt;p&gt;This is worth pausing on, because people mix the words up constantly. "Percentage" by itself is meaningless: percentage of what? A percentile is a percentage paired with a value, and that pairing is exactly what makes it a complete statement. "P99 = 500ms" tells you precisely that 99% of requests finish in under 500ms. That's why engineering teams write SLAs like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;P99 latency must stay under 300ms.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a promise about the worst realistic case most users could hit. Compare it to "average latency is 50ms," which tells you nothing about how bad the&lt;br&gt;
bad days get.&lt;/p&gt;
&lt;h2&gt;
  
  
  The deeper reason the tail matters
&lt;/h2&gt;

&lt;p&gt;This is the part that changed how I think about it, and it's just middle-school probability.&lt;/p&gt;

&lt;p&gt;Say a webpage needs 100 small backend calls to fully render. That's not unusual at all, think of a page pulling in dozens of tiny images, widgets, or API responses. Suppose each individual call is fast 99% of the time. Sounds great. P99 is "good."&lt;/p&gt;

&lt;p&gt;What's the chance all 100 calls come back fast, so the whole p&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.99^100 ≈ 0.366
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;36.6%. Even though every single piece is "good" 99% of the time, the page as a whole is slow almost two out of three times, purely because it depends on&lt;br&gt;
so many moving parts, and any one slow piece drags the whole e&lt;/p&gt;

&lt;p&gt;Watch how fast this compounds as the number of dependent calls grows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Number of dependent calls&lt;/th&gt;
&lt;th&gt;Chance the whole thing is fast&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;99.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;90.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;60.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;36.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;13.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is called tail latency amplification, and it's the real reason companies operating at Google or Amazon scale obsess over P99 and even P99.9 instead of the average. At that scale, the "rare" bad case stops being rare. It becomes the default experience of the overall system, because there are so many chances for something in the chain to be the one unlucky piece.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What it tells you&lt;/th&gt;
&lt;th&gt;Weakness&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Average&lt;/td&gt;
&lt;td&gt;The mathematical mean of all values&lt;/td&gt;
&lt;td&gt;Easily distos, describes no real user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Percentage (alone)&lt;/td&gt;
&lt;td&gt;Nothing, without a paired value&lt;/td&gt;
&lt;td&gt;Not a complete statement by itself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Percentile (P50, P90, P99...)&lt;/td&gt;
&lt;td&gt;The value below which X% of results fall&lt;/td&gt;
&lt;td&gt;Requires sorting the data, more work but far more honest&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The one-sentence takeaway
&lt;/h2&gt;

&lt;p&gt;The average tells you a number nobody experienced. Percentiles tell you what percentage of real people had a good experience, and exactly how bad it gets for the unlucky few. That second part is what actually decides how a system feels to use.&lt;/p&gt;

&lt;p&gt;I keep coming back to this: the average isn't wrong because the math is wrong. It's wrong because it answers a question nobody actually asked. Nobody wants to know the mean of all outcomes. They want to know what their own experience will probably be, and how bad it can realistically get. Percentiles&lt;br&gt;
answer that. Averages just sound like they do.&lt;/p&gt;

&lt;p&gt;Next time someone hands you a dashboard with a single "avg lathe P99 next to it. That one extra number is usually where thereal story is hiding.&lt;/p&gt;

&lt;p&gt;Do you track P99 or P99.9 in your own systems, or has "average latency" been quietly misleading your team too? I'd genuinely like to hear your version of the 4000ms outlier in the comments.&lt;/p&gt;

&lt;p&gt;If this was useful, I write about problem-solving, AI/ML, and the thinking behind the tools we build with. Feel free to connect with me on LinkedIn.&lt;/p&gt;

</description>
      <category>mlops</category>
      <category>llm</category>
      <category>ai</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>The Greed of Low Bias and Low Variance</title>
      <dc:creator>Subeg Singh</dc:creator>
      <pubDate>Fri, 19 Jun 2026 18:04:03 +0000</pubDate>
      <link>https://dev.to/subeg_singh_kapoor/the-greed-of-low-bias-and-low-variance-408i</link>
      <guid>https://dev.to/subeg_singh_kapoor/the-greed-of-low-bias-and-low-variance-408i</guid>
      <description>&lt;h2&gt;
  
  
  Every model is a little bit greedy
&lt;/h2&gt;

&lt;p&gt;I will start with a example of two archers. The first one always aims slightly left of the bullseye i.e. to the left of the centre.&lt;br&gt;
Every single arrow, same mistake, same direction. The second one aims dead&lt;br&gt;
center on average, but for both of them, their hand shakes, so the arrows land all over the board.&lt;/p&gt;

&lt;p&gt;Neither archer is "bad". They're both being greedy in their own way. The first archer is greedy for consistency. He has locked onto one strategy and won't budge, even though it's wrong. He just wants the result with same input even though it is wrong. The second archer is greedy for flexibility. He will react to every gust of wind, every twitch, every tiny signal, even the ones that don't matter.&lt;/p&gt;

&lt;p&gt;That's bias and variance. The maddening part of machine learning is that you&lt;br&gt;
can't fix both at once. Reduce one, and the other usually grows. Once you&lt;br&gt;
understand why it happens, and how we actually measure it, it stops being a&lt;br&gt;
vague textbook warning and starts feeling like common sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bias: the model that won't change its mind
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Bias is systematic error&lt;/em&gt;. It's what happens when your model is too simple to capture the real pattern in the data. Think a straight line trying to fit a curve. No matter how much data you throw at it, it keeps making the same kind of mistake, because the shape it's allowed to take is fundamentally wrong.&lt;br&gt;
High-bias models underfit: they're stable, but stably wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Variance: the model that overreacts
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Variance is instability&lt;/em&gt;. It's what happens when your model is so flexible that it doesn't just learn the underlying pattern, it learns the noise too. Train it on one sample of data and it draws one curve. Train it on a slightly different sample and it draws a wildly different curve. High-variance models overfit: they're accurate on the data they've seen, but unreliable everywhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  So how do you actually measure variance?
&lt;/h2&gt;

&lt;p&gt;"How much does my model swing around" sounds vague until you turn it into a&lt;br&gt;
concrete procedure. The trick: fix the test point, vary the training data.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take your training set and resample it. The standard way is bootstrapping,
where you randomly draw examples with replacement to create a new training
set of the same size. Do this B = 100 times and you get 100 slightly different training sets, all pulled from the same pool.&lt;/li&gt;
&lt;li&gt;Train your model fresh on each of those 100 training sets. Same algorithm, same hyperparameters. The only thing that changes is which examples it saw.&lt;/li&gt;
&lt;li&gt;Now take one single test point, something none of those models trained on, and ask all 100 models to predict it.&lt;/li&gt;
&lt;li&gt;You now have 100 different predictions for the exact same input. The spread of those 100 numbers is your variance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Note what stayed constant: the test point. If you let the test data change too, like in plain k-fold cross-validation, you're no longer isolating variance.&lt;br&gt;
You're mixing it with "this fold happened to be harder." Keeping the test point fixed is what makes the isolation real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do we square the deviations?
&lt;/h2&gt;

&lt;p&gt;Once you have your 100 predictions and their average, why not measure "how far&lt;br&gt;
off" each prediction is with plain distance instead of squaring it?&lt;/p&gt;

&lt;p&gt;The first problem is cancellation. A prediction that's +5 above the average&lt;br&gt;
and one that's -5 below cancel out to zero when summed, even though both&lt;br&gt;
represent real instability. Squaring makes every deviation positive, so errors in opposite directions can't hide each other.&lt;/p&gt;

&lt;p&gt;The second is that squaring punishes big swings harder. A model that's&lt;br&gt;
occasionally wildly wrong is more dangerous than one that's consistently a little off. Squaring grows faster than the deviation itself, so an error of 10 contributes 100x more than an error of 1. That's by design. You want the measurement to be sensitive to extreme instability, not just average shakiness.&lt;/p&gt;

&lt;p&gt;Third, it matches the actual statistical definition. Variance, in statistics, is the expected squared deviation from the mean. This isn't something machine learning invented. It's the same definition you'd use to describe how spread out any dataset is, so using it here isn't a choice, it's just staying&lt;br&gt;
consistent.&lt;/p&gt;

&lt;p&gt;And fourth, squared functions are smooth and differentiable everywhere, which&lt;br&gt;
matters once you start optimizing models. This is also why the famous identity&lt;br&gt;
&lt;em&gt;Total Error = Bias² + Variance + Irreducible Noise&lt;/em&gt; uses squared bias. The decomposition only works cleanly when both terms are squared quantities.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual goal
&lt;/h2&gt;

&lt;p&gt;You're not trying to make bias zero or variance zero. That target doesn't exist outside of toy examples with infinite noiseless data. You're looking for the point where their combined greed costs you the least: a model flexible enough to be roughly right, but stable enough not to chase noise.&lt;/p&gt;

&lt;p&gt;That means giving up a little of each archer's stubbornness. Add just enough&lt;br&gt;
flexibility to fix the systematic miss, and just enough constraint to steady the shaky hand. Regularization, more training data, ensembling are all different tools for the same goal: tell your model to be less greedy.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>datascience</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
