X and y Notation, Labels and Classes

34 min
Block 1 — Core vocabulary
Objective
master the X / y notational convention and its origin, verify the dimensions of each in code, define a label and a class rigorously, designate the positive class on an explicit criterion and measure the consequences of that designation, and separate the majority class from the minority class.
Estimated duration
35 minutes
Prerequisites
chapters 001 to 006
Associated quizzes
007.1-quiz-x-y-notation.md to 007.6-quiz-majority-minority-classes.md

1. The X and y convention

1.1 The sentence to decode

"We fit the model on X and y, with X of shape (n, p) and y of shape (n,)."

Read in full: the model is trained on a table of explanatory variables holding n rows and p columns, together with a column of n answers, the i-th answer corresponding to the i-th row.

The convention is not a stylistic habit. It is typed. The case of the letter carries information about the mathematical nature of the object, and a reader who knows the convention extracts the dimensionality of every symbol in a formula without being told. That is the entire point of it.

1.2 The origin: the typography of linear algebra

Mathematical objectConventional typographyExampleNumber of dimensions
ScalarLowercase italic, or a Greek lettern, p, α0
VectorLowercase, often boldy, x, β1
MatrixUppercaseX, A, Σ2

This notational discipline predates machine learning by several decades. It is attributed to Alston Householder (Principles of Numerical Analysis, 1953) and was carried into the standard literature of numerical linear algebra from there.

Direct consequence. X is uppercase because it is a matrix: two axes, observations and variables. y is lowercase because it is a vector: one value per observation, one axis.

The second source of the convention is the matrix formulation of the linear model, standard in statistics:

y = X β + ε, with the least-squares estimator β̂ = (Xᵀ X)⁻¹ Xᵀ y

The argument from dimensional coherence. Read the estimator one factor at a time. Xᵀ X is a product of a p × n matrix by an n × p matrix, which is defined only if X is genuinely two-dimensional, and which yields a p × p matrix. Its inverse is p × p. Xᵀ y is a product of a p × n matrix by a vector of length n, which is defined only if y has exactly as many components as X has rows, and which yields a vector of length p. The product of the two is therefore a vector of length p: one coefficient per explanatory variable, which is precisely what β̂ must be.

Every step of that reading depends on X being a matrix and y a vector. The notation is what makes the check possible at a glance, and the same discipline propagates into the shapes that the libraries enforce at runtime.

DEFINITION — Matrix of explanatory variables (X)

Rigorous definition

Given a dataset of n observations described by p explanatory variables, the matrix of explanatory variables X is the element of Rn×p\mathbb{R}^{n \times p} whose entry xijx_{ij} is the value taken by the j-th explanatory variable on the i-th observation. The i-th row of X, written xix_i, is the feature vector of observation i.

Synonyms: design matrix, predictor matrix, feature matrix.

In plain terms

The table of information handed to the model, stripped of the column to predict and of every column that has no explanatory role.

Point of caution

X contains neither the target, nor identifiers, nor traceability columns. Every column left in place will be used by the algorithm, including an identifier that happens to correlate with the target by accident of how the file was assembled (chapters 005 and 028).

DEFINITION — Target vector (y)

Rigorous definition

The target vector y is the element of Rn\mathbb{R}^n in regression, or of Cn\mathcal{C}^n in classification where 𝒞 is the finite set of classes, whose i-th component yiy_i is the value of the target variable observed for the i-th observation. The correspondence is positional: yiy_i is the answer attached to row xix_i of X.

Synonyms: response vector, dependent variable, target, labels.

In plain terms

The column of correct answers, in the order of the rows of the table.

Point of caution

y is a one-dimensional object, of shape (n,) and not (n, 1). The distinction has no mathematical content and a very real effect in code (section 2.3).

1.3 Splitting the table into X and y

The dataset used throughout this chapter is simulated so that every figure below can be reproduced exactly. It describes 120,000 card transactions, carries fourteen explanatory variables, two non-modeling columns and one binary target, status, whose two levels are Fraud and Normal.

python
import numpy as np
import pandas as pd

rng = np.random.default_rng(7)
n = 120_000

is_fraud = np.zeros(n, dtype=bool)
is_fraud[rng.choice(n, 348, replace=False)] = True

night = np.where(np.arange(24) < 6, 0.085, 0.0272222)
hour = np.where(is_fraud,
                rng.choice(24, n, p=night / night.sum()),
                rng.integers(6, 24, n))

amount = np.clip(np.round(np.where(is_fraud,
                                   rng.lognormal(4.3, 1.1, n),
                                   rng.lognormal(3.4, 1.0, n)), 2), 1.0, 9_500.0)

df = pd.DataFrame({
    "transaction_id": [f"T-{i + 1:07d}" for i in range(n)],
    "operation_date": pd.to_datetime("2026-01-01")
                      + pd.to_timedelta(rng.integers(0, 180, n), unit="D"),
    "amount": amount,
    "hour": hour,
    "day_of_week": rng.integers(0, 7, n),
    "days_since_last_transaction": np.round(np.abs(rng.normal(2.4, 2.0, n)), 2),
    "transactions_last_24h": rng.poisson(np.where(is_fraud, 3.2, 2.1), n),
    "amount_to_median_ratio": np.round(amount / rng.uniform(25, 70, n), 3),
    "distance_from_home_km": np.round(rng.exponential(np.where(is_fraud, 55, 22), n), 1),
    "distance_from_last_transaction_km": np.round(rng.exponential(np.where(is_fraud, 40, 18), n), 1),
    "card_age_months": rng.integers(1, 145, n),
    "merchant_risk_score": np.round(rng.beta(np.where(is_fraud, 2.6, 1.6), 4.0, n), 3),
    "is_online": (rng.random(n) < np.where(is_fraud, 0.55, 0.31)).astype(int),
    "is_foreign_country": (rng.random(n) < np.where(is_fraud, 0.14, 0.04)).astype(int),
    "chip_used": (rng.random(n) < np.where(is_fraud, 0.55, 0.86)).astype(int),
    "failed_attempts_1h": rng.poisson(np.where(is_fraud, 0.45, 0.06), n),
    "status": np.where(is_fraud, "Fraud", "Normal"),
})

# Two enrichment fields are not always populated by the upstream system.
df.loc[rng.choice(n, 1_800, replace=False), "merchant_risk_score"] = np.nan
df.loc[rng.choice(n, 900, replace=False), "days_since_last_transaction"] = np.nan

df.to_csv("card_transactions.csv", index=False)
print(df.shape)
print(df["status"].value_counts())
print("rows carrying at least one missing value:", int(df.isna().any(axis=1).sum()))
(120000, 17)
status
Normal    119652
Fraud        348
Name: count, dtype: int64
rows carrying at least one missing value: 2688

The split itself is three lines.

python
import pandas as pd

df = pd.read_csv("card_transactions.csv")

TARGET, NOT_MODELED = "status", ["transaction_id", "operation_date"]
X = df.drop(columns=[TARGET] + NOT_MODELED)
y = df[TARGET]

Interpretation. X is built by subtraction, never by enumerating the columns to keep. The consequence is deliberate and worth stating: a column added upstream — by a new join, an enriched export, a schema change in the source system — enters the model automatically, without anyone deciding that it should. Building X by subtraction is the right default because it fails loudly when the schema changes rather than silently dropping a variable, but it makes an explicit review of the source schema part of the routine rather than an optional courtesy.

Variants, and one form that is simply wrong. An uppercase Y is justified for a multi-output target of shape (n, t) — several values predicted jointly for the same observation (chapter 048) — and is wrong for a single target, where it announces a matrix that does not exist. Part of the neural-network literature places observations in columns, giving X the shape (p, n); the variant is internally coherent and incompatible with the scikit-learn interface, which requires observations in rows. Mixing the two conventions inside one codebase produces transpositions that run without error and train on nonsense.


2. Shapes and their verification in code

2.1 The structuring integers and the invariant

SymbolNamescikit-learn equivalentWhat it counts
nNumber of observationsn_samplesRows of X, length of y
pNumber of explanatory variablesn_featuresColumns of X
kNumber of classesn_classesDistinct levels of y

Some references write d for the number of variables and m for the number of observations. The letter is immaterial; the position is not. The first axis is always the observation axis.

The equality of lengths is checked automatically by scikit-learn on every call to fit. The row-by-row correspondence is checkable by no library whatsoever. It is a property of the code that produced X and y, and nothing in the arrays themselves records whether it still holds. That asymmetry is the whole subject of sections 2.4 and 2.5.

Vocabulary reminder. The shape of an array is the tuple of the sizes of its axes. (n,) and (n, 1) hold the same number of values and do not have the same rank: a vector on one side, a one-column matrix on the other.

2.2 The systematic check

python
print("X shape :", X.shape)
print("y shape :", y.shape)
print("y ndim  :", y.ndim)
print("lengths match :", X.shape[0] == y.shape[0])
print()
print(list(X.columns))
X shape : (120000, 14)
y shape : (120000,)
y ndim  : 1
lengths match : True

['amount', 'hour', 'day_of_week', 'days_since_last_transaction',
 'transactions_last_24h', 'amount_to_median_ratio', 'distance_from_home_km',
 'distance_from_last_transaction_km', 'card_age_months', 'merchant_risk_score',
 'is_online', 'is_foreign_country', 'chip_used', 'failed_attempts_1h']

Interpretation. 120,000 observations described by 14 variables, and 120,000 target values. The rank of 1 on y confirms that it is a vector and not a one-column table. Printing the column list is worth the two extra characters: it is the only cheap way to confirm that the target has actually left X, and that no identifier has come back in through a merge.

The check costs nothing and is repeated after every filtering step, every merge and every resampling operation. Cheap and repeated beats thorough and occasional.

2.3 The column-vector pitfall

python
y_correct = df["status"]     # pandas Series    -> shape (120000,)   ndim 1
y_wrong = df[["status"]]     # pandas DataFrame -> shape (120000, 1) ndim 2

One pair of brackets separates the two. Handed to an estimator, the second triggers the following warning, which does not interrupt execution and repeats at every fold of every cross-validation run:

DataConversionWarning: A column-vector y was passed when a 1d array was
expected. Please change the shape of y to (n_samples,), for example using
ravel().

Fix. Write df["status"], or df[["status"]].to_numpy().ravel() when the two-dimensional object comes from upstream code you do not control.

Point of caution. The warning is benign in this case because scikit-learn silently reshapes the array for you. The habit it encourages is not benign. A practitioner who learns to ignore DataConversionWarning also ignores the warnings that announce a real defect, and a training log dense with repeated warnings stops being read at all.

2.4 Loud misalignment

The most widespread scenario consists in filtering X after y has already been extracted.

python
from sklearn.ensemble import RandomForestClassifier

X = df.drop(columns=["transaction_id", "operation_date", "status"])
y = df["status"]

X = X.dropna()          # 2,688 rows removed from X, none from y
RandomForestClassifier().fit(X, y)
ValueError: Found input variables with inconsistent numbers of samples:
[117312, 120000]

Interpretation. This error is the benign case, because it is loud. The consistency check fires before any learning happens, the message names both lengths, and the fix is obvious once read. The correct form filters the source table before the split:

python
df_clean = df.dropna()
X = df_clean.drop(columns=["transaction_id", "operation_date", "status"])
y = df_clean["status"]

2.5 Silent misalignment

The dangerous case preserves the lengths and destroys the order.

python
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score

X = df.drop(columns=["transaction_id", "operation_date", "status"])
y = (df["status"] == "Fraud").astype(int)

X_sorted = X.sort_values("amount")          # X reordered, y untouched

print("lengths match :", X_sorted.shape[0] == y.shape[0])
print("aligned   :", round(cross_val_score(RandomForestClassifier(random_state=42, n_jobs=-1),
        X, y, cv=5, scoring="roc_auc").mean(), 3))
print("misaligned:", round(cross_val_score(RandomForestClassifier(random_state=42, n_jobs=-1),
        X_sorted, y, cv=5, scoring="roc_auc").mean(), 3))
lengths match : True
aligned   : 0.898
misaligned: 0.492

Interpretation. No exception is raised. Estimators convert pandas objects to NumPy arrays and ignore the index: the correspondence is destroyed with no signal at all, and the model spends its training budget learning an association between transactions and the labels of other transactions. The result is the only symptom available — a ROC AUC of 0.492 where the same code on the same data yields 0.898.

An AUC sitting on 0.5 in a problem the business believes to be predictable is the signature of a misalignment, not of a bad algorithm. The reflex it should provoke is to audit the construction of X and y, not to switch to a different model family.

The guard rail. Once X and y have been separated, the pandas index is the only remaining witness to their correspondence.

python
print("lengths match :", X_sorted.shape[0] == y.shape[0])
print("indexes match :", X_sorted.index.equals(y.index))
assert X_sorted.index.equals(y.index), "X and y are no longer aligned"
lengths match : True
indexes match : False
AssertionError: X and y are no longer aligned

2.6 Which operations are dangerous

Operation applied to X aloneLengths preservedCorrespondence preservedSeverity
dropna(), boolean filtering, query()NoNoBenign — raises ValueError
sort_values()YesNoCritical — silent
sample(frac=1)YesNoCritical — silent
reset_index(drop=True) on one side onlyYesYes at first, then destroyed by any later join or assertCritical — silent, and it removes the witness
groupby().transform()YesYesSafe
Adding or dropping a columnYesYesSafe

The rule that covers all six rows. Every operation that changes the set of rows or their order is applied to the source table, before X and y are separated. After the split, X and y are read, never reordered.

Point of caution on reset_index(drop=True). It is the worst of the three silent operations, because it is usually applied with the intention of tidying up. It leaves the data in place and deletes the only evidence that would have let a later assert detect an earlier reordering. Reindex both objects together or neither.


3. Label and ground truth

DEFINITION — Label

Rigorous definition

The value of the target variable attached to an observation in a supervised learning dataset, taken as the reference answer for that observation. The ordered set of labels constitutes the target vector y.

In plain terms

The correct answer, already known, attached to one row of the table.

Scope of the term

In the strict sense, a label is a categorical value naming the category the observation belongs to. In classification, a label is a class. In regression, the associated quantity is a numeric target value and not a label in the strict sense, even though professional usage extends the word to both situations and speaks of labeled data for any dataset carrying an observed target.

Point of caution

A label is not an absolute truth. It is a recorded value, which may be wrong, or may reflect the decision of a fallible operator. A fraud dataset contains the frauds that were detected; the rest carry the label Normal and are indistinguishable, in the file, from genuinely normal transactions.

3.1 Classification label and regression target value

CriterionClassificationRegression
Nature of yiy_iA categoryA real number
Rigorous termClass labelTarget value, response value
CodomainA finite set 𝒞 of cardinality kℝ or an interval of ℝ
Distance between two valuesUndefinedDefined and interpretable
Order between two valuesUndefined, except in the ordinal caseTotal and meaningful
Examplestatus ∈ {Fraud, Normal}claim_amount = $4,380.50

Point of caution. The distinction cannot be read off the storage type of the column. A target coded 0 and 1 is numeric in the sense of pandas and categorical in the statistical sense; a target holding the strings "1" through "5" for a rating may be ordinal. The criterion is the nature of the phenomenon measured, and the operational test is whether the difference between two values means anything (chapter 010).

The consequence is immediate and often overlooked. Nothing prevents a regressor from being fitted on a target coded 0 and 1; the code runs, the residuals are computed, an R² comes out. What has been produced is not a classifier, its outputs are not probabilities, and none of the classification metrics apply to it.

3.2 Ground truth and the quality of labeling

Between the phenomenon and the value written into y sit two fallible steps: a detection and a recording. The label is the output of that chain, not the phenomenon itself.

StepFailure modeEffect on the model
DetectionUndetected frauds are labeled NormalThe positive class is under-counted; measured recall overstates true recall
AnnotationTwo analysts disagree on borderline casesThe learnable signal is capped by the disagreement rate
RecordingLate updates, retroactive correctionsThe label depends on the extraction date, and the dataset is not reproducible
DefinitionThe written rule changes over the historyTwo periods carry incompatible labels under one column name

Where several operators annotate the same observations, their rate of disagreement bounds the performance attainable by any model. No algorithm can learn a distinction more sharply than the labels encode it. Raw agreement is a poor summary of that ceiling because two annotators who both mark nearly everything Normal agree almost perfectly by construction. Cohen's kappa (Cohen, 1960) corrects for the agreement expected by chance.

DEFINITION — Cohen's kappa (Cohen, 1960)

Rigorous definition

For two annotators labeling the same m observations into the same categories, let pop_o be the observed proportion of agreement and pep_e the proportion expected if the two annotators labeled independently, with their respective marginal frequencies. Cohen's kappa is:

κ=(pope)/(1pe)\displaystyle \kappa = (p_o - p_e) / (1 - p_e)

Reading the scale. κ = 1 is perfect agreement; κ = 0 is agreement no better than chance; κ < 0 is systematic disagreement. The conventional bands are due to Landis and Koch (1977): below 0.20 slight, 0.21 to 0.40 fair, 0.41 to 0.60 moderate, 0.61 to 0.80 substantial, above 0.80 almost perfect. The bands are a convention, not a result.

In plain terms

How much the two annotators agree, over and above what two people would agree on by simply guessing at the same rates.

Point of caution

Kappa is depressed by a skewed marginal distribution. On a class present in 2% of the sample, chance agreement is already very high, so a modest kappa may coexist with an excellent raw agreement rate. Report both, and report the counts.

The block below simulates a double review of 500 flagged transactions by two fraud analysts. Both are competent; neither is infallible.

python
import numpy as np
import pandas as pd
from sklearn.metrics import cohen_kappa_score

rng = np.random.default_rng(11)
m = 500
true_fraud = rng.random(m) < 0.18
a1 = np.where(rng.random(m) < 0.95, true_fraud, ~true_fraud)
a2 = np.where(rng.random(m) < 0.93, true_fraud, ~true_fraud)

review = pd.DataFrame({"analyst_a": np.where(a1, "Fraud", "Normal"),
                       "analyst_b": np.where(a2, "Fraud", "Normal")})

print(pd.crosstab(review["analyst_a"], review["analyst_b"]))
print("Raw agreement :", round((review.analyst_a == review.analyst_b).mean(), 4))
print("Cohen's kappa :", round(cohen_kappa_score(review.analyst_a, review.analyst_b), 4))
analyst_b  Fraud  Normal
analyst_a               
Fraud         96      22
Normal        31     351
Raw agreement : 0.894
Cohen's kappa : 0.7136

Interpretation. The two analysts agree on 89.4% of the files, which sounds close to settled. Corrected for chance, the agreement is 0.71 — substantial, not excellent. Fifty-three of the five hundred files are classified differently by two qualified people applying the same rule book. Those fifty-three carry a label whose value depends on which analyst happened to open the case, and a model trained on them is being asked to reproduce a coin flip.

Operational consequence. Before attributing a disappointing result to the algorithm, audit the labeling: who labeled, under what written definition, with what measured agreement, and over what period. A model whose measured performance approaches the inter-annotator agreement rate has extracted what the labels contain. Further tuning will not help; relabeling might.


4. The class, and the conventions of 0/1 encoding

DEFINITION — Class

Rigorous definition

A level of the target variable in a classification problem. The set of k classes forms a partition of the space of observations: every observation belongs to one class and to exactly one. The classes are therefore mutually exclusive and collectively exhaustive.

In plain terms

One of the possible categories of the answer, and only one per row.

Cardinality

k = 2 in binary classification, k > 2 in multiclass. Multilabel classification lifts the exclusivity assumption and lets one observation carry several labels at once (chapter 011); it is a different formalism, not a special case of this one.

Point of caution

A class is a level of the target. The levels of an explanatory variable are categories, and the two are handled by entirely different machinery: classes are encoded once, with a decision attached (section 4.1); categories are encoded as indicator columns, and the choice of scheme is a modeling question (chapter 022).

Second point of caution

The partition must be exhaustive in fact, not only on paper. A target with levels Fraud and Normal is exhaustive only if every transaction reviewed falls into one of the two. If reviewers may also mark a case "undetermined" and those rows are silently dropped, the model is being fitted on a filtered population and its performance will not transfer to the full stream.

4.1 From text label to numeric code

Most estimators work with numeric labels. The conversion is mechanical and carries an implicit decision: which class receives the code 1.

python
from sklearn.preprocessing import LabelEncoder

encoder = LabelEncoder().fit(df["status"])
print(dict(zip(encoder.classes_, encoder.transform(encoder.classes_).tolist())))
{'Fraud': 0, 'Normal': 1}

Interpretation. LabelEncoder sorts the levels in lexicographic order. "Fraud" precedes "Normal" in the alphabet, so fraud receives the code 0 and normal behavior receives the code 1. Since the asymmetric metrics default to pos_label=1, the positive class silently becomes Normal — the exact inverse of the business intent, with no warning of any kind.

The failure is not a scikit-learn defect. The library has no way to know which level matters; sorting is the only deterministic rule available to it. The defect is in leaving the decision to the alphabet.

Recommended form. State the encoding rather than inherit it.

python
y = (df["status"] == "Fraud").astype(int)
print(y.value_counts())
status
0    119652
1       348
Name: count, dtype: int64

Interpretation. The code 1 designates fraud by construction and not by alphabetical accident, and a single line of code documents the decision for every future reader. The expression is self-verifying: anyone reading it knows immediately what "positive" means in every metric computed downstream.

4.2 The encoding conventions to know

ConventionContentContext
0 / 10 = absence of the phenomenon, 1 = presenceStandard in binary classification
Lexicographic orderLevels are sorted, then numbered from 0Default behavior of scikit-learn
−1 / +1Negative and positive classMargin formulations: SVM, boosting theory (chapters 039 and 041)
classes_Attribute exposing the order the estimator retainedTo be inspected systematically
One-hot on the targetk indicator columnsMulticlass with neural networks; not the scikit-learn interface

Point of caution. The −1 / +1 convention is not interchangeable with 0 / 1. It appears in the mathematical formulation of margin-based methods, where the product y · f(x) is positive exactly when the prediction is correct. The scikit-learn API expects 0 / 1 regardless; the −1 / +1 form belongs to the papers, not to the calling code.

4.3 classes_ and the column order of predict_proba

The matrix returned by predict_proba holds one column per class, ordered according to estimator.classes_. The ordering is not documented per call; it is readable on the fitted object, and it must be read.

The split below is reused in sections 5 and 6. It is stratified, for the reason given in section 6.1: with 348 frauds in 120,000 rows, an unstratified draw can easily leave the test set with a materially different fraud rate.

python
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import (accuracy_score, balanced_accuracy_score,
                             confusion_matrix, f1_score, matthews_corrcoef,
                             precision_score, recall_score, roc_auc_score)

X = df.drop(columns=["transaction_id", "operation_date", "status"])
y = (df["status"] == "Fraud").astype(int)

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=42)
print("train:", X_train.shape, "| test:", X_test.shape,
      "| frauds in test:", int(y_test.sum()))
train: (96000, 14) | test: (24000, 14) | frauds in test: 70
python
model = RandomForestClassifier(n_estimators=300, class_weight="balanced",
                               random_state=42, n_jobs=-1).fit(X_train, y_train)

print("classes_:", model.classes_)
print("AUC with P(Fraud)  =", round(roc_auc_score(y_test, model.predict_proba(X_test)[:, 1]), 4))
print("AUC with P(Normal) =", round(roc_auc_score(y_test, model.predict_proba(X_test)[:, 0]), 4))
classes_: [0 1]
AUC with P(Fraud)  = 0.879
AUC with P(Normal) = 0.121

Interpretation. With classes_ equal to [0 1] and 1 designating fraud, column 1 does carry the probability of fraud, and the AUC of 0.879 measures a genuinely discriminating model. Column 0 carries the complementary probability, and scoring against it returns 1 − 0.879 = 0.121.

Now the same experiment with the text labels left unencoded.

python
Xtr, Xte, ytr, yte = train_test_split(X, df["status"], test_size=0.2,
                                      stratify=df["status"], random_state=42)
text_model = RandomForestClassifier(n_estimators=300, class_weight="balanced",
                                    random_state=42, n_jobs=-1).fit(Xtr, ytr)

print("classes_:", text_model.classes_)
print("AUC using column 1 against the Fraud indicator:",
      round(roc_auc_score((yte == "Fraud").astype(int),
                          text_model.predict_proba(Xte)[:, 1]), 4))
classes_: ['Fraud' 'Normal']
AUC using column 1 against the Fraud indicator: 0.1223

Interpretation. Nothing failed. The model is exactly as good as before; only the column read is wrong. classes_ now sorts Fraud first, so column 1 holds the probability of being normal, and evaluating it against the fraud indicator returns 0.1223 — a value far below 0.5, obtained by a model whose actual discriminating power is 0.878.

The diagnostic worth memorizing. An AUC clearly below 0.5 almost never means a model that has learned the phenomenon backwards. It means the score column, or the label encoding, is inverted. Reading classes_ settles it in one line.


5. The choice of the positive class

DEFINITION — Positive class and negative class

Rigorous definition

In a binary classification problem, the positive class is the level of the target variable designated as the event of interest. It serves as the reference for computing TP, FP, FN and TN, and therefore for every asymmetric metric derived from them; the negative class is the complementary level.

The vocabulary is borrowed from medical diagnosis, where a "positive" test signals the presence of the condition being looked for.

In plain terms

The positive class is what you are trying to detect.

The nature of the concept

The designation is a methodological decision, taken during problem framing, in the same way that the unit of analysis is decided (chapter 005). It is not a property of the data and it is not discovered by inspecting the file.

Point of caution

"Positive" does not mean "favorable". In a screening model the positive class is Affected; in a churn model it is Churned; in a default model it is Default. The term is descriptive, never evaluative.

ANALOGY — The screening test

A patient whose test comes back positive has not received good news. The laboratory expressed no judgment of value. It reported that the substance being looked for was detected.

A test designed to detect a disease is calibrated on the disease. Nobody asks what the sensitivity of a test is for good health: the question is grammatically well formed and clinically meaningless, because the instrument was built around one event and reports on that event.

The positive class of a model obeys the same logic. It is the substance being looked for. And as in the laboratory, two numbers describe the instrument's behavior on that substance and on nothing else: how often it finds the substance when it is there, and how often what it reports is genuinely the substance. Both are undefined until the substance has been named.

5.1 The designation criterion

The three criteria almost always converge: what an organization is trying to detect is also what is rare, and also what is expensive to miss. Where they diverge, the first prevails, because it is the business criterion and the other two are proxies for it.

Use casePositive classNegative classAction triggered
Fraud detectionFraudNormalBlock, or send to manual review
Medical screeningAffectedHealthyFollow-up examination
Customer churnChurnedRetainedRetention offer
Predictive maintenanceFailure within 30 daysNominal operationPreventive intervention
Credit defaultDefaultRepaidRefusal, or reinforced guarantees
Quality control certifying conformityConformingDefectiveRelease of the batch

Note the last row. It is the exception that shows the criterion is genuinely the business one: a certification process acts on the conforming pieces, which are the majority, and designating them positive is legitimate. Section 6.2 returns to it.

5.2 The consequences on the metrics

A model is trained on the 96,000 transactions of the training set and evaluated on the 24,000 held-out ones, which contain 70 frauds. The operating threshold is set at 0.02, an alert budget the review team can absorb.

python
model = RandomForestClassifier(n_estimators=300, class_weight="balanced",
                               random_state=42, n_jobs=-1).fit(X_train, y_train)
fraud_score = model.predict_proba(X_test)[:, 1]
y_pred = (fraud_score >= 0.02).astype(int)

tn, fp, fn, tp = confusion_matrix(y_test, y_pred).ravel()
print(f"TN={tn}  FP={fp}  FN={fn}  TP={tp}")
TN=23591  FP=339  FN=26  TP=44
Predicted NormalPredicted Fraud
Actual NormalTN = 23,591FP = 339
Actual FraudFN = 26TP = 44

The same predictions, the same test set, the same model. Only the designated positive class changes between the two halves of the block below.

python
for name, value in [
        ("Accuracy",             accuracy_score(y_test, y_pred)),
        ("Balanced accuracy",    balanced_accuracy_score(y_test, y_pred)),
        ("Matthews coefficient", matthews_corrcoef(y_test, y_pred)),
        ("Precision  Fraud",     precision_score(y_test, y_pred, pos_label=1)),
        ("Recall     Fraud",     recall_score(y_test, y_pred, pos_label=1)),
        ("F1         Fraud",     f1_score(y_test, y_pred, pos_label=1)),
        ("Precision  Normal",    precision_score(y_test, y_pred, pos_label=0)),
        ("Recall     Normal",    recall_score(y_test, y_pred, pos_label=0)),
        ("F1         Normal",    f1_score(y_test, y_pred, pos_label=0))]:
    print(f"{name:<22}: {value:.4f}")
Accuracy              : 0.9848
Balanced accuracy     : 0.8072
Matthews coefficient  : 0.2644
Precision  Fraud      : 0.1149
Recall     Fraud      : 0.6286
F1         Fraud      : 0.1943
Precision  Normal     : 0.9989
Recall     Normal     : 0.9858
F1         Normal     : 0.9923

Interpretation. One model, one set of predictions, and an F1-score of 0.1943 or 0.9923 depending on which level is called positive — a factor of five, and on scarcer positives the gap widens to an order of magnitude. Read as a fraud detector the system is mediocre: it raises 383 alerts to catch 44 frauds, so roughly eight alerts in nine are wasted analyst time, and 26 frauds out of 70 pass through untouched. Read as a normality classifier the same system is close to flawless, and the 26 missed frauds — the entire reason the project exists — disappear into the fourth decimal place.

Neither reading is a computational error. Both figures are correct. Only one of them answers the question the organization asked, and the number alone does not say which.

Consequence for reporting. A metric reported without naming its class is not a result. "The F1-score of the model is 0.99" is not a claim that can be checked, challenged or compared. "The F1-score on the Fraud class, at threshold 0.02, is 0.194" is.

5.3 Which metrics move and which do not

The inversion can be verified directly, by swapping the roles and recomputing.

python
y_inv, pred_inv = 1 - y_test, 1 - y_pred

print("Accuracy inverted     :", round(accuracy_score(y_inv, pred_inv), 4))
print("Balanced acc inverted :", round(balanced_accuracy_score(y_inv, pred_inv), 4))
print("MCC inverted          :", round(matthews_corrcoef(y_inv, pred_inv), 4))
print("Precision(Normal) == NPV of the Fraud designation:",
      round(precision_score(y_inv, pred_inv, pos_label=1), 4), "==", round(tn / (tn + fn), 4))
print("Recall(Normal) == specificity of the Fraud designation:",
      round(recall_score(y_inv, pred_inv, pos_label=1), 4), "==", round(tn / (tn + fp), 4))
Accuracy inverted     : 0.9848
Balanced acc inverted : 0.8072
MCC inverted          : 0.2644
Precision(Normal) == NPV of the Fraud designation: 0.9989 == 0.9989
Recall(Normal) == specificity of the Fraud designation: 0.9858 == 0.9858
MetricBehavior when the positive class is inverted
Accuracy, balanced accuracy, log loss, Matthews coefficient, Cohen's kappaInvariant — the three figures above are unchanged to the fourth decimal
PrecisionBecomes the negative predictive value of the original designation
RecallBecomes the specificity of the original designation
F1-score, PR-AUCChange, drastically under imbalance
ROC AUCUnchanged if the score used is that of the new positive class; becomes 1 − AUC otherwise (section 4.3)

How to read the table. The invariant metrics are those built symmetrically on the four cells of the confusion matrix. The metrics that move are those built on one row or one column of it. That is not a defect: precision and recall are asymmetric by design, because the question they answer is asymmetric. The error is not using them, it is using them without stating the reference class.

These metrics are defined one by one in chapters 052 to 065. What must be acquired here is their dependence on the reference class. The same designation also governs how the confusion matrix is read, where swapping the roles turns a false negative into a false positive (chapter 052); the direction in which the decision threshold moves (chapter 062); and which class is reinforced by class_weight and by oversampling (chapter 051).

5.4 The classic designation mistakes

MistakeHow it shows upFix
Letting alphabetical order decideFraud encoded as 0 by LabelEncoderEncode the class of interest as 1, explicitly
Confusing positive with favorableRetained designated positive in a churn modelPositive = the event being looked for
Designating the majority classFlattering metrics that cannot be acted onDesignate the rare event to be detected
Omitting pos_label with text labelsValueError: pos_label=1 is not a valid labelPass pos_label="Fraud", or recode to 0/1
Reading confusion_matrix().ravel() without checking the orderTN, FP, FN and TP swappedSet labels= explicitly and check classes_

The last two are worth seeing run.

python
y_true = pd.Series(["Normal", "Fraud", "Normal", "Fraud", "Normal"])
y_pred = pd.Series(["Normal", "Normal", "Fraud", "Fraud", "Normal"])

try:
    f1_score(y_true, y_pred)
except ValueError as exc:
    print("ValueError:", exc)
print("with pos_label='Fraud':", round(f1_score(y_true, y_pred, pos_label="Fraud"), 4))
ValueError: pos_label=1 is not a valid label. It should be one of ['Fraud', 'Normal']
with pos_label='Fraud': 0.5
python
y_true = np.array(["Normal"] * 8 + ["Fraud"] * 4)
y_pred = np.array(["Normal"] * 6 + ["Fraud"] * 2 + ["Normal"] * 1 + ["Fraud"] * 3)

print("default label order      :", confusion_matrix(y_true, y_pred).ravel())
print("labels=['Normal','Fraud']:", confusion_matrix(y_true, y_pred,
                                                    labels=["Normal", "Fraud"]).ravel())
default label order      : [3 1 2 6]
labels=['Normal','Fraud']: [6 2 1 3]

Interpretation. The idiom tn, fp, fn, tp = confusion_matrix(...).ravel() is correct only when the label order is [negative, positive]. With text labels sorted alphabetically, Fraud comes first, and the unpacking assigns TN = 3, FP = 1, FN = 2, TP = 6 where the true values are TN = 6, FP = 2, FN = 1, TP = 3. Every downstream metric computed by hand from those four numbers is wrong, and no error is raised at any point.

Recommendation. The designation belongs to the framing of the problem, on the same footing as the unit of analysis: agreed with the business, written down in plain language, and materialized in the code by a single explicit expression placed where anyone will read it.


6. Majority class and minority class

DEFINITION — Majority class, minority class, prevalence

Rigorous definition

The majority class is the one with the largest count in the dataset under consideration; the minority class is the one with the smallest. In multiclass problems the qualification extends by decreasing count.

The prevalence of the positive class is the proportion of observations belonging to it, π = n₁ / n. The imbalance ratio is the quotient IR = nmajn_{\mathrm{maj}} / nminn_{\mathrm{min}}.

In plain terms

The most frequent class and the rarest one; the percentage of positive cases, and the number of negatives per positive.

Point of caution

These qualifications are empirical. They describe the composition of a sample, not a property of the phenomenon. A resampled training set may show equal counts while the phenomenon remains rare in the population — which is why the test set is never rebalanced (chapter 051), and why the prevalence quoted in a report must always say which set it was measured on.

Second point of caution

What limits learning is not the ratio alone but the absolute count of the minority class. A ratio of 100:1 with 5,000 positives is workable; the same ratio with 30 positives is not, whatever technique is applied to it. Two numbers are therefore reported together, never one: the ratio and the count.

6.1 Counting, in code

python
counts = df["status"].value_counts()

print(counts)
print("Prevalence      :", round(counts["Fraud"] / counts.sum(), 5))
print("Imbalance ratio :", round(counts.max() / counts.min(), 1), ": 1")
print("Majority share  :", round(100 * counts.max() / counts.sum(), 2), "%")
print("Minority share  :", round(100 * counts.min() / counts.sum(), 2), "%")
status
Normal    119652
Fraud        348
Name: count, dtype: int64
Prevalence      : 0.0029
Imbalance ratio : 343.8 : 1
Majority share  : 99.71 %
Minority share  : 0.29 %

Interpretation. The majority class is Normal with 99.71% of the observations, the minority class is Fraud with 0.29%, that is, close to 344 normal transactions for every fraudulent one. The absolute count of 348 frauds is low: after an 80/20 split the test set holds only 70 of them, which makes any metric computed on that class unstable — one fraud more or less caught moves recall by 1.4 points — and makes a stratified split mandatory (chapter 027).

Recommendation. value_counts() on the target is the first command run after loading a classification dataset, before any exploration and before any modeling decision. It conditions the choice of metrics (chapter 075), the splitting strategy (chapter 027) and the treatment of imbalance (chapter 051).

DomainTypical prevalence of the positive class
Telecom churn15% to 30%
Credit default2% to 8%
Card fraud0.1% to 0.5%
Network intrusion detectionbelow 0.1%
Rare-disease screening0.01% to 1%

Balanced classes are the exception. Imbalance is the normal situation in applied classification, and a course that treats it as a special case teaches the wrong default.

6.2 Positive and minority: two distinct notions

CriterionPositive classMinority class
How it is establishedDecided during framingMeasured by value_counts()
Can it change without the data changingYes, if the business question changesNo
Can it change without the question changingNoYes, after resampling or a new extraction
Where it is recordedIn the project documentation and in one line of codeIn the dataset

The dominant configuration pairs the two, and the pairing is so common that the words get used interchangeably. Two situations break it.

A majority positive class. A quality-control process in which 92% of the parts are conforming, and whose purpose is to certify conformity rather than to catch defects, legitimately designates the majority as positive. The action triggered — releasing the batch — attaches to conformity.

A positive class that stops being the minority. After rebalancing the training set, the positive class may be half the training rows and remain 0.29% of the test rows. Both statements are true at the same time, of different sets. This is exactly why "the minority class" must never be used as a shorthand for "the positive class" in a report: the referent changes between the training section and the evaluation section.

6.3 The immediate consequence: misleading accuracy

python
from sklearn.dummy import DummyClassifier

baseline = DummyClassifier(strategy="most_frequent").fit(X_train, y_train)
y_base = baseline.predict(X_test)

print("Accuracy      :", round(accuracy_score(y_test, y_base), 4))
print("Recall Fraud  :", round(recall_score(y_test, y_base, pos_label=1), 4))
print("ROC AUC       :", round(roc_auc_score(y_test, baseline.predict_proba(X_test)[:, 1]), 4))
print("Frauds caught :", int(((y_base == 1) & (y_test == 1)).sum()), "out of", int(y_test.sum()))
Accuracy      : 0.9971
Recall Fraud  : 0.0
ROC AUC       : 0.5
Frauds caught : 0 out of 70

Interpretation. A model that always answers Normal reaches 99.71% accuracy and detects not a single fraud. It is perfectly useless and perfectly well scored. That is why accuracy is set aside as soon as the imbalance is marked (chapters 053 and 060), and why the AUC of 0.5 is the honest summary of this model: it ranks nothing.

The baseline is mandatory, not optional. DummyClassifier is not a teaching device. It is the reference point every project owes itself: a model that does not beat it has learned nothing, whatever its accuracy says. Compare the trained model of section 5.2 against it on the figures that matter.

ModelAccuracyRecall on FraudFrauds caught out of 70Alerts raised
DummyClassifier(strategy="most_frequent")0.99710.000000
Random forest, threshold 0.020.98480.628644383

How to read this table. The useful model has the lower accuracy of the two. Any selection procedure driven by accuracy on this problem would retain the model that catches nothing. The choice of metric is not a reporting detail; it decides which model ships (chapters 050, 051 and 075).


7. Common reasoning mistakes

MISTAKE — Taking equal lengths as proof of alignment

X.shape[0] == y.shape[0] is a necessary condition and never a sufficient one. A sort, a shuffle or a reindex applied to one of the two objects preserves the lengths and destroys the correspondence, silently. Estimators convert pandas objects to NumPy arrays and ignore the index, so nothing downstream can detect it. The only symptom is a performance figure sitting at chance level.

Correct formulation : "The invariant to preserve is the positional correspondence: row i of X and the value yiy_i describe the same observation. Any filtering or sorting is applied to the source table, before the split."

MISTAKE — Letting the encoding designate the positive class

LabelEncoder sorts the levels lexicographically. With Fraud and Normal, fraud receives the code 0, and the metrics, which default to pos_label=1, then measure the Normal class. Nothing warns, and every figure in the report is computed on the wrong event.

Correct formulation : "The positive class is declared explicitly, for example y = (df["status"] == "Fraud").astype(int), and verified by reading classes_ after fitting."

MISTAKE — Confusing the positive class with the favorable class

"Positive" means "presence of the condition being looked for". The term is descriptive, not evaluative. The positive class of a screening model is Affected, that of a churn model is Churned, that of a default model is Default. None of the three is good news.

Correct formulation : "The positive class is the event the model exists to detect, independently of whether that event is desirable."

MISTAKE — Quoting a metric without naming the class

Precision, recall, F1-score and PR-AUC are asymmetric. On the same model, on the same predictions, the F1-score is 0.194 for Fraud and 0.992 for Normal. A figure quoted without its reference class cannot be checked, challenged or compared against anything.

Correct formulation : "The F1-score on the Fraud class, at threshold 0.02, is 0.194", and never "the model's F1-score is 0.194".

MISTAKE — Identifying the minority class with the positive class by definition

The positive class results from a methodological decision; the minority class results from a count. Their coincidence is an empirical fact about most use cases, not a definition. After rebalancing, the positive class is no longer the minority in the training set and remains the minority in the test set.

Correct formulation : "The positive class is designated, the minority class is observed."

MISTAKE — Reading a ROC AUC below 0.5 as a badly performing model

An AUC clearly under 0.5 means the ranking is being scored in the wrong direction: the column of predict_proba that was read corresponds to the other class, or the label encoding is inverted relative to the metric's pos_label. A model that had genuinely learned the phenomenon backwards would be, by the same token, an excellent model once its output was negated — which is not what is happening.

Correct formulation : "The AUC of 0.122 indicates an inverted score column. classes_ reads ['Fraud' 'Normal'], so column 1 carries the probability of Normal; scored on the correct column, the AUC is 0.878."

MISTAKE — Treating the labels as the phenomenon

The ground truth is a measurement produced by a fallible chain: detection, annotation, recording. Undetected frauds carry the label Normal and are indistinguishable from genuine normal transactions. A measured recall of 0.63 is recall against the frauds that were found, not against the frauds that occurred.

Correct formulation : "The model recovers 63% of the frauds identified by the existing detection chain. Its performance against undetected fraud cannot be measured with these labels."


8. Summary

NOTATION
    X   matrix of explanatory variables   shape (n, p)   UPPERCASE
    y   target vector                     shape (n,)     lowercase
    Origin: the typography of linear algebra
    matrix = uppercase, vector = lowercase, scalar = italic
    Dimensional coherence: (XT X)-1 XT y is defined only if X is a
    matrix and y a vector of length n.

THE ALIGNMENT INVARIANT
    Necessary : X.shape[0] == y.shape[0]     checked by the library
    Sufficient: row i of X and y_i describe the SAME observation
                checked by no library at all
    Loud   dropna, filtering       -> ValueError, benign
    Silent sort_values, sample,    -> no exception, AUC at chance
           one-sided reset_index
    Rule: reorder the source table, before the split. Never after.

LABEL AND CLASS
    Label: the value of the target variable for one observation.
        classification -> class label, categorical
        regression     -> target value, numeric
    Class: a level of the target; the classes form a partition,
        mutually exclusive and collectively exhaustive.
    Ground truth is a measurement, not the phenomenon.
    Inter-annotator agreement bounds attainable performance;
    Cohen's kappa (1960) corrects raw agreement for chance.

ENCODING
    LabelEncoder sorts lexicographically: "Fraud" -> 0, "Normal" -> 1.
    Declare the encoding instead:
        y = (df["status"] == "Fraud").astype(int)
    predict_proba columns follow estimator.classes_ .
    An AUC well below 0.5 means an inverted column, not a bad model.

POSITIVE CLASS
    DESIGNATED, not observed.
    Criterion: the event you are trying to detect,
               the one that triggers an action.
    Positive = presence of the phenomenon, never "favorable".
    It determines precision, recall, F1, PR-AUC and the reading
    of the confusion matrix.
    Invariant to inversion: accuracy, balanced accuracy, log loss,
    Matthews coefficient, Cohen's kappa.

MAJORITY / MINORITY CLASS
    OBSERVED by counting: value_counts()
    Prevalence = n_positive / n      Ratio = n_maj / n_min
    The ABSOLUTE count of the minority class matters as much
    as the ratio. Report both.
    DummyClassifier(strategy="most_frequent") is the mandatory
    baseline: on this dataset it scores 0.9971 accuracy and
    catches 0 frauds out of 70.

Summary statement

X is uppercase because it is a matrix of shape (n, p) and y is lowercase because it is a vector of shape (n,), their correspondence being positional and never guaranteed by the equality of their lengths alone; a label is the value of the target variable recorded for one observation and a class is a level of that variable; and the positive class is the one the organization is trying to detect — a methodological designation, distinct from the empirical observation of minority status, on which the reading of the confusion matrix and every asymmetric metric depend.


Associated quizzes

  • 007.1-quiz-x-y-notation.md
  • 007.2-quiz-shapes-and-alignment.md
  • 007.3-quiz-labels-and-ground-truth.md
  • 007.4-quiz-classes-and-encoding.md
  • 007.5-quiz-positive-class.md
  • 007.6-quiz-majority-minority-classes.md

Next chapter : 008.0-algorithm-vs-model.md