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

# Classification

> Predict a label from a table

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

df = read_csv("credit-card-fraud.csv")
train = df[df["__hollerith_split"] == "train"].drop(columns="__hollerith_split")
score = df[df["__hollerith_split"] == "test"].drop(
    columns=["__hollerith_split", "risk_label"]
)

clf = Hollerith()
clf.fit(train, target="risk_label")

preds = clf.predict(score)
print(preds[:5])
# [0, 0, 1, 0, 0]
```

One labeled table, one column to predict. `fit` stages the table, `predict` scores new rows
against it.

`predict` returns a plain Python list, row-aligned with the frame you passed in. Not an
array and not a Series — slice it, iterate it, or zip it with your row ids.

`risk_label` holds `0` and `1`, so the labels come back as integers. A whole-number target
with 20 or fewer distinct values is read as classification, which is what you want here.
See [Preparing your table](/guides/preparing-your-table) for when that inference is wrong.

## Multi-class is the same call

Nothing about the call changes when the target holds three labels instead of two. The
distinct values in your target column are the only difference.

```python theme={null}
flowers = Hollerith().fit(iris, target="species")
flowers.classes_
# ['setosa', 'versicolor', 'virginica']
```

`classes_` is the sorted set of labels seen at `fit` time, and exists for classification
only. A client resumed with `Hollerith.from_fitted_context()` has none until `predict_proba`
runs.

## Class probabilities

```python theme={null}
proba = clf.predict_proba(score)
proba.shape
# (200, 2)
clf.classes_
# [0, 1]

fraud_score = proba[:, clf.classes_.index(1)]
```

A NumPy array of float64, one row per input row and one column per class, ordered to match
`classes_`. Index columns through `classes_` rather than by a position you assumed.

Reach for probabilities when a label is not enough. The fraud sample is 10% positive, and a
label collapses an imbalanced call into a threshold somebody else picked for you.

## predict\_proba rewrites classes\_

`predict_proba` overwrites `classes_` with the ordering the server returned. Read the
attribute again after the call instead of trusting an order you captured before it.

```python theme={null}
order = clf.classes_              # stale the moment predict_proba returns
proba = clf.predict_proba(score)
col = clf.classes_.index(1)       # re-read, then index
```

## Picking an operating point

```python theme={null}
import numpy as np

flagged = fraud_score > 0.2                 # a threshold you own
queue = np.argsort(-fraud_score)[:50]       # or rank, and take the top 50
```

A cutoff of 0.5 is a choice, not a default worth keeping. Move it down when a missed fraud
costs more than a false alarm, and up when reviewer time is the scarce thing.

Ranking avoids the question. If a team can work fifty cases a day, send the fifty
highest-scoring rows and let the cutoff land where it lands.

## The ceiling is 160 classes

`fit` counts the distinct values in your target before anything is uploaded. Past 160 it
raises a `ValidationError` with code `dataset_too_large`, and no job is submitted.

There is no automatic fallback. Nothing is grouped, bucketed or truncated for you, so a
target with 400 product codes has to become a smaller target first:

* **Group the tail.** Keep the labels that carry volume, fold the rest into one `other`.
* **Predict a coarser level.** Category rather than product code, region rather than store.
* **Split the problem.** One model per segment, each with a target inside the ceiling.

## When the task is inferred wrong

Hollerith reads the task from your target column. A non-numeric or boolean target is
classification, and so is an integer target with 20 or fewer distinct values.

Integer class ids above that count are read as magnitudes instead. Pass `task=` whenever the
inference goes the wrong way.

```python theme={null}
clf.fit(df, target="segment_id", task="classification")   # 40 ids, not a quantity
clf.task_
# 'classification'
```

## Good candidates

A column that already exists in your table, and rows that look like the rows you will score:

* **Mixed-type, wide rows.** Numbers, categories and missing values are read as they are.
* **Imbalanced targets.** `predict_proba` plus a threshold you set beats a fixed label.
* **Tables that move weekly.** Re-fitting is one call, so drift is cheaper to fix than to detect.
* **Small labeled sets.** A few hundred labeled rows is a working starting point.

## When this will not help

* **The signal lives in free text.** Text and dates are ordinal-encoded, so the model sees
  that two rows share a value, not what the value means.
* **The target is time-indexed.** Row-wise `predict` has no notion of order — see
  [Forecast](/guides/forecasting).
* **More than 160 classes.** The call fails rather than degrading.
* **A pipeline you have already tuned.** A gradient boosting model somebody spent weeks on
  may still win on one stable table. Measure both before you move.

## Before you ship

* [ ] `task_` is `classification`, not an inference you did not intend.
* [ ] `classes_` re-read after any `predict_proba` call.
* [ ] The threshold written down, with the cost that chose it.
* [ ] [`evaluate()`](/guides/evaluating-accuracy) run once on the labeled table you fit on.
* [ ] `fitted_context_expires_at_` checked if you predict on a schedule.

## Next

* [Evaluating accuracy](/guides/evaluating-accuracy) — accuracy is the one metric returned
* [The fitted context](/concepts/fitted-context) — fit once, predict many
* [Regression and prediction intervals](/guides/regression) — the numeric-target sibling
* [Improving accuracy](/guides/improving-accuracy) — when the first number disappoints
* [Preparing your table](/guides/preparing-your-table) — targets, dtypes and the traps
* [Limits](/reference/limits) — rows, columns, classes and the daily quota
