> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hollerith.monarcha.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Regression and prediction intervals

> Predict a number, and the interval around it

```python theme={null}
from hollerith import Hollerith, read_csv

df = read_csv("wine-quality.csv")
train = df[df["__hollerith_split"] == "train"].drop(columns="__hollerith_split")
score = df[df["__hollerith_split"] == "test"].drop(
    columns=["__hollerith_split", "quality"]
)

reg = Hollerith()
reg.fit(train, target="quality", task="regression")

preds = reg.predict(score)
print(preds[:5])
# [5.62, 6.14, 5.09, 6.77, 5.31]
```

A numeric target and the same two calls. `predict` returns a plain Python list of floats,
row-aligned with the frame you passed in.

`task="regression"` is doing real work in that snippet. Without it, `quality` is read as a
set of classes — the reason is [further down](#a-small-integer-target-becomes-classification).

## Prediction intervals

```python theme={null}
result = reg.predict(score, quantiles=[0.1, 0.5, 0.9])
result.head(2)
#    prediction   0.1   0.5   0.9
# 0        5.62  4.98  5.60  6.31
# 1        6.14  5.41  6.12  6.88
```

Passing `quantiles=` changes the return type. You get a pandas DataFrame with a `prediction`
column, the mean, plus one column per level you asked for.

Levels are yours to choose. `[0.1, 0.5, 0.9]` gives an 80% band; `[0.05, 0.95]` gives a wider
one and a more cautious lower edge.

## The column labels are strings

```python theme={null}
result["0.1"]     # the low column
result[0.1]       # KeyError: 0.1
```

The columns are named from the levels the engine echoes back, and they arrive as strings. A
float key raises `KeyError`, which is the first thing most people hit.

Keep the levels in one place and stringify at the point of use.

```python theme={null}
levels = [0.1, 0.5, 0.9]
low, high = result[str(levels[0])], result[str(levels[-1])]
```

## An interval is a decision, a point is not

A single number tells you what to expect. A band tells you what to commit to, which is
usually the question you actually have.

* **Plan to the median.** `result["0.5"]` is the level to order, staff or forecast against.
* **Buffer with the upper level.** The gap between `0.5` and `0.9` is what a stockout or an
  overrun costs you, priced.
* **Gate on the lower level.** Act only where the pessimistic end still clears your bar.

```python theme={null}
confident = result[result["0.1"] >= 6.0]     # the low end alone clears the bar
```

## wait=False does not give you the DataFrame

`predict(wait=False, quantiles=[...])` returns a raw handle. The DataFrame is assembled after
the wait, so with `wait=False` you assemble it yourself.

```python theme={null}
import pandas as pd

handle = reg.predict(score, wait=False, quantiles=[0.1, 0.5, 0.9])
result = handle.wait()

frame = pd.DataFrame(result.quantiles, columns=[str(q) for q in result.quantile_levels])
frame.insert(0, "prediction", result.predictions)
```

`quantiles=` is regression-only. On a classification task the call raises `ValueError`
before anything is submitted.

## A small-integer target becomes classification

The task is inferred from your target column. An integer target with 20 or fewer distinct
values is treated as classification, whatever you meant by it.

`quality` runs 3 to 8. A units-sold count, a 1-to-5 rating and a 0/1 flag land the same way,
silently, with no warning:

```python theme={null}
reg.fit(train, target="quality")                       # -> classification
reg.fit(train, target="quality", task="regression")    # -> regression
reg.task_
# 'regression'
```

Check `task_` after any `fit` where the target is a whole number. It is the single most
likely surprise in the SDK.

## Evaluation returns RMSE

```python theme={null}
reg.evaluate()
print(reg.evaluation_)
# Evaluation(RMSE=0.62, method=kfold, folds=5, rows=640)
```

Regression returns one metric, `RMSE`, in the units of your target. There is no MAE and no
R² — the engine does not compute them.

At 10,000 rows or fewer the split is k-fold; above it, a single holdout. Both are covered in
[Evaluating accuracy](/guides/evaluating-accuracy).

## When this will not help

* **The target is time-indexed.** Row-wise `predict` has no notion of order — see
  [Forecast](/guides/forecasting).
* **You need calibrated intervals.** The quantiles are the engine's, and nothing here
  guarantees a stated coverage rate on your data.
* **The signal lives in free text.** Text and dates are ordinal-encoded; the model sees
  shared values, not meaning.

## Before you ship

* [ ] `task_` read back after `fit`, not assumed.
* [ ] Quantile columns keyed by string, everywhere.
* [ ] The decision written down as a level, not a point.
* [ ] `evaluate()` run once, on the labeled table you fit on.

## Next

* [Classification](/guides/classification) — labels, probabilities and thresholds
* [Preparing your table](/guides/preparing-your-table) — targets, dtypes and the traps
* [Limits](/reference/limits) — rows, columns and the daily quota
* [Python SDK](/reference/python-sdk) — every argument and return type
