The Model That Catches Nothing
Why accuracy is the wrong metric for anything rare
Week 1 ended with a warning: an accuracy figure with no baseline beside it means nothing. This week takes that apart properly, because on real data accuracy is not merely unhelpful, it is actively misleading.
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
# A fraud-shaped problem: one case in twenty five is real, which is
# roughly what payment fraud looks like and nothing like a textbook set.
X, y = make_classification(n_samples=4000, n_features=12, n_informative=5,
n_redundant=2, weights=[0.96, 0.04],
flip_y=0.02, class_sep=0.9, random_state=0)
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3,
stratify=y, random_state=0)
print('%d training rows, %d held out' % (len(Xtr), len(Xte)))
print('fraud is %.1f%% of the data' % (100 * y.mean()))
fraud is 4.7% of the data
The number that looks like success
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
# A fraud-shaped problem: one case in twenty five is real, which is
# roughly what payment fraud looks like and nothing like a textbook set.
X, y = make_classification(n_samples=4000, n_features=12, n_informative=5,
n_redundant=2, weights=[0.96, 0.04],
flip_y=0.02, class_sep=0.9, random_state=0)
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3,
stratify=y, random_state=0)
print('%d training rows, %d held out' % (len(Xtr), len(Xte)))
print('fraud is %.1f%% of the data' % (100 * y.mean()))
from sklearn.dummy import DummyClassifier
from sklearn.linear_model import LogisticRegression
lazy = DummyClassifier(strategy='most_frequent').fit(Xtr, ytr)
model = LogisticRegression(max_iter=2000).fit(Xtr, ytr)
print('%-34s %10s' % ('', 'accuracy'))
print('%-34s %10.4f' % ('predict "not fraud" every time',
lazy.score(Xte, yte)))
print('%-34s %10.4f' % ('an actual model', model.score(Xte, yte)))
fraud is 4.7% of the data
accuracy
predict "not fraud" every time 0.9525
an actual model 0.9650
A model that has never once said the word fraud is 96 percent accurate. Put that figure in a slide and it reads as a triumph. It catches nothing, it would save nobody any money, and it is a single line of code that ignores its input entirely.
This is the most common dishonest number in the field
It is rarely dishonest on purpose. Accuracy is the default metric in every library, it is the one non-specialists ask for, and on imbalanced data it is dominated entirely by the majority class. Any problem where the interesting event is rare, and most valuable problems are, has this property.
What to look at instead
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
# A fraud-shaped problem: one case in twenty five is real, which is
# roughly what payment fraud looks like and nothing like a textbook set.
X, y = make_classification(n_samples=4000, n_features=12, n_informative=5,
n_redundant=2, weights=[0.96, 0.04],
flip_y=0.02, class_sep=0.9, random_state=0)
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3,
stratify=y, random_state=0)
print('%d training rows, %d held out' % (len(Xtr), len(Xte)))
print('fraud is %.1f%% of the data' % (100 * y.mean()))
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix
model = LogisticRegression(max_iter=2000).fit(Xtr, ytr)
tn, fp, fn, tp = confusion_matrix(yte, model.predict(Xte)).ravel()
print('%-28s %6d' % ('fraud caught', tp))
print('%-28s %6d' % ('fraud missed', fn))
print('%-28s %6d' % ('honest flagged as fraud', fp))
print('%-28s %6d' % ('honest left alone', tn))
print()
print('of the %d real fraud cases, it found %d' % (tp + fn, tp))
fraud is 4.7% of the data
fraud caught 18
fraud missed 39
honest flagged as fraud 3
honest left alone 1140
of the 57 real fraud cases, it found 18