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).

Quickstart

Pass discretize= and predictions are mapped onto the target’s level lattice instead, so 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: 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").

Configuration

Both are constructor parameters and predict kwargs. A kwarg you actually pass wins for that call, but None means “unset”, not “off” — so predict(X, discretize=None) falls back to the constructor’s strategy rather than turning snapping off. Construct without discretize= if you want it off.

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. Custom bar_distribution checkpoints have no bank, so they are limited to the strategies that read only the point prediction: snap-mean, snap-median, and prior-match.
  • Hosted API. Remote requests return point predictions only, so SynthefyNoriClient supports discretize="snap-mean" and nothing else. The other strategies need mode="local".
  • 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(model="nori-30m", discrete_y_snap_max_unique=30).

Next steps

Missing Values & Imputation

Pass NaNs straight through, and take control of the fill when you want it.

Explainability

Which features drove the label the model chose.