{
  "id": 696759,
  "title": "Your Test Fires Don't Look Like Your Training Fires",
  "content": "<h2><strong>Score: 0.97258 | Improvement over baseline: +0.00092</strong></h2>\n<h2>The Core Idea in One Sentence</h2>\n<h2>The test near-zone fires look structurally different from training near-zone fires — larger, slower, and almost entirely single-perimeter — so I upweight the training fires that most resemble that profile.</h2>\n<h2>Background</h2>\n<p>My base model is the standard tri-survival stack (GBSA + CoxPH + RSF + LGB IPCW) that most top solutions use. After reaching 0.97166 with that stack, I spent time on EDA to understand what was left on the table.\nThe key finding: the 28 near-zone test fires have a dramatically different feature profile from the 69 near-zone training fires.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Train near (mean)</th>\n<th>Test near (mean)</th>\n<th>Ratio</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>closing_speed_abs_m_per_h</code></td>\n<td>9.1</td>\n<td>0.31</td>\n<td>29× lower</td>\n</tr>\n<tr>\n<td><code>alignment_abs</code></td>\n<td>0.34</td>\n<td>0.20</td>\n<td>41% lower</td>\n</tr>\n<tr>\n<td><code>area_first_ha</code></td>\n<td>230 ha</td>\n<td>401 ha</td>\n<td>74% larger</td>\n</tr>\n<tr>\n<td><code>low_temporal_resolution</code></td>\n<td>52%</td>\n<td>64%</td>\n<td>More LQ fires</td>\n</tr>\n</tbody>\n</table>\n<h2>The test near-zone fires are slow, large, and poorly observed. The model trained on fast, small, well-observed training fires doesn't generalise well to them.</h2>\n<h2>The Fix: IsolationForest Anomaly Upweighting</h2>\n<p>The idea is simple: identify which training fires are most \"anomalous\" (i.e. most similar to the out-of-distribution test fires) and give them more weight during training.</p>\n<pre><code>from sklearn.ensemble import IsolationForest\nfrom sklearn.preprocessing import RobustScaler\n# Fit IsolationForest on training features\nscaler = RobustScaler()\nX_scaled = scaler.fit_transform(X_gbsa_train.fillna(0))\niso = IsolationForest(n_estimators=200, contamination=0.2, random_state=42)\niso.fit(X_scaled)\n# conformity score: lower = more anomalous\nconformity = iso.score_samples(X_scaled)\n# Map to weights: anomalous fires → 1.0, normal fires → 0.05\nsample_weights = np.interp(conformity,\n(conformity.min(), conformity.max()),\n(1.0, 0.05))\n</code></pre>\n<p>These weights are then passed to every GBSA and Cox model fit:</p>\n<pre><code>m.fit(X_train.iloc[tr_idx], y_surv[tr_idx],\nsample_weight=sample_weights[tr_idx])\n</code></pre>\n<h2>The logic: the ~44 fires flagged as anomalous (<code>contamination=0.20</code>) get full weight (1.0) during training. The remaining ~177 \"normal\" fires get down-weighted to 0.05. This forces the model to prioritise learning the anomalous regime — which is exactly where the test fires live.</h2>\n<h2>Parameter Sweep</h2>\n<p>I ran a systematic sweep across floor values and contamination levels:\n<strong>Floor sweep (contamination=0.10 fixed):</strong></p>\n<table>\n<thead>\n<tr>\n<th>Floor</th>\n<th>Score</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0.50</td>\n<td>0.97184</td>\n</tr>\n<tr>\n<td>0.10</td>\n<td>0.97240</td>\n</tr>\n<tr>\n<td>0.07</td>\n<td>0.97250</td>\n</tr>\n<tr>\n<td>0.06</td>\n<td>0.97254</td>\n</tr>\n<tr>\n<td><strong>0.05</strong></td>\n<td><strong>0.97256</strong></td>\n</tr>\n<tr>\n<td>0.03</td>\n<td>0.97169</td>\n</tr>\n<tr>\n<td>0.01</td>\n<td>0.97127</td>\n</tr>\n<tr>\n<td>0.00</td>\n<td>0.97085</td>\n</tr>\n<tr>\n<td>Floor=0.05 is the peak. Below it the model starts ignoring too many training fires and overfits to the anomalous subset.</td>\n<td></td>\n</tr>\n<tr>\n<td><strong>Contamination sweep (floor=0.05 fixed):</strong></td>\n<td></td>\n</tr>\n<tr>\n<td>Contamination</td>\n<td>Fires upweighted</td>\n</tr>\n<tr>\n<td>--------------</td>\n<td>-----------------</td>\n</tr>\n<tr>\n<td>0.05</td>\n<td>~11</td>\n</tr>\n<tr>\n<td><strong>0.10</strong></td>\n<td><strong>~22</strong></td>\n</tr>\n<tr>\n<td>0.15</td>\n<td>~33</td>\n</tr>\n<tr>\n<td>0.20</td>\n<td>~44</td>\n</tr>\n<tr>\n<td>0.25</td>\n<td>~55</td>\n</tr>\n<tr>\n<td>Interesting: contamination=0.15, 0.20, and 0.25 all land at exactly 0.97258. The signal saturates above ~33 upweighted fires — adding more anomalous fires to the upweighted set doesn't help further.</td>\n<td></td>\n</tr>\n</tbody>\n</table>\n<h2><strong>This submission</strong> uses <code>contamination=0.20, floor=0.05</code> — one of the three configurations that achieved the best score.</h2>\n<h2>What Didn't Work</h2>\n<p>For completeness, I tried many other approaches after finding this signal:</p>\n<ul>\n<li><strong>Applying weights to RSF</strong> — 0.97238, slightly hurt</li>\n<li><strong>Perimeter distance features</strong> — 0.96978, hurt</li>\n<li><strong>Combining anomaly weights + perimeter features</strong> — 0.97101, hurt</li>\n<li><strong>Single config × 80 seeds</strong> — 0.97049, hurt</li>\n<li><strong>Config search (leaf=5, sub=0.9)</strong> — 0.96660, badly hurt</li>\n<li><strong>CatBoost replacing LGB</strong> — 0.97131, hurt</li>\n</ul>\n<h2>The conclusion: anomaly upweighting in GBSA + Cox only is the right application. Adding it to RSF or combining with other changes dilutes the signal.</h2>\n<h2>Key Lessons</h2>\n<p><strong>OOF doesn't predict LB at 221 rows.</strong> I had multiple experiments with strong OOF improvement (notably v21 with +0.00053 OOF hybrid) that hurt on the leaderboard. The only reliable signal is the public LB itself.\n<strong>The floor matters more than the contamination.</strong> The shape of the weight distribution (how harshly normal fires are downweighted) has more impact than exactly how many fires are flagged as anomalous.</p>\n<h2><strong>Structural insights beat tuning.</strong> The single biggest gain in this competition came from the 5km distance gate — a structural insight about the data. Anomaly upweighting is a close second, and it's also fundamentally about data structure (recognising the train/test distribution shift).</h2>\n<h2>Implementation</h2>\n<p>The change is minimal — four lines added before the main GBSA training loop:</p>\n<pre><code>from sklearn.ensemble import IsolationForest\nfrom sklearn.preprocessing import RobustScaler\n_scaler = RobustScaler()\n_X = _scaler.fit_transform(X_gbsa_train.fillna(0))\n_iso = IsolationForest(n_estimators=200, contamination=0.2, random_state=42)\n_iso.fit(_X)\n_conf = _iso.score_samples(_X)\nsample_weights = np.interp(_conf, (_conf.min(), _conf.max()), (1.0, 0.05))\n</code></pre>\n<p>Then pass <code>sample_weight=sample_weights[tr_idx]</code> to every <code>m.fit()</code> call in the GBSA and Cox loops.</p>\n<h2>Everything else — blend weights, feature engineering, model configs, seeds — is identical to the standard reference architecture.</h2>",
  "comments": [],
  "comment_count": 0,
  "messages": [
    {
      "id": 3452627,
      "postDate": "2026-05-04T00:22:21.113Z",
      "content": "<h2><strong>Score: 0.97258 | Improvement over baseline: +0.00092</strong></h2>\n<h2>The Core Idea in One Sentence</h2>\n<h2>The test near-zone fires look structurally different from training near-zone fires — larger, slower, and almost entirely single-perimeter — so I upweight the training fires that most resemble that profile.</h2>\n<h2>Background</h2>\n<p>My base model is the standard tri-survival stack (GBSA + CoxPH + RSF + LGB IPCW) that most top solutions use. After reaching 0.97166 with that stack, I spent time on EDA to understand what was left on the table.\nThe key finding: the 28 near-zone test fires have a dramatically different feature profile from the 69 near-zone training fires.</p>\n<table>\n<thead>\n<tr>\n<th>Feature</th>\n<th>Train near (mean)</th>\n<th>Test near (mean)</th>\n<th>Ratio</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>closing_speed_abs_m_per_h</code></td>\n<td>9.1</td>\n<td>0.31</td>\n<td>29× lower</td>\n</tr>\n<tr>\n<td><code>alignment_abs</code></td>\n<td>0.34</td>\n<td>0.20</td>\n<td>41% lower</td>\n</tr>\n<tr>\n<td><code>area_first_ha</code></td>\n<td>230 ha</td>\n<td>401 ha</td>\n<td>74% larger</td>\n</tr>\n<tr>\n<td><code>low_temporal_resolution</code></td>\n<td>52%</td>\n<td>64%</td>\n<td>More LQ fires</td>\n</tr>\n</tbody>\n</table>\n<h2>The test near-zone fires are slow, large, and poorly observed. The model trained on fast, small, well-observed training fires doesn't generalise well to them.</h2>\n<h2>The Fix: IsolationForest Anomaly Upweighting</h2>\n<p>The idea is simple: identify which training fires are most \"anomalous\" (i.e. most similar to the out-of-distribution test fires) and give them more weight during training.</p>\n<pre><code>from sklearn.ensemble import IsolationForest\nfrom sklearn.preprocessing import RobustScaler\n# Fit IsolationForest on training features\nscaler = RobustScaler()\nX_scaled = scaler.fit_transform(X_gbsa_train.fillna(0))\niso = IsolationForest(n_estimators=200, contamination=0.2, random_state=42)\niso.fit(X_scaled)\n# conformity score: lower = more anomalous\nconformity = iso.score_samples(X_scaled)\n# Map to weights: anomalous fires → 1.0, normal fires → 0.05\nsample_weights = np.interp(conformity,\n(conformity.min(), conformity.max()),\n(1.0, 0.05))\n</code></pre>\n<p>These weights are then passed to every GBSA and Cox model fit:</p>\n<pre><code>m.fit(X_train.iloc[tr_idx], y_surv[tr_idx],\nsample_weight=sample_weights[tr_idx])\n</code></pre>\n<h2>The logic: the ~44 fires flagged as anomalous (<code>contamination=0.20</code>) get full weight (1.0) during training. The remaining ~177 \"normal\" fires get down-weighted to 0.05. This forces the model to prioritise learning the anomalous regime — which is exactly where the test fires live.</h2>\n<h2>Parameter Sweep</h2>\n<p>I ran a systematic sweep across floor values and contamination levels:\n<strong>Floor sweep (contamination=0.10 fixed):</strong></p>\n<table>\n<thead>\n<tr>\n<th>Floor</th>\n<th>Score</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0.50</td>\n<td>0.97184</td>\n</tr>\n<tr>\n<td>0.10</td>\n<td>0.97240</td>\n</tr>\n<tr>\n<td>0.07</td>\n<td>0.97250</td>\n</tr>\n<tr>\n<td>0.06</td>\n<td>0.97254</td>\n</tr>\n<tr>\n<td><strong>0.05</strong></td>\n<td><strong>0.97256</strong></td>\n</tr>\n<tr>\n<td>0.03</td>\n<td>0.97169</td>\n</tr>\n<tr>\n<td>0.01</td>\n<td>0.97127</td>\n</tr>\n<tr>\n<td>0.00</td>\n<td>0.97085</td>\n</tr>\n<tr>\n<td>Floor=0.05 is the peak. Below it the model starts ignoring too many training fires and overfits to the anomalous subset.</td>\n<td></td>\n</tr>\n<tr>\n<td><strong>Contamination sweep (floor=0.05 fixed):</strong></td>\n<td></td>\n</tr>\n<tr>\n<td>Contamination</td>\n<td>Fires upweighted</td>\n</tr>\n<tr>\n<td>--------------</td>\n<td>-----------------</td>\n</tr>\n<tr>\n<td>0.05</td>\n<td>~11</td>\n</tr>\n<tr>\n<td><strong>0.10</strong></td>\n<td><strong>~22</strong></td>\n</tr>\n<tr>\n<td>0.15</td>\n<td>~33</td>\n</tr>\n<tr>\n<td>0.20</td>\n<td>~44</td>\n</tr>\n<tr>\n<td>0.25</td>\n<td>~55</td>\n</tr>\n<tr>\n<td>Interesting: contamination=0.15, 0.20, and 0.25 all land at exactly 0.97258. The signal saturates above ~33 upweighted fires — adding more anomalous fires to the upweighted set doesn't help further.</td>\n<td></td>\n</tr>\n</tbody>\n</table>\n<h2><strong>This submission</strong> uses <code>contamination=0.20, floor=0.05</code> — one of the three configurations that achieved the best score.</h2>\n<h2>What Didn't Work</h2>\n<p>For completeness, I tried many other approaches after finding this signal:</p>\n<ul>\n<li><strong>Applying weights to RSF</strong> — 0.97238, slightly hurt</li>\n<li><strong>Perimeter distance features</strong> — 0.96978, hurt</li>\n<li><strong>Combining anomaly weights + perimeter features</strong> — 0.97101, hurt</li>\n<li><strong>Single config × 80 seeds</strong> — 0.97049, hurt</li>\n<li><strong>Config search (leaf=5, sub=0.9)</strong> — 0.96660, badly hurt</li>\n<li><strong>CatBoost replacing LGB</strong> — 0.97131, hurt</li>\n</ul>\n<h2>The conclusion: anomaly upweighting in GBSA + Cox only is the right application. Adding it to RSF or combining with other changes dilutes the signal.</h2>\n<h2>Key Lessons</h2>\n<p><strong>OOF doesn't predict LB at 221 rows.</strong> I had multiple experiments with strong OOF improvement (notably v21 with +0.00053 OOF hybrid) that hurt on the leaderboard. The only reliable signal is the public LB itself.\n<strong>The floor matters more than the contamination.</strong> The shape of the weight distribution (how harshly normal fires are downweighted) has more impact than exactly how many fires are flagged as anomalous.</p>\n<h2><strong>Structural insights beat tuning.</strong> The single biggest gain in this competition came from the 5km distance gate — a structural insight about the data. Anomaly upweighting is a close second, and it's also fundamentally about data structure (recognising the train/test distribution shift).</h2>\n<h2>Implementation</h2>\n<p>The change is minimal — four lines added before the main GBSA training loop:</p>\n<pre><code>from sklearn.ensemble import IsolationForest\nfrom sklearn.preprocessing import RobustScaler\n_scaler = RobustScaler()\n_X = _scaler.fit_transform(X_gbsa_train.fillna(0))\n_iso = IsolationForest(n_estimators=200, contamination=0.2, random_state=42)\n_iso.fit(_X)\n_conf = _iso.score_samples(_X)\nsample_weights = np.interp(_conf, (_conf.min(), _conf.max()), (1.0, 0.05))\n</code></pre>\n<p>Then pass <code>sample_weight=sample_weights[tr_idx]</code> to every <code>m.fit()</code> call in the GBSA and Cox loops.</p>\n<h2>Everything else — blend weights, feature engineering, model configs, seeds — is identical to the standard reference architecture.</h2>",
      "rawMarkdown": "\n\n**Score: 0.97258 | Improvement over baseline: +0.00092**\n\n---\n\n## The Core Idea in One Sentence\n\nThe test near-zone fires look structurally different from training near-zone fires — larger, slower, and almost entirely single-perimeter — so I upweight the training fires that most resemble that profile.\n\n---\n\n## Background\n\nMy base model is the standard tri-survival stack (GBSA + CoxPH + RSF + LGB IPCW) that most top solutions use. After reaching 0.97166 with that stack, I spent time on EDA to understand what was left on the table.\n\nThe key finding: the 28 near-zone test fires have a dramatically different feature profile from the 69 near-zone training fires.\n\n| Feature | Train near (mean) | Test near (mean) | Ratio |\n|---------|-------------------|------------------|-------|\n| `closing_speed_abs_m_per_h` | 9.1 | 0.31 | 29× lower |\n| `alignment_abs` | 0.34 | 0.20 | 41% lower |\n| `area_first_ha` | 230 ha | 401 ha | 74% larger |\n| `low_temporal_resolution` | 52% | 64% | More LQ fires |\n\nThe test near-zone fires are slow, large, and poorly observed. The model trained on fast, small, well-observed training fires doesn't generalise well to them.\n\n---\n\n## The Fix: IsolationForest Anomaly Upweighting\n\nThe idea is simple: identify which training fires are most \"anomalous\" (i.e. most similar to the out-of-distribution test fires) and give them more weight during training.\n\n```python\nfrom sklearn.ensemble import IsolationForest\nfrom sklearn.preprocessing import RobustScaler\n\n# Fit IsolationForest on training features\nscaler = RobustScaler()\nX_scaled = scaler.fit_transform(X_gbsa_train.fillna(0))\n\niso = IsolationForest(n_estimators=200, contamination=0.2, random_state=42)\niso.fit(X_scaled)\n\n# conformity score: lower = more anomalous\nconformity = iso.score_samples(X_scaled)\n\n# Map to weights: anomalous fires → 1.0, normal fires → 0.05\nsample_weights = np.interp(conformity,\n (conformity.min(), conformity.max()),\n (1.0, 0.05))\n```\n\nThese weights are then passed to every GBSA and Cox model fit:\n\n```python\nm.fit(X_train.iloc[tr_idx], y_surv[tr_idx],\n sample_weight=sample_weights[tr_idx])\n```\n\nThe logic: the ~44 fires flagged as anomalous (`contamination=0.20`) get full weight (1.0) during training. The remaining ~177 \"normal\" fires get down-weighted to 0.05. This forces the model to prioritise learning the anomalous regime — which is exactly where the test fires live.\n\n---\n\n## Parameter Sweep\n\nI ran a systematic sweep across floor values and contamination levels:\n\n**Floor sweep (contamination=0.10 fixed):**\n\n| Floor | Score |\n|-------|-------|\n| 0.50 | 0.97184 |\n| 0.10 | 0.97240 |\n| 0.07 | 0.97250 |\n| 0.06 | 0.97254 |\n| **0.05** | **0.97256** |\n| 0.03 | 0.97169 |\n| 0.01 | 0.97127 |\n| 0.00 | 0.97085 |\n\nFloor=0.05 is the peak. Below it the model starts ignoring too many training fires and overfits to the anomalous subset.\n\n**Contamination sweep (floor=0.05 fixed):**\n\n| Contamination | Fires upweighted | Score |\n|--------------|-----------------|-------|\n| 0.05 | ~11 | pending |\n| **0.10** | **~22** | **0.97256** |\n| 0.15 | ~33 | 0.97258 |\n| 0.20 | ~44 | 0.97258 |\n| 0.25 | ~55 | 0.97258 |\n\nInteresting: contamination=0.15, 0.20, and 0.25 all land at exactly 0.97258. The signal saturates above ~33 upweighted fires — adding more anomalous fires to the upweighted set doesn't help further.\n\n**This submission** uses `contamination=0.20, floor=0.05` — one of the three configurations that achieved the best score.\n\n---\n\n## What Didn't Work\n\nFor completeness, I tried many other approaches after finding this signal:\n\n- **Applying weights to RSF** — 0.97238, slightly hurt\n- **Perimeter distance features** — 0.96978, hurt\n- **Combining anomaly weights + perimeter features** — 0.97101, hurt\n- **Single config × 80 seeds** — 0.97049, hurt\n- **Config search (leaf=5, sub=0.9)** — 0.96660, badly hurt\n- **CatBoost replacing LGB** — 0.97131, hurt\n\nThe conclusion: anomaly upweighting in GBSA + Cox only is the right application. Adding it to RSF or combining with other changes dilutes the signal.\n\n---\n\n## Key Lessons\n\n**OOF doesn't predict LB at 221 rows.** I had multiple experiments with strong OOF improvement (notably v21 with +0.00053 OOF hybrid) that hurt on the leaderboard. The only reliable signal is the public LB itself.\n\n**The floor matters more than the contamination.** The shape of the weight distribution (how harshly normal fires are downweighted) has more impact than exactly how many fires are flagged as anomalous.\n\n**Structural insights beat tuning.** The single biggest gain in this competition came from the 5km distance gate — a structural insight about the data. Anomaly upweighting is a close second, and it's also fundamentally about data structure (recognising the train/test distribution shift).\n\n---\n\n## Implementation\n\nThe change is minimal — four lines added before the main GBSA training loop:\n\n```python\nfrom sklearn.ensemble import IsolationForest\nfrom sklearn.preprocessing import RobustScaler\n\n_scaler = RobustScaler()\n_X = _scaler.fit_transform(X_gbsa_train.fillna(0))\n_iso = IsolationForest(n_estimators=200, contamination=0.2, random_state=42)\n_iso.fit(_X)\n_conf = _iso.score_samples(_X)\nsample_weights = np.interp(_conf, (_conf.min(), _conf.max()), (1.0, 0.05))\n```\n\nThen pass `sample_weight=sample_weights[tr_idx]` to every `m.fit()` call in the GBSA and Cox loops.\n\nEverything else — blend weights, feature engineering, model configs, seeds — is identical to the standard reference architecture.\n\n---\n\n",
      "votes": 2
    }
  ],
  "complete": true,
  "source": "live",
  "fetched_at": "2026-10-20T08:47:58Z",
  "competition_id": 125681,
  "competition_slug": "WiDSWorldWide_GlobalDathon26",
  "team_id": 15504794,
  "solution_url": "/competitions/WiDSWorldWide_GlobalDathon26/writeups/your-test-fires-dont-look-like-your-training-fire"
}