Nominal and ordinal features
A nominal feature is a category with no order and an ordinal feature is a category whose values have one, and which of the two a column is decides every encoder that can be used on it.
Last updated: 12 Oct, 2026 · pandas 3.0 · scikit-learn 1.9
The outlier part finished the numeric columns. A text column cannot reach a model at all until it becomes numbers, and the first question is not which function to call. It is whether the categories in the column stand in an order.
Every encoder writes a claim into the data that the model will believe. One hot claims nothing beyond identity. Ordinal claims an order and a spacing. The point of this lesson is that one of those claims is free and the other has to be earned.
Telling the two kinds apart
The question to ask about a column
A column is ordinal when you can finish the sentence "this value is more than that one" without inventing anything. Small, medium and large. First, business and economy class. A grade of A, B or C. If finishing the sentence takes an assumption, the column is nominal.
- Nominal: city, airline, colour, port of embarkation, product category.
- Ordinal: size, grade, education level, satisfaction rating, income band.
- It depends: day of the week is ordered inside a week and wraps around at the end of it, which Cyclical encoding for hours and weekdays deals with.
What an invented order does
Mapping a nominal column to 0, 1 and 2 always runs. The model then reads those numbers as a scale, and the scale came from the alphabet or from the order the levels happened to appear in.
embarked.map({"C": 0, "Q": 1, "S": 2}) # why that order and not another?Three arbitrary orders on one nominal column
Embarked holds the three ports a passenger boarded at: Cherbourg, Queenstown and Southampton. No order exists among them. Correlating the coded column with survival under three different arbitrary orders shows what the code is worth.
import pandas as pd
URL = ("https://raw.githubusercontent.com/krishnaik06/Feature-Engineering-Live-sessions/"
"master/titanic.csv")
df = pd.read_csv(URL, usecols=["Embarked", "Survived"]).dropna()
orders = [{"C": 0, "Q": 1, "S": 2},
{"S": 0, "C": 1, "Q": 2},
{"Q": 0, "S": 1, "C": 2}]
for order in orders:
coded = df["Embarked"].map(order)
print(order, " correlation with Survived:",
round(coded.corr(df["Survived"]), 4)){'C': 0, 'Q': 1, 'S': 2} correlation with Survived: -0.1697
{'S': 0, 'C': 1, 'Q': 2} correlation with Survived: 0.1087
{'Q': 0, 'S': 1, 'C': 2} correlation with Survived: 0.1268What the three correlations mean
- The number changes with the order. Minus 0.1697, plus 0.1087, plus 0.1268. Same column, same target, three answers.
- One of them changes sign. A model reading the first coding learns that a higher code means a worse chance of survival. Reading the third it learns the opposite.
- None of the three is the right one. There is no order among three ports, so there is no coding that tells the truth. Every one of them is an invention.
- Nothing warned about any of this. All three ran, all three produced a plausible number, and the choice that produced it was never written down.
Ordering an age band from what you know about the business
The clip meets the same choice on a shopping dataset whose Age column arrives as the bands 0-17, 18-25, 26-35 and so on. Dummy columns are shown first and then dropped in favour of ranks, because the bands are in an order and the amount people spend moves along it.
The Titanic Age column arrives as a number rather than as bands, so pd.cut makes the bands first. The decision after that is the one the clip makes; only the dataset differs.
Ranking a column that does have an order
pd.cut turns the numeric age into bands. labels names them and bins sets the edges, so the result is an ordered category where the rank is not an invention: a passenger in 46-60 is older than one in 18-25 by definition.
import pandas as pd
URL = ("https://raw.githubusercontent.com/krishnaik06/Feature-Engineering-Live-sessions/"
"master/titanic.csv")
df = pd.read_csv(URL, usecols=["Age", "Survived"])
labels = ["0-17", "18-25", "26-35", "36-45", "46-60", "60+"]
df["band"] = pd.cut(df["Age"], bins=[0, 17, 25, 35, 45, 60, 100], labels=labels)
rank = {name: i for i, name in enumerate(labels)} # 0-17 is 0, 60+ is 5
df["band_rank"] = df["band"].map(rank)
print(df[["Age", "band", "band_rank"]].head(6).to_string(index=False))
print()
print("one hot would cost", df["band"].nunique(),
"columns; the rank costs 1")
print(df.groupby("band", observed=True)["Survived"].mean().round(4).to_string())Age band band_rank 22.0 18-25 1 38.0 36-45 3 26.0 26-35 2 35.0 26-35 2 35.0 26-35 2 NaN NaN NaN one hot would cost 6 columns; the rank costs 1 band 0-17 0.5398 18-25 0.3351 26-35 0.4235 36-45 0.3947 46-60 0.4074 60+ 0.2273
Reading the ranked bands
- The rank follows the band. 0-17 is 0 and 60+ is 5, and the numbers in between are the bands in the order a person would read them.
- Six categories became one column. One hot would have added six, or five with the first dropped. That saving is the reason to use a rank when a real order exists.
- The survival rates are not in rank order. 0-17 survived at 0.5398, 18-25 at 0.3351 and 26-35 at 0.4235. The column is ordered in age and the target does not follow it, so a model that can only draw a straight line through the rank will do badly.
- Row 5 has no band. That passenger has no age, so
pd.cutreturned NaN and the rank is NaN too. An encoder does not fill holes; the missing-value part does that first.
The third point is the honest limit of ordinal encoding. It claims an order, which is true here, and it also claims equal spacing between consecutive ranks, which is a second claim nobody checked. One hot makes neither claim and costs five more columns.
Nominal vs ordinal
| Nominal | Ordinal | |
|---|---|---|
| Example | Embarked: C, Q, S | band: 0-17 ... 60+ |
| Is one value more than another? | no | yes |
| Safe encoders | one hot, count, frequency, target mean | all of those, plus a rank |
| Cost of the usual choice | n or n-1 new columns | 1 new column |
| What goes wrong with the other choice | a rank invents an order the model will use | one hot throws the order away, which a tree then has to rediscover |
Where the choice is made in practice
- A survey column. Strongly disagree through strongly agree is ordinal, and a rank keeps the frame narrow enough to model.
- A product catalogue. Category and brand are nominal, with hundreds of levels, which is the problem One hot encoding with many categories takes on.
- A size column that looks numeric. Shoe sizes, screen sizes and clothing sizes arrive as text and are ordered, so they are rank candidates rather than dummy candidates.
Related
- Previous: Treating outliers by capping or dropping
- Next: One hot encoding
- See also: Ordinal encoding
- Reference: pandas.cut
- Add a fourth order to the list,
{"S": 0, "Q": 1, "C": 2}, and read the fourth correlation. - Change the band edges to
[0, 12, 20, 40, 60, 100]and print the survival rates again. - Print
df["band"].dtypeanddf["band"].cat.categoriesto see whatpd.cutreturned.
Little by little, you're building something great.