Checklist OAI 2026 Stage II · Task 3
SOTA-AI Editorial
Decision Trees
Automatically preprocess two-dimensional datasets and choose decision-tree hyperparameters so that the tree beats the organisers' baseline accuracy on as many of 100 hidden datasets as possible.
Hints
Hint 1
Plot each dataset before choosing a tree strategy; the task is a collection of different geometries, not one shared distribution.
Hint 2
Check feature scale, covariance, and orientation. Can a data-driven rotation make the tree’s axis-aligned splits more useful?
Hint 3
Use the unlabeled test points to stabilize transductive feature preprocessing, while keeping their labels out of training and model selection.
Hint 4
Select pruning strength with stratified CV, and be cautious about tiny CV-score differences on small datasets.
The solution in one line
Symmetrically whiten pooled features, rotate them to reduce 24-bin histogram-estimated mutual information, then select pruning by repeated stratified CV with a prior based on the CV-best alpha; measured result: 66.0/100 on released collection A.
The editorial
Poland 2026 Stage 2 — Drzewa decyzyjne
Problem name: Drzewa decyzyjne (Decision Trees)
Problem domain: Classical ML, geometric preprocessing, model selection
Evaluation metric: Number of datasets on which a decision tree matches or beats the supplied baseline accuracy; 1.2 points per dataset, capped at 100
Abridged statement: For each of many independent two-dimensional binary classification datasets, preprocess the labeled training points and unlabeled test points, choose decision-tree hyperparameters, and predict the test labels. The released collection A is available for development; collection B is used for the hidden evaluation.
Dataset analysis and EDA
Collection A contains 100 separate datasets. Each has two numeric features and binary labels. Training-set sizes range from 74 to 2,043 points (median 677.5); the minority class is 25%–50% of each training set. The supplied baseline accuracies range from 0.572 to 0.976, with a median of 0.933.
Scatterplots show substantial variation in scale, orientation, and class geometry. Examples include diagonal bands, clusters, and overlapping or noisy boundaries. Many patterns are poorly aligned with the original feature axes, so a tree that only makes axis-aligned splits in raw coordinates can underperform. The two features remain the full input; preprocessing must preserve both matrix shapes.
EDA performed: point-cloud scatterplots, class-balance and sample-size summaries, feature covariance/eigenvalue inspection, and score comparisons for raw coordinates, whitening, ICA-style rotations, PCA, polar and polynomial maps, and spectral embeddings.
Experiments
| Approach | Collection A result | Notes |
|---|---|---|
| Starter notebook: pooled whitening, moment-based ICA rotation, repeated-CV pruning | 49/100 wins; 58.8 points; 88.54% mean accuracy | Reproduced from the released notebook implementation. |
| Train-only, 16-bin mutual-information rotation with the original CV policy | 51/100 wins; 61.2 points; 88.68% mean accuracy | Improved over the starter, but later replaced. |
| Pooled, 24-bin mutual-information rotation; global alpha prior | 54/100 wins; 64.8 points | CV tolerance 0.001 and fixed prior 0.0015. |
| Pooled, 24-bin rotation; prior conditioned on training-only best-CV alpha | 55/100 wins; 66.0 points; 88.70% mean accuracy | Selected implementation below. |
| Pooled, 16-bin mutual-information rotation with repeated-CV pruning | 55/100 wins; 66.0 points | Tied the selected 24-bin method; did not improve it. |
| Pooled, 20-bin mutual-information rotation with repeated-CV pruning | 51/100 wins; 61.2 points | Tested as a bin-count sensitivity check. |
Pooled, 16-bin rotation with one fixed ccp_alpha=0.00225 |
50/100 wins; 60.0 points | Fixed alpha performed worse than cross-validated pruning. |
k-nearest-neighbor mutual-information rotation (k=5, 256-point pooled subsample, 1° search) with repeated-CV pruning |
45/100 wins; 54.0 points; 88.40% mean accuracy | More flexible dependence estimate, but worse on collection A. |
| Expanded CV grid over pruning strength and minimum leaf size | 48/100 wins; 57.6 points | More hyperparameter choices did not improve the released A score. |
| Spectral embedding, sample of 34 datasets | 0/34 wins with 8 or 12 neighbors; 2/34 with 20 neighbors | Tested and discarded; this is a subset result, not a full-A score. |
| PCA, polar, and simple polynomial coordinate maps | Did not improve the selected approach | Tested as alternatives; not used in the final pipeline. |
The final preprocessing rotation minimizes a discretized mutual-information estimate between the two whitened projections. It uses 24 equal-frequency bins per projection and searches rotations from 0° through 179°. Both whitening and the rotation objective use pooled train and test features without labels; the same transform is applied to both arrays.
The tree searches ccp_alpha values from 0.0015 to 0.0045 by repeated stratified cross-validation (up to four folds, two repeats). It finds the best mean CV alpha, then sets one global prior: 0.0015 when the CV-best alpha is at most 0.003, otherwise 0.00375. Among candidates within 0.1 percentage points of the best mean CV accuracy, it chooses the alpha closest to that prior. This condition uses only the current dataset’s training labels and CV scores.
Solution and validation
Implementation: decision_trees.py
On the 100 released A datasets, the final functions beat or match baseline_acc on 55 datasets, for 66.0/100 points. Full credit requires at least 84 wins, so the best tested rule is 29 datasets short. Mean test accuracy is 88.70%. The official shape assertions passed: preprocessed training and test arrays retain their input shapes.
Each dataset’s tree hyperparameters are chosen using only its training labels and stratified cross-validation. The unlabeled test features are used only with training features to estimate the shared whitening transform and rotation. The released A output labels were used to compare global solution variants during development, so 66.0/100 is a development/validation score, not an untouched estimate of collection B performance. Per-dataset oracle choices made after inspecting test labels were excluded from the final code and score. A diagnostic leave-one-family-out assessment of the prior policy scored 54/100 (64.8 points); family IDs were used only to create that diagnostic split, never by the solution.
Full credit was not reached. Collection B is hidden and its score is unknown. An independent search compared raw, PCA, whitening, histogram-MI, moment-based, polar, and kNN-MI transforms with global and CV-selected pruning. None exceeded 66.0/100. The high and varied baseline accuracies suggest the selected single-tree interface cannot match every geometry with one simple preprocessing strategy.
Compute and footprint
The solution uses CPU NumPy and scikit-learn only; it requires no GPU or external model. Preprocessing searches 180 two-dimensional rotations per dataset, followed by small decision-tree CV fits. It preserves the task’s two-feature input and output contract.
Code
Solution code
"""Decision-tree solution for the Polish 2026 Stage 2 task.
The input consists of many independent two-feature binary classification
problems. We use the unlabeled test points to estimate a stable whitening
transform, rotate the axes to reduce dependence between the training
projections, then choose the tree pruning strength by repeated stratified CV.
"""
from __future__ import annotations
import numpy as np
from sklearn.model_selection import RepeatedStratifiedKFold
from sklearn.tree import DecisionTreeClassifier
def preprocess_data(X_train: np.ndarray, X_test: np.ndarray):
"""Whiten pooled features and choose a low-mutual-information rotation.
The histogram objective is estimated from the pooled, unlabeled point
cloud. Both train and test points share the resulting transform. The function keeps
the required two-column shape and has a safe identity fallback for invalid
or unexpected inputs.
"""
X_train = np.asarray(X_train, dtype=np.float64)
X_test = np.asarray(X_test, dtype=np.float64)
if (
X_train.ndim != 2
or X_test.ndim != 2
or X_train.shape[1] != 2
or X_test.shape[1] != 2
or len(X_train) < 2
or len(X_test) == 0
):
return X_train.copy(), X_test.copy()
pooled = np.concatenate((X_train, X_test), axis=0)
if not np.isfinite(pooled).all():
return X_train.copy(), X_test.copy()
center = pooled.mean(axis=0)
covariance = np.cov(pooled, rowvar=False)
eigenvalues, eigenvectors = np.linalg.eigh(covariance)
floor = max(float(eigenvalues.max()), 1.0) * 1e-12
# Symmetric whitening preserves the original coordinate frame while
# correcting for scale and correlation.
whitening = (
eigenvectors
@ np.diag(1.0 / np.sqrt(np.maximum(eigenvalues, floor)))
@ eigenvectors.T
)
Z = (pooled - center) @ whitening
n_train = len(X_train)
angles = np.deg2rad(np.arange(180, dtype=np.float64))
cosines = np.cos(angles)
sines = np.sin(angles)
first = Z[:, 0, None] * cosines + Z[:, 1, None] * sines
second = -Z[:, 0, None] * sines + Z[:, 1, None] * cosines
# Estimate mutual information from 24 equal-frequency bins on each axis.
# The small pseudocount makes the estimate finite for sparse histograms.
mutual_information = np.empty(len(angles), dtype=np.float64)
quantiles = np.linspace(0.0, 1.0, 25)
for j in range(len(angles)):
edges_1 = np.unique(np.quantile(first[:, j], quantiles))
edges_2 = np.unique(np.quantile(second[:, j], quantiles))
if len(edges_1) < 2 or len(edges_2) < 2:
mutual_information[j] = np.inf
continue
counts = np.histogram2d(
first[:, j], second[:, j], bins=(edges_1, edges_2)
)[0]
counts += 1e-6
joint = counts / counts.sum()
marginal_1 = joint.sum(axis=1, keepdims=True)
marginal_2 = joint.sum(axis=0, keepdims=True)
mutual_information[j] = np.sum(
joint * np.log(joint / (marginal_1 @ marginal_2))
)
best = int(np.argmin(mutual_information))
c, s = cosines[best], sines[best]
rotation = np.array([[c, -s], [s, c]])
Z = Z @ rotation
return Z[:n_train], Z[n_train:]
def get_decision_tree_hyperparameters(X_train: np.ndarray, y_train: np.ndarray):
"""Select cost-complexity pruning strength by repeated stratified CV."""
X_train = np.asarray(X_train, dtype=np.float64)
y_train = np.asarray(y_train).ravel()
classes, counts = np.unique(y_train, return_counts=True)
fallback_alpha = 0.0015
if len(classes) < 2:
return {"ccp_alpha": fallback_alpha, "random_state": 42}
n_splits = min(4, int(counts.min()))
if n_splits < 2:
return {"ccp_alpha": fallback_alpha, "random_state": 42}
alphas = np.round(0.0015 + 0.00025 * np.arange(13), 5)
cv = RepeatedStratifiedKFold(
n_splits=n_splits, n_repeats=2, random_state=1741
)
folds = list(cv.split(X_train, y_train))
scores = np.zeros((len(alphas), len(folds)), dtype=np.float64)
for alpha_index, alpha in enumerate(alphas):
for fold_index, (train_index, valid_index) in enumerate(folds):
tree = DecisionTreeClassifier(
ccp_alpha=float(alpha), random_state=42
)
tree.fit(X_train[train_index], y_train[train_index])
scores[alpha_index, fold_index] = tree.score(
X_train[valid_index], y_train[valid_index]
)
mean_scores = scores.mean(axis=1)
best_score = float(mean_scores.max())
# The selected global policy uses a prior conditioned on the best CV alpha.
# Both values are fixed across tasks; the condition depends only on this
# dataset's training labels and repeated-CV scores.
best_index = int(np.argmax(mean_scores))
cv_best_alpha = float(alphas[best_index])
prior_alpha = 0.0015 if cv_best_alpha <= 0.003 else 0.00375
# Retain near-best candidates because individual CV estimates are noisy.
eligible = np.flatnonzero(mean_scores >= best_score - 0.001)
selected = min(
eligible, key=lambda index: abs(float(alphas[index]) - prior_alpha)
)
return {"ccp_alpha": float(alphas[selected]), "random_state": 42}
def fit_tree(X_train: np.ndarray, y_train: np.ndarray) -> DecisionTreeClassifier:
"""Convenience helper for local use of the two task functions above."""
params = get_decision_tree_hyperparameters(X_train, y_train)
return DecisionTreeClassifier(**params).fit(X_train, y_train)
Editorials are written by members of the SOTA community, not by the olympiad. The official statement and scoring win wherever an editorial differs.