I spent way too long building a tabular classifier from scratch. It doesn't
beat XGBoost. I'm publishing it anyway — with honest benchmarks — because
the honest result seems more useful than a drawer full of code.
Repo:
The honest benchmarks
One note before the tables. Everything below runs the basic version of
the algorithm, on defaults — I kept it that way so the idea stays visible
and the code stays readable (~900 lines of NumPy). There are plenty of
tweaks left on the table (just prompt an AI to improve the score). I tried
a bunch of them in another version of this algorithm — and with every tweak
it started looking closer and closer to the methods that already exist. So
this version stays as close to the original idea as possible.
5-fold stratified CV, default parameters everywhere, cells are
accuracy / ROC-AUC / mean fit time.
| Dataset | HypothesisTree | DecisionTree | RandomForest | HistGradientBoosting |
|---|---|---|---|---|
| moons (400x2) | 0.843 / 0.842 / 20ms | 0.890 / 0.890 / 1ms | 0.920 / 0.958 / 72ms | 0.915 / 0.961 / 354ms |
| iris (150x4) | 0.933 / 0.950 / 11ms | 0.953 / 0.965 / 1ms | 0.947 / 0.994 / 62ms | 0.940 / 0.986 / 81ms |
| wine (178x13) | 0.826 / 0.866 / 21ms | 0.893 / 0.919 / 1ms | 0.977 / 0.999 / 67ms | 0.966 / 0.998 / 93ms |
| breast_cancer (569x30) | 0.902 / 0.882 / 91ms | 0.910 / 0.900 / 6ms | 0.956 / 0.989 / 118ms | 0.958 / 0.991 / 128ms |
And the same protocol over 14 OpenML datasets:
| Dataset | HypothesisTree | DecisionTree | RandomForest | HistGradientBoosting |
|---|---|---|---|---|
| banknote (1372x4) | 0.926 / 0.927 / 97ms | 0.983 / 0.983 / 2ms | 0.993 / 1.000 / 123ms | 0.994 / 1.000 / 442ms |
| blood-transfusion (748x4) | 0.762 / 0.532 / 54ms | 0.710 / 0.573 / 1ms | 0.749 / 0.686 / 84ms | 0.749 / 0.691 / 195ms |
| diabetes (768x8) | 0.706 / 0.667 / 505ms | 0.700 / 0.672 / 5ms | 0.769 / 0.824 / 293ms | 0.746 / 0.799 / 592ms |
| ionosphere (351x34) | 0.892 / 0.895 / 226ms | 0.897 / 0.889 / 11ms | 0.934 / 0.978 / 290ms | 0.943 / 0.968 / 349ms |
| sonar (208x60) | 0.669 / 0.658 / 352ms | 0.712 / 0.712 / 8ms | 0.827 / 0.927 / 267ms | 0.841 / 0.935 / 207ms |
| vehicle (846x18) | 0.609 / 0.741 / 1.0s | 0.692 / 0.795 / 10ms | 0.733 / 0.929 / 340ms | 0.771 / 0.928 / 2.4s |
| qsar-biodeg (1055x41) | 0.808 / 0.743 / 1.9s | 0.817 / 0.797 / 22ms | 0.871 / 0.935 / 455ms | 0.882 / 0.936 / 892ms |
| kc1 (2109x21) | 0.850 / 0.594 / 907ms | 0.814 / 0.609 / 9ms | 0.861 / 0.825 / 188ms | 0.857 / 0.776 / 164ms |
| pc1 (1109x21) | 0.924 / 0.610 / 284ms | 0.910 / 0.675 / 5ms | 0.937 / 0.848 / 127ms | 0.930 / 0.833 / 149ms |
| steel-plates-fault (1941x33) | 0.853 / 0.792 / 1.6s | 1.000 / 1.000 / 9ms | 0.993 / 1.000 / 237ms | 1.000 / 1.000 / 91ms |
| climate-crashes (540x20) | 0.896 / 0.510 / 126ms | 0.881 / 0.620 / 4ms | 0.917 / 0.813 / 106ms | 0.906 / 0.844 / 101ms |
| segment (2310x18) | 0.913 / 0.951 / 350ms | 0.956 / 0.974 / 11ms | 0.972 / 0.998 / 240ms | 0.980 / 0.999 / 755ms |
| wilt (4839x5) | 0.938 / 0.530 / 2.2s | 0.977 / 0.885 / 9ms | 0.982 / 0.989 / 349ms | 0.984 / 0.986 / 166ms |
| phoneme (5404x5) | 0.775 / 0.715 / 3.7s | 0.872 / 0.843 / 18ms | 0.910 / 0.961 / 570ms | 0.896 / 0.952 / 156ms |
Mean accuracy rank (1 = best): HistGradientBoosting 1.46, RandomForest
1.79, DecisionTree 3.25, HypothesisTree 3.50.
Reading guide: accuracy lands in single-decision-tree territory (it beats
the tree outright on 5 of 14 and is best of all four models on exactly one
dataset, blood-transfusion). The ensembles win, as they do against nearly
everything on tabular data. On imbalanced datasets accuracy holds up but
AUC collapses toward 0.5 — probabilities come from per-cluster confidence,
and most clusters saturate at 1.0, so there's almost no ranking signal.
That one is the weakest part of the model, and it's documented in the
README rather than hidden.
What surprised me
A hyperparameter I designed, tuned, and then proved does nothing (as
probably most of them). The match score isexp(-d/softness)— which is
monotonic ind, so within any competition the ranking never changes,
no matter what softness is. I tuned that knob more than once before
noticing.I tried to make a general learning algorithm — I made a tree. Boxes
in feature space, growth driven by errors, parent-child structure...
every design decision that worked pulled the thing closer to the shape
of the methods I was trying to out-do. There's probably a lesson in
there about why trees keep winning on tabular data.The tree still overfits to noise, and a neural net handles it much
better (what a real shock) Error-driven carving means every noisy
point eventually earns its own little box if you let it — you can see it
on the moons dataset (noise=0.25), where my model drops below even a
plain decision tree while smoother models shrug.
Was it worth it?
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.
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.
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.
What's next
I'm probably done with the current tree. Maybe PyPI, if anyone cares.
The next thing I want to try is merging MLPs and trees — trees are fast and
well-optimized, neural nets deal with noise and unstructured data, and
surprise #3 suggests they'd cover each other's blind spots.
Repo: https://github.com/cloudlesson95-arch/hypothesis-tree — issues,
benchmarks disputes, and pointers to related work I've missed (RCE networks
and PRIM box-hunting are the closest relatives I found) are all welcome.
SOCIAL SHARE CARD GENERATOR