Ordinal encoding
Ordinal encoding replaces each category with a rank, so a column of n ordered categories becomes one numeric column instead of n indicator columns.
Last updated: 12 Oct, 2026 · pandas 3.0 · scikit-learn 1.9
One hot encoding with many categories cut the column count by grouping rare levels. An ordered column can skip the whole problem: one column holds the order, and the order is the information.
The clip defines the technique and gives three examples of an order worth encoding: grades, a batsman's years of experience and the days of the week.
Syntax: a dictionary and Series.map, or scikit-learn's transformer with the order spelled out.
series.map({"F": 0, "D": 1, "C": 2, "B": 3, "A": 4})
OrdinalEncoder(categories=[["F", "D", "C", "B", "A"]]) # worst firstRanking the days of a week
A date column, and the weekday name
pd.date_range builds a fixed set of dates, so the example produces the same answer whenever it is run. dt.day_name() reads the weekday off each date.
days = pd.date_range("2026-10-01", periods=10, freq="D")
df["weekday"] = df["Day"].dt.day_name()The order, written down
The dictionary is the whole technique. Writing it by hand is the step where the order gets decided by a person who knows what the column means, which is the one part no library can do.
order = {"Monday": 1, "Tuesday": 2, "Wednesday": 3, "Thursday": 4,
"Friday": 5, "Saturday": 6, "Sunday": 7}A date column turned into a rank
import pandas as pd
days = pd.date_range("2026-10-01", periods=10, freq="D")
df = pd.DataFrame({"Day": days})
df["weekday"] = df["Day"].dt.day_name()
order = {"Monday": 1, "Tuesday": 2, "Wednesday": 3, "Thursday": 4,
"Friday": 5, "Saturday": 6, "Sunday": 7}
df["weekday_rank"] = df["weekday"].map(order)
print(df.head(8).to_string(index=False))
print("dtype:", df["weekday_rank"].dtype)Day weekday weekday_rank 2026-10-01 Thursday 4 2026-10-02 Friday 5 2026-10-03 Saturday 6 2026-10-04 Sunday 7 2026-10-05 Monday 1 2026-10-06 Tuesday 2 2026-10-07 Wednesday 3 2026-10-08 Thursday 4 dtype: int64
Reading the ranked dates
- Seven categories became one integer column. One hot would have added seven columns, or six with the first dropped.
- The rank is an int64. Every value in the column matched a key in the dictionary. A value that does not match comes back as NaN, which turns the column into a float and is the usual way this goes wrong.
- The dates are fixed. Building the frame from a date range rather than from today means the output is the same on every run, which a date column built from
datetime.today()cannot promise. - Monday is 1 and Sunday is 7. That ordering is a choice. Starting at 0 instead changes nothing a model can see, because adding a constant to every value of a feature moves the intercept and leaves the rest of the fit alone.
Letting scikit-learn do it, and the default that gets it wrong
OrdinalEncoder does the same job as a transformer, which means it can sit in a pipeline and remember its categories. With no categories argument it sorts the levels alphabetically, and for grades the alphabet is almost exactly backwards.
import pandas as pd
from sklearn.preprocessing import OrdinalEncoder
grades = pd.DataFrame({"grade": ["B", "A", "C", "F", "A", "D"]})
wrong = OrdinalEncoder().fit(grades)
print("alphabetical order :", list(wrong.categories_[0]))
print("codes :", wrong.transform(grades).ravel())
print()
right = OrdinalEncoder(categories=[["F", "D", "C", "B", "A"]]).fit(grades)
print("the order you chose:", list(right.categories_[0]))
print("codes :", right.transform(grades).ravel())alphabetical order : ['A', 'B', 'C', 'D', 'F'] codes : [1. 0. 2. 4. 0. 3.] the order you chose: ['F', 'D', 'C', 'B', 'A'] codes : [3. 4. 2. 0. 4. 1.]
Why the default is a bug here
- Alphabetically, A is 0 and F is 4. The best grade gets the smallest number and the worst grade the largest, so the column is an upside-down ranking.
- Nothing raised. Both calls returned a column of floats with no warning. A model trained on the first one learns that a larger code means a better student.
- The explicit list fixes it.
categories=[["F", "D", "C", "B", "A"]]puts F at 0 and A at 4, which is the direction a grade runs. - The codes come back as floats.
OrdinalEncoderalways returns float64, because it reserves room for the value it writes when it meets a level it has not seen.
Mapping with a dictionary vs OrdinalEncoder
| Series.map with a dict | OrdinalEncoder | |
|---|---|---|
| Order | whatever you write | alphabetical unless you pass categories |
| A level not in the order | becomes NaN, silently | raises, or takes a value you choose |
| Output type | int64 if every level matched, float64 otherwise | always float64 |
| Several columns at once | one call per column | one call for the whole frame |
| Works in a Pipeline | no | yes |
| Best for | one column with an order you are writing by hand | several ordered columns inside a pipeline |
Where a rank is the right encoding
- A size or grade column. Small, medium and large have one order and everybody agrees on it.
- A rating scale. One to five stars is already a rank; keeping it as one column is cheaper than five dummies and loses nothing.
- A column with many ordered levels. Income bands or age bands with a dozen levels would cost a dozen columns as dummies and cost one as a rank.
Related
- Previous: One hot encoding with many categories
- Next: Label encoding
- See also: Nominal and ordinal features
- Reference: sklearn.preprocessing.OrdinalEncoder
- Remove one weekday from the dictionary and print the dtype of the ranked column.
- Change the dictionary so Sunday is 1 and Monday is 7, and compare the two ranked columns side by side.
- Pass
handle_unknown="use_encoded_value", unknown_value=-1toOrdinalEncoderand transform a frame holding the grade E.
This is what real progress feels like.