Skip to main content
Some regression targets only ever take a handful of values: a 1–5 star rating, a wine-quality score of 3–8, a count of rooms. Nori still treats these as regression — it predicts a full distribution over the real line — but by default predict returns the distribution’s mean, which almost never lands exactly on one of your levels (you ask for a rating and get 3.87). Pass discretize= and predictions are mapped onto the target’s level lattice instead — every returned value is one your target can actually take:
Discretization is strictly opt-in: nothing is snapped unless you pass discretize= (a strategy) or categorical_levels= (a known level set). There is no separate flag — either argument activates the feature, and categorical_levels alone uses the default strategy ("map-cell").

Choosing a strategy

Nori emits a full predictive distribution per row, and discretization asks: which single lattice point best summarizes it? The right answer depends on the metric you are scored on — the mode wins on accuracy, the median on MAE, the mean on quadratic penalties. Pick accordingly:
discretizeWhat it isUse it for
"map-cell" (default)mode of the discrete posterior: integrate the distribution’s mass in a cell around each level, take the argmaxaccuracy, macro-F1
"median-cell"median of that discrete posterior (first level where cumulative cell mass crosses 0.5)MAE / ordinal closeness
"snap-mean"nearest level to the point meanquadratically-penalized agreement (QWK)
"snap-median"nearest level to the distribution medianMAE, with custom bar_distribution checkpoints
"expected-level"Σ level·P(level) — a lattice-informed continuous expectation, deliberately off-latticeanalysis / sanity checks (≈ the plain mean)
"prior-match"rank rows by point prediction, assign labels so predicted frequencies match training priorscalibration experiments only — benchmarked significantly worse; prefer map-cell / median-cell
On a 13-dataset benchmark of discrete-target regression tasks (K ≤ 10 levels, drawn from TALENT / OpenML-CTR23 / TabArena), the ordering above is exactly what shows up: map-cell is the accuracy leader (0.755 vs 0.707 for snapping the mean), median-cell the MAE leader, and snap-mean the QWK leader. The gains are largest on skewed targets, where the mean falls between levels or on a low-probability one — up to +48pp accuracy over mean-snapping on the most skewed dataset.
If your task is scored by squared error or R², do not discretize. The continuous mean is already optimal for those metrics, and any projection onto the lattice trades R² away. discretize= is for when you need labels, not a better regression score.

Declaring the level set: categorical_levels

categorical_levels is the set of values the target can take — the label set, in classification terms. By default it is inferred from the distinct values of the y you fit on, which is leak-safe. Pass it explicitly when your context is small and may under-cover the true scale — for example a 1–5 rating whose context happens to contain no 1s:
Every predicted label is guaranteed to be one of these levels. The values must be numeric and order-significant (the cell-based strategies build probability cells at the midpoints between adjacent values) — hence levels, ordinal terminology, rather than labels. String or unordered class labels are out of scope: Nori is a regressor, not a classifier.

Works with the sklearn ecosystem

discretize and categorical_levels are also estimator parameters, so clone, cross_val_score, and GridSearchCV reach them; predict kwargs override the constructor per call:
The one-shot helper accepts the same arguments: infer(X_train, y_train, X_test, discretize="map-cell").

Good to know

  • Failed predictions stay visible. A NaN point prediction stays NaN after discretization rather than becoming a confident label.
  • Checkpoints. The default Hugging Face checkpoints expose the full quantile bank, so every strategy works out of the box. Only custom bar_distribution checkpoints are limited to the snap-* strategies.
  • Programmatic strategy list. The strategy names and one-line descriptions are importable — synthefy_nori.discretize.DISCRETIZE_METHODS / DISCRETIZE_METHOD_DESCRIPTIONS — for building CLIs or UIs on top. The underlying lattice math (cell_masses, discretize_predictions, snap_to_levels) is public too.
Upgrading from an older release? Earlier versions silently snapped point predictions whenever the target had ≤ 30 distinct values. That implicit behavior is now off by default — snapping only happens when you ask for it. Low-cardinality pipelines may see small numeric shifts (typically R² improves). To restore the legacy behavior, pass NoriRegressor(discrete_y_snap_max_unique=30).