<?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: new_char</title>
    <description>The latest articles on DEV Community by new_char (@cloudlesson95arch).</description>
    <link>https://dev.to/cloudlesson95arch</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%2F4029160%2F5466f5a8-51dd-4855-ab81-1ed6c2ff6a9c.png</url>
      <title>DEV Community: new_char</title>
      <link>https://dev.to/cloudlesson95arch</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cloudlesson95arch"/>
    <language>en</language>
    <item>
      <title>Yes, another tree classifier. Here's what building one from scratch taught me.</title>
      <dc:creator>new_char</dc:creator>
      <pubDate>Tue, 14 Jul 2026 19:45:28 +0000</pubDate>
      <link>https://dev.to/cloudlesson95arch/yes-another-tree-classifier-heres-what-building-one-from-scratch-taught-me-263b</link>
      <guid>https://dev.to/cloudlesson95arch/yes-another-tree-classifier-heres-what-building-one-from-scratch-taught-me-263b</guid>
      <description>&lt;p&gt;I spent way too long building a tabular classifier from scratch. It doesn't&lt;br&gt;
beat XGBoost. I'm publishing it anyway — with honest benchmarks — because&lt;br&gt;
the honest result seems more useful than a drawer full of code.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/cloudlesson95-arch/hypothesis-tree" rel="noopener noreferrer"&gt;https://github.com/cloudlesson95-arch/hypothesis-tree&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;I wanted a tree that makes decisions purely on the input data, where every&lt;br&gt;
decision can be traced back to that data. Not "feature 3 &amp;gt; 0.7 somewhere in&lt;br&gt;
tree #412 of the ensemble" — an actual training row you can print and look&lt;br&gt;
at: &lt;em&gt;this sample matched the cluster anchored on that row, on these&lt;br&gt;
features, within these bounds.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The hope was that finding the proper feature &lt;strong&gt;combinations&lt;/strong&gt; was the key —&lt;br&gt;
that if the model carved multi-feature boxes around groups of errors&lt;br&gt;
instead of splitting one threshold at a time, it could beat the other trees&lt;br&gt;
at their own game.&lt;/p&gt;

&lt;p&gt;So the model is a tree of "clusters". Each cluster is a stored training row&lt;br&gt;
(an exemplar), a mask saying which features it cares about, a tolerance box&lt;br&gt;
around it, and a class label. Prediction is routing: a sample descends the&lt;br&gt;
tree through parent-vs-children competitions, and the deepest matching box&lt;br&gt;
wins. Training only grows the tree where it makes mistakes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;seed the root from the first training row
repeat:
    route every training row through the tree
    zero errors? -&amp;gt; done (with some patience)
    for each cluster that caught errors:
        widen it to own its correct rows
        scan features for ranges where errors concentrate
        AND the ranges into a box, carve the box out as a child
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How it works — one carve, end to end
&lt;/h2&gt;

&lt;p&gt;Easiest way to show it is a real run. Seven rows, two features, class A in&lt;br&gt;
the lower-left, class B to the right:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;row&lt;/th&gt;
&lt;th&gt;x0&lt;/th&gt;
&lt;th&gt;x1&lt;/th&gt;
&lt;th&gt;class&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;A ← seeds the root&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1.5&lt;/td&gt;
&lt;td&gt;2.0&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2.0&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1.2&lt;/td&gt;
&lt;td&gt;1.8&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4.0&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;4.5&lt;/td&gt;
&lt;td&gt;2.0&lt;/td&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;4.2&lt;/td&gt;
&lt;td&gt;1.5&lt;/td&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;epoch 0: errors=3 new_clusters=1 total_clusters=2
epoch 1: errors=0 new_clusters=0 total_clusters=2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;One carve fixed everything. Here's what happened inside epoch 0.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything routes to the root.&lt;/strong&gt; The root is row 0 with infinite&lt;br&gt;
tolerance — it matches anything, so the untrained model predicts A for all&lt;br&gt;
seven rows. The three B rows are errors ("bads"); the four A rows are&lt;br&gt;
"goods". From here on everything works in &lt;em&gt;diff space&lt;/em&gt; — each row expressed&lt;br&gt;
as its offset from the root's anchor &lt;code&gt;[1, 1]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bad diffs  (rows 4-6):  [3.0, 0.0]  [3.5, 1.0]  [3.2, 0.5]
good diffs (rows 0-3):  [0.0, 0.0]  [0.5, 1.0]  [1.0, 0.0]  [0.2, 0.8]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The parent claims its goods.&lt;/strong&gt; Before carving, the root's infinite box&lt;br&gt;
collapses to exactly the envelope of its good diffs — &lt;code&gt;[1, 1]&lt;/code&gt; to &lt;code&gt;[2, 2]&lt;/code&gt;&lt;br&gt;
in absolute terms. The parent now owns precisely the territory it&lt;br&gt;
classifies correctly; the child gets sized against that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which feature separates the errors?&lt;/strong&gt; For each feature, put the bad and&lt;br&gt;
good values on a number line and run a max-subarray scan (Kadane's&lt;br&gt;
algorithm) on the running (bads − goods) count:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;feature&lt;/th&gt;
&lt;th&gt;best range&lt;/th&gt;
&lt;th&gt;bads&lt;/th&gt;
&lt;th&gt;goods&lt;/th&gt;
&lt;th&gt;net&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;x0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;[3.0, 3.5]&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;x1&lt;/td&gt;
&lt;td&gt;[0.5, 0.5]&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;x0 is decisive: all three bad diffs sit far from every good diff. x1 is&lt;br&gt;
nearly useless — its value line interleaves bads and goods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;value:  0.0   0.5   0.8   1.0
bads:    1     1     0     1
goods:   2     0     1     1
score:  -1    +1    -1     0     -&amp;gt; best range [0.5, 0.5], net 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Try to AND features together.&lt;/strong&gt; The family starts as x0's box (net 3),&lt;br&gt;
then tries adding x1: restricted to the family's rows, x1's best range&lt;br&gt;
would keep only 1 bad — net drops from 3 to 1, so x1 is rejected. The child&lt;br&gt;
will care about x0 only. This greedy AND is the whole point of the stage:&lt;br&gt;
when single features are individually weak but jointly strong, the&lt;br&gt;
intersection can reach a positive net that no single feature has. (Here it&lt;br&gt;
simply had nothing to add.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anchor and tolerances.&lt;/strong&gt; The anchor is the actual bad row closest to the&lt;br&gt;
family's median — row 6, &lt;code&gt;[4.2, 1.5]&lt;/code&gt;. Tolerances are the family's extent&lt;br&gt;
around it: &lt;code&gt;-0.2 / +0.3&lt;/code&gt; on x0, so the child's box is &lt;code&gt;[4.0, 4.5]&lt;/code&gt; —&lt;br&gt;
precisely the span of the three B rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A sanity gate.&lt;/strong&gt; Before committing, simulate routing for the parent's&lt;br&gt;
four good rows: would any of them now prefer the child? They sit at 0.0–1.0&lt;br&gt;
on x0, hopelessly far from a box starting at 3.0 — the parent keeps all&lt;br&gt;
four. (If a proposed child would swallow its parent's population, its box&lt;br&gt;
gets collapsed to a point instead.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commit.&lt;/strong&gt; The box holds 3 bads and 0 goods — a clean separation, so the&lt;br&gt;
child takes the bads' label:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cluster 1: anchor=[4.2, 1.5], label=B, mask=[1 0],
           tolerance -0.2/+0.3 on x0, parent=0, confidence 1.00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next epoch, the three B rows descend into the new box and predict B. Zero&lt;br&gt;
errors. And a sample it never saw, &lt;code&gt;[4.1, 1.2]&lt;/code&gt;, routes &lt;code&gt;[0, 1]&lt;/code&gt; → &lt;strong&gt;B&lt;/strong&gt;,&lt;br&gt;
with the explanation being concrete: it matched the cluster anchored on&lt;br&gt;
training row &lt;code&gt;[4.2, 1.5]&lt;/code&gt;, on feature x0, within [-0.2, +0.3].&lt;/p&gt;

&lt;p&gt;The full version of this walkthrough (with the guard rails real datasets&lt;br&gt;
need) is in the repo:&lt;br&gt;
&lt;a href="https://github.com/cloudlesson95-arch/hypothesis-tree/blob/main/docs/how_it_works.md" rel="noopener noreferrer"&gt;https://github.com/cloudlesson95-arch/hypothesis-tree/blob/main/docs/how_it_works.md&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest benchmarks
&lt;/h2&gt;

&lt;p&gt;One note before the tables. Everything below runs the &lt;strong&gt;basic&lt;/strong&gt; version of&lt;br&gt;
the algorithm, on defaults — I kept it that way so the idea stays visible&lt;br&gt;
and the code stays readable (~900 lines of NumPy). There are plenty of&lt;br&gt;
tweaks left on the table (just prompt an AI to improve the score). I tried&lt;br&gt;
a bunch of them in another version of this algorithm — and with every tweak&lt;br&gt;
it started looking closer and closer to the methods that already exist. So&lt;br&gt;
this version stays as close to the original idea as possible.&lt;/p&gt;

&lt;p&gt;5-fold stratified CV, default parameters everywhere, cells are&lt;br&gt;
&lt;strong&gt;accuracy / ROC-AUC / mean fit time&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dataset&lt;/th&gt;
&lt;th&gt;HypothesisTree&lt;/th&gt;
&lt;th&gt;DecisionTree&lt;/th&gt;
&lt;th&gt;RandomForest&lt;/th&gt;
&lt;th&gt;HistGradientBoosting&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;moons (400x2)&lt;/td&gt;
&lt;td&gt;0.843 / 0.842 / 20ms&lt;/td&gt;
&lt;td&gt;0.890 / 0.890 / 1ms&lt;/td&gt;
&lt;td&gt;0.920 / 0.958 / 72ms&lt;/td&gt;
&lt;td&gt;0.915 / 0.961 / 354ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iris (150x4)&lt;/td&gt;
&lt;td&gt;0.933 / 0.950 / 11ms&lt;/td&gt;
&lt;td&gt;0.953 / 0.965 / 1ms&lt;/td&gt;
&lt;td&gt;0.947 / 0.994 / 62ms&lt;/td&gt;
&lt;td&gt;0.940 / 0.986 / 81ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wine (178x13)&lt;/td&gt;
&lt;td&gt;0.826 / 0.866 / 21ms&lt;/td&gt;
&lt;td&gt;0.893 / 0.919 / 1ms&lt;/td&gt;
&lt;td&gt;0.977 / 0.999 / 67ms&lt;/td&gt;
&lt;td&gt;0.966 / 0.998 / 93ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;breast_cancer (569x30)&lt;/td&gt;
&lt;td&gt;0.902 / 0.882 / 91ms&lt;/td&gt;
&lt;td&gt;0.910 / 0.900 / 6ms&lt;/td&gt;
&lt;td&gt;0.956 / 0.989 / 118ms&lt;/td&gt;
&lt;td&gt;0.958 / 0.991 / 128ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And the same protocol over 14 OpenML datasets:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dataset&lt;/th&gt;
&lt;th&gt;HypothesisTree&lt;/th&gt;
&lt;th&gt;DecisionTree&lt;/th&gt;
&lt;th&gt;RandomForest&lt;/th&gt;
&lt;th&gt;HistGradientBoosting&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;banknote (1372x4)&lt;/td&gt;
&lt;td&gt;0.926 / 0.927 / 97ms&lt;/td&gt;
&lt;td&gt;0.983 / 0.983 / 2ms&lt;/td&gt;
&lt;td&gt;0.993 / 1.000 / 123ms&lt;/td&gt;
&lt;td&gt;0.994 / 1.000 / 442ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;blood-transfusion (748x4)&lt;/td&gt;
&lt;td&gt;0.762 / 0.532 / 54ms&lt;/td&gt;
&lt;td&gt;0.710 / 0.573 / 1ms&lt;/td&gt;
&lt;td&gt;0.749 / 0.686 / 84ms&lt;/td&gt;
&lt;td&gt;0.749 / 0.691 / 195ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;diabetes (768x8)&lt;/td&gt;
&lt;td&gt;0.706 / 0.667 / 505ms&lt;/td&gt;
&lt;td&gt;0.700 / 0.672 / 5ms&lt;/td&gt;
&lt;td&gt;0.769 / 0.824 / 293ms&lt;/td&gt;
&lt;td&gt;0.746 / 0.799 / 592ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ionosphere (351x34)&lt;/td&gt;
&lt;td&gt;0.892 / 0.895 / 226ms&lt;/td&gt;
&lt;td&gt;0.897 / 0.889 / 11ms&lt;/td&gt;
&lt;td&gt;0.934 / 0.978 / 290ms&lt;/td&gt;
&lt;td&gt;0.943 / 0.968 / 349ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sonar (208x60)&lt;/td&gt;
&lt;td&gt;0.669 / 0.658 / 352ms&lt;/td&gt;
&lt;td&gt;0.712 / 0.712 / 8ms&lt;/td&gt;
&lt;td&gt;0.827 / 0.927 / 267ms&lt;/td&gt;
&lt;td&gt;0.841 / 0.935 / 207ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vehicle (846x18)&lt;/td&gt;
&lt;td&gt;0.609 / 0.741 / 1.0s&lt;/td&gt;
&lt;td&gt;0.692 / 0.795 / 10ms&lt;/td&gt;
&lt;td&gt;0.733 / 0.929 / 340ms&lt;/td&gt;
&lt;td&gt;0.771 / 0.928 / 2.4s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qsar-biodeg (1055x41)&lt;/td&gt;
&lt;td&gt;0.808 / 0.743 / 1.9s&lt;/td&gt;
&lt;td&gt;0.817 / 0.797 / 22ms&lt;/td&gt;
&lt;td&gt;0.871 / 0.935 / 455ms&lt;/td&gt;
&lt;td&gt;0.882 / 0.936 / 892ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kc1 (2109x21)&lt;/td&gt;
&lt;td&gt;0.850 / 0.594 / 907ms&lt;/td&gt;
&lt;td&gt;0.814 / 0.609 / 9ms&lt;/td&gt;
&lt;td&gt;0.861 / 0.825 / 188ms&lt;/td&gt;
&lt;td&gt;0.857 / 0.776 / 164ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pc1 (1109x21)&lt;/td&gt;
&lt;td&gt;0.924 / 0.610 / 284ms&lt;/td&gt;
&lt;td&gt;0.910 / 0.675 / 5ms&lt;/td&gt;
&lt;td&gt;0.937 / 0.848 / 127ms&lt;/td&gt;
&lt;td&gt;0.930 / 0.833 / 149ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;steel-plates-fault (1941x33)&lt;/td&gt;
&lt;td&gt;0.853 / 0.792 / 1.6s&lt;/td&gt;
&lt;td&gt;1.000 / 1.000 / 9ms&lt;/td&gt;
&lt;td&gt;0.993 / 1.000 / 237ms&lt;/td&gt;
&lt;td&gt;1.000 / 1.000 / 91ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;climate-crashes (540x20)&lt;/td&gt;
&lt;td&gt;0.896 / 0.510 / 126ms&lt;/td&gt;
&lt;td&gt;0.881 / 0.620 / 4ms&lt;/td&gt;
&lt;td&gt;0.917 / 0.813 / 106ms&lt;/td&gt;
&lt;td&gt;0.906 / 0.844 / 101ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;segment (2310x18)&lt;/td&gt;
&lt;td&gt;0.913 / 0.951 / 350ms&lt;/td&gt;
&lt;td&gt;0.956 / 0.974 / 11ms&lt;/td&gt;
&lt;td&gt;0.972 / 0.998 / 240ms&lt;/td&gt;
&lt;td&gt;0.980 / 0.999 / 755ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wilt (4839x5)&lt;/td&gt;
&lt;td&gt;0.938 / 0.530 / 2.2s&lt;/td&gt;
&lt;td&gt;0.977 / 0.885 / 9ms&lt;/td&gt;
&lt;td&gt;0.982 / 0.989 / 349ms&lt;/td&gt;
&lt;td&gt;0.984 / 0.986 / 166ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;phoneme (5404x5)&lt;/td&gt;
&lt;td&gt;0.775 / 0.715 / 3.7s&lt;/td&gt;
&lt;td&gt;0.872 / 0.843 / 18ms&lt;/td&gt;
&lt;td&gt;0.910 / 0.961 / 570ms&lt;/td&gt;
&lt;td&gt;0.896 / 0.952 / 156ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Mean accuracy rank (1 = best): HistGradientBoosting &lt;strong&gt;1.46&lt;/strong&gt;, RandomForest&lt;br&gt;
&lt;strong&gt;1.79&lt;/strong&gt;, DecisionTree &lt;strong&gt;3.25&lt;/strong&gt;, HypothesisTree &lt;strong&gt;3.50&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Reading guide: accuracy lands in single-decision-tree territory (it beats&lt;br&gt;
the tree outright on 5 of 14 and is best of all four models on exactly one&lt;br&gt;
dataset, blood-transfusion). The ensembles win, as they do against nearly&lt;br&gt;
everything on tabular data. On imbalanced datasets accuracy holds up but&lt;br&gt;
AUC collapses toward 0.5 — probabilities come from per-cluster confidence,&lt;br&gt;
and most clusters saturate at 1.0, so there's almost no ranking signal.&lt;br&gt;
That one is the weakest part of the model, and it's documented in the&lt;br&gt;
README rather than hidden.&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A hyperparameter I designed, tuned, and then proved does nothing&lt;/strong&gt; (as&lt;br&gt;
probably most of them). The match score is &lt;code&gt;exp(-d/softness)&lt;/code&gt; — which is&lt;br&gt;
monotonic in &lt;code&gt;d&lt;/code&gt;, so within any competition the ranking never changes,&lt;br&gt;
no matter what softness is. I tuned that knob more than once before&lt;br&gt;
noticing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;I tried to make a general learning algorithm — I made a tree.&lt;/strong&gt; Boxes&lt;br&gt;
in feature space, growth driven by errors, parent-child structure...&lt;br&gt;
every design decision that worked pulled the thing closer to the shape&lt;br&gt;
of the methods I was trying to out-do. There's probably a lesson in&lt;br&gt;
there about why trees keep winning on tabular data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The tree still overfits to noise, and a neural net handles it much&lt;br&gt;
better (what a real shock)&lt;/strong&gt; Error-driven carving means every noisy&lt;br&gt;
point eventually earns its own little box if you let it — you can see it&lt;br&gt;
on the moons dataset (noise=0.25), where my model drops below even a&lt;br&gt;
plain decision tree while smoother models shrug.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Was it worth it?
&lt;/h2&gt;

&lt;p&gt;I started this when nobody around cared about AI, and I genuinely hoped it would beat the established methods. It didn't, and watching the benchmark table say so, fold after fold, was not a great time.&lt;/p&gt;

&lt;p&gt;But the gains don't fit in a table. I can derive every decision this model makes from first principles. I learned to benchmark honestly instead of hopefully.&lt;/p&gt;

&lt;p&gt;And where a drawer full of code used to be, there's now a tested, documented repo with benchmark tables I don't have to apologize for. That trade I'd take again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;I'm probably done with the current tree. Maybe PyPI, if anyone cares.&lt;/p&gt;

&lt;p&gt;The next thing I want to try is merging MLPs and trees — trees are fast and&lt;br&gt;
well-optimized, neural nets deal with noise and unstructured data, and&lt;br&gt;
surprise #3 suggests they'd cover each other's blind spots.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/cloudlesson95-arch/hypothesis-tree" rel="noopener noreferrer"&gt;https://github.com/cloudlesson95-arch/hypothesis-tree&lt;/a&gt; — issues,&lt;br&gt;
benchmarks disputes, and pointers to related work I've missed (RCE networks&lt;br&gt;
and PRIM box-hunting are the closest relatives I found) are all welcome.&lt;/p&gt;

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