<?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: Danishh-ux</title>
    <description>The latest articles on DEV Community by Danishh-ux (@danishhux).</description>
    <link>https://dev.to/danishhux</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%2F4101704%2F53831250-f76f-4a9b-8e21-5884e5a1a0e7.png</url>
      <title>DEV Community: Danishh-ux</title>
      <link>https://dev.to/danishhux</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/danishhux"/>
    <language>en</language>
    <item>
      <title>My Model Had 100% Recall — Then I Realized It Was Predicting "Exoplanet" for Everything</title>
      <dc:creator>Danishh-ux</dc:creator>
      <pubDate>Sun, 30 Aug 2026 18:05:26 +0000</pubDate>
      <link>https://dev.to/danishhux/my-model-had-100-recall-then-i-realized-it-was-predicting-exoplanet-for-everything-4ii6</link>
      <guid>https://dev.to/danishhux/my-model-had-100-recall-then-i-realized-it-was-predicting-exoplanet-for-everything-4ii6</guid>
      <description>&lt;p&gt;A few months ago, I set out to build a model that detects exoplanets from stellar brightness data (light curves) collected by NASA's Kepler telescope. When a planet passes in front of a star, it blocks a tiny bit of light — a "transit." The goal: train a model to spot that pattern.&lt;/p&gt;

&lt;p&gt;I thought it'd be a straightforward binary classification problem. It was not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: 100% recall, useless model
&lt;/h2&gt;

&lt;p&gt;My first model was a standard 1D CNN — a few conv layers, batch norm, LeakyReLU, max pooling. Training loss went down steadily. Looked great.&lt;/p&gt;

&lt;p&gt;Then I checked the actual metrics:&lt;/p&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;Score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Recall&lt;/td&gt;
&lt;td&gt;1.0000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Precision&lt;/td&gt;
&lt;td&gt;0.0071&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F1 Score&lt;/td&gt;
&lt;td&gt;0.0141&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Perfect recall. Precision near zero. The model was labeling almost &lt;em&gt;every&lt;/em&gt; sample as an exoplanet. It hadn't learned anything — it had just found the laziest way to minimize loss.&lt;/p&gt;

&lt;p&gt;This is the trap of imbalanced data. The dataset had:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Split&lt;/th&gt;
&lt;th&gt;Normal Stars&lt;/th&gt;
&lt;th&gt;Exoplanets&lt;/th&gt;
&lt;th&gt;% Positive&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Train&lt;/td&gt;
&lt;td&gt;4292&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;0.72%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validation&lt;/td&gt;
&lt;td&gt;758&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;0.79%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test&lt;/td&gt;
&lt;td&gt;565&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;0.88%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Out of ~5,600 samples, only 42 were exoplanets — less than 1%. A model that predicts "not an exoplanet" every single time gets 99% accuracy while being completely useless. Accuracy was lying to me.&lt;/p&gt;

&lt;h2&gt;
  
  
  What didn't work
&lt;/h2&gt;

&lt;p&gt;I tried the usual playbook for imbalance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SMOTE&lt;/strong&gt; — generates synthetic minority-class examples. Barely helped, because light curves have complex temporal structure that's hard to fake by interpolating between real examples.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SMOTETomek&lt;/strong&gt; — combined oversampling with cleanup. Slightly better, still unstable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threshold tuning&lt;/strong&gt; (0.30–0.60) — adjusting the decision boundary can't fix a model that's producing garbage probability estimates in the first place.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these addressed the actual problem: the model almost never &lt;em&gt;saw&lt;/em&gt; a positive example during training.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually worked
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;WeightedRandomSampler.&lt;/strong&gt; Instead of sampling batches randomly (where most batches had zero exoplanets), I weighted sampling so minority-class examples showed up far more often during training. This alone was one of the biggest single improvements in the whole project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Focal Loss&lt;/strong&gt; instead of standard Binary Cross Entropy. BCE treats every sample equally, so the model gets flooded with "easy" majority-class gradient signal and barely learns from the rare hard examples. Focal Loss down-weights easy examples and focuses learning on the difficult ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FL = α(1 - pt)^γ * BCE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with &lt;code&gt;alpha = 0.25&lt;/code&gt;, &lt;code&gt;gamma = 2.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A 1D Residual Network.&lt;/strong&gt; I'd assumed ResNets were an image-only architecture. Turns out residual connections work just as well for 1D time series — the skip connection (&lt;code&gt;output = F(x) + x&lt;/code&gt; instead of just &lt;code&gt;F(x)&lt;/code&gt;) helps preserve signal information and stabilizes gradient flow through deeper networks. The final model: an initial conv block, four residual blocks, global average pooling, and a fully connected output layer.&lt;/p&gt;

&lt;p&gt;Along with these, I also applied a median filter to remove sensor spikes and normalized each light curve individually (zero mean, unit variance) so the model focused on the &lt;em&gt;shape&lt;/em&gt; of the dip rather than absolute brightness.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results (and the part I almost didn't report properly)
&lt;/h2&gt;

&lt;p&gt;Best single validation run: &lt;strong&gt;F1 = 0.9091&lt;/strong&gt;. Looked great in isolation.&lt;/p&gt;

&lt;p&gt;But with only 5-6 positive examples in any given validation/test split, one number from one split doesn't mean much. So I ran Stratified 5-Fold Cross Validation instead:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fold&lt;/th&gt;
&lt;th&gt;F1 Score&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;0.4545&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0.8421&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0.7778&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0.5333&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;0.6667&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Average&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.6549&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's a big spread — 0.45 to 0.84. With only ~6-7 positive examples per fold, misclassifying even a single exoplanet swings the F1 score substantially. This variance &lt;em&gt;is&lt;/em&gt; the real result. It's a more honest signal than any single number.&lt;/p&gt;

&lt;p&gt;For contrast, here's the final test set report:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Precision&lt;/th&gt;
&lt;th&gt;Recall&lt;/th&gt;
&lt;th&gt;F1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Non-Exoplanet&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exoplanet&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A perfect score across the board, on only 5 positive samples. It's tempting to lead with that number — it looks amazing. But with a sample size that small, it's close to meaningless on its own. The cross-validation average is the number I actually trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took away from this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Class imbalance can be a harder problem than model architecture.&lt;/li&gt;
&lt;li&gt;Accuracy is often actively misleading on imbalanced data — check precision/recall/F1 from the start.&lt;/li&gt;
&lt;li&gt;A "perfect" test score on 5 samples tells you almost nothing. Cross-validation exists for exactly this situation.&lt;/li&gt;
&lt;li&gt;Fixing the &lt;em&gt;data pipeline&lt;/em&gt; (sampling, loss function) mattered more here than any architecture change.&lt;/li&gt;
&lt;li&gt;ResNets aren't just for images.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Code and full report: [link to your repo]&lt;/p&gt;

&lt;p&gt;Would genuinely love feedback — if anyone has dealt with similarly extreme class imbalance (sub-1% positive class) on time-series data, I'd like to hear what worked for you.&lt;br&gt;
`&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>deeplearning</category>
      <category>datascience</category>
      <category>python</category>
    </item>
  </channel>
</rss>
