==============================================================================================
RAHUL'S ML BLOG -- notes on machine learning, worked out by hand est. 2026
==============================================================================================
home | about | archive | glossary | contact
----------------------------------------------------------------------------------------------
CHAPTER 3 . SORTING INTO BINS . PART 1 OF 4
Sorting Into Bins: The S-Curve, the Four-Box Table, and Why Accuracy Lies
============================================================================================
A machine that predicts a price answers with a sliding number.
A house worth 2.8.
A car that does 18 miles to the gallon.
This post answers with something heavier -- a verdict.
Picture the room.
A doctor has photographed the cells from a breast lump.
The doctor measured thirty things about them: size, texture, smoothness.
The question on the table is not "how malignant?"
It is the one that keeps people up at night: malignant, OR NOT?
Two bins.
One answer.
And a wrong answer that can cost a life.
The machine still brews a sliding number inside.
But that number can no longer just be read aloud.
It has to be bent into a chance.
Then that chance has to be chopped into a label.
And from here on every choice we make is haunted by which kind of mistake
we can least afford to make.
You want to predict a chance -- a number that has to live between 0 and 1. Reach
for a straight line, chance = 0.2 + 0.1 * age, and feed it a 50-year-old:
chance = 0.2 + 0.1*50 = 5.2 = 520%
0 |--------------------------1|.........5.2
floor ceiling the line sailed clean through it
A straight stick climbs forever; it has no ceiling and no floor. But a chance
physically cannot pass 1 or fall below 0. The shapes do not match.
So bend the line into an S. The sigmoid takes any dial sum, however large, and
squashes it into the 0-to-1 band -- the higher the input climbs, the harder the
curve flattens against the ceiling:
1 | .--------------
| .-''
0 |__..--''_____________________
big negative -> ~0 big positive -> ~1
Now grade a yes/no machine where 99 of every 100 lumps are harmless. Here is a
doctor who never looks at the scan and calls everyone "well":
100 lumps: 99 well + 1 sick
he says: "WELL" to all 100
-------------------------------
right on 99, wrong on 1 -> accuracy = 99% (the board hands him a medal)
Ninety-nine percent -- and the one sick patient walks out the door to die. The
single number that mattered, did you catch the sick one, is buried under a heap
of easy correct "well"s.
So stop scoring on accuracy and ask recall: of the truly sick, what fraction did
you actually catch?
sick caught / sick total = 0 / 1 = 0% (and now he is fired)
When one bin dwarfs the other, accuracy flatters a machine that catches nothing.
Recall refuses to.
FROM NUMBER TO BIN
a number machine: ----------*------> answer = 2.83
a bin machine: [ B = well ] [ M = sick ] answer = ONE box
A dial is a weight the machine multiplies each measured column by.
The dial-adding machine multiplies each column by its dial, then adds the results.
That gives one sliding number.
But a sliding number is not a bin.
So the machine needs two more things.
It needs a SQUASH that bends any number into the range 0 to 1, so it reads as a chance.
Then it needs a CUTOFF that chops that chance into a label.
columns -> *dials -> add -> SQUASH -> chance 0-1 -> CUTOFF -> [B] or [M]
THE SQUASH CURVE
The squash is a function that takes any number and returns a chance between 0 and 1.
Its graph has an S-shape, so call it the S-curve.
Its standard name is the sigmoid; sigmoid just means S-shaped.
Any real number goes in.
A chance between 0 and 1 comes out.
The formula, writing sig for the squash:
sig(z) = 1 / (1 + e^-z)
Here z is the dial sum: z = b0 + x1*b1 + x2*b2 + ... + x30*b30.
Each x is a measured column.
Each b is its dial, the weight that column is multiplied by.
b0 is a plain starting number with no column, called the offset.
The letter e is a fixed math constant, about 2.718.
Where does that formula come from? It is not astrology -- derive it from ODDS.
Odds = chance for, divided by chance against:
chance 0.5 -> odds = 0.5 / 0.5 = 1 (even money)
chance 0.8 -> odds = 0.8 / 0.2 = 4 (4 to 1 on)
chance 0.9 -> odds = 0.9 / 0.1 = 9 (9 to 1 on)
Now the one modelling CHOICE in this whole machine: let each +1 step of the dial
sum z MULTIPLY the odds by a fixed amount (call it e, about 2.718 -- any fixed
multiplier gives the same S-shape; e is picked because its slopes come out clean).
So odds = e^z. At z = 0 the odds are e^0 = 1, even money. Each +1 of z nearly
triples the odds; each -1 cuts them to about a third.
Walk it back to a chance. If odds = (chance) / (1 - chance) = e^z, solve by pencil:
chance = e^z x (1 - chance)
chance = e^z - e^z x chance
chance + e^z x chance = e^z
chance x (1 + e^z) = e^z
chance = e^z / (1 + e^z)
Divide top and bottom by e^z:
chance = 1 / (1/e^z + 1) = 1 / (1 + e^-z) <- the squash curve
So the S-curve is nothing but "dial sum sets the odds, odds walked back to a chance."
Check the ends.
z huge -> e^-z tiny -> chance near 1.
z hugely negative -> e^-z huge -> chance near 0.
z = 0 -> 1 / (1 + 1) = 0.5, even money.
A concrete 4-person worked example, by pencil. Suppose we have only ONE column
(bmi) and 4 people:
person bmi (x) dial sum z = b0 + b1*x
------------------------------------------
A 0.04 1.2 - 30 * 0.04 = 1.2 - 1.2 = 0.0
B 0.06 1.2 - 30 * 0.06 = 1.2 - 1.8 = -0.6
C 0.12 1.2 - 30 * 0.12 = 1.2 - 3.6 = -2.4
D 0.18 1.2 - 30 * 0.18 = 1.2 - 5.4 = -4.2
Now squash each z through the curve:
person z sig(z) = 1 / (1 + e^-z) chance
--------------------------------------------------------------
A 0.0 1 / (1 + e^0) = 1 / (1+1) = 0.500
B -0.6 1 / (1 + e^0.6) = 1 / (1+1.822) = 0.354
C -2.4 1 / (1 + e^2.4) = 1 / (1+11.023) = 0.083
D -4.2 1 / (1 + e^4.2) = 1 / (1+66.686) = 0.015
big positive z -> sig(z) near 1 -> "sick"
big negative z -> sig(z) near 0 -> "well"
z = 0 -> sig(0) = 0.5 -> fence
To compute e^0.6 by pencil: look up a table of exponentials, or note
e^0.5 ~= 1.649 and e^0.1 ~= 1.105, so e^0.6 = e^0.5 * e^0.1
~= 1.649 * 1.105 ~= 1.822. That is close enough for the picture.
A fifth person E walks in (made-up): bmi 0.10, same dials (b0 = 1.2, b1 = -30).
Compute the dial sum z and the squashed chance on your slate. Hint:
e^1.8 = e^0.6 * e^0.6 * e^0.6, and e^0.6 ~= 1.822 from the note above.
check your slate: z = 1.2 - 30 * 0.10 = 1.2 - 3.0 = -1.8; e^1.8 ~= 1.822 *
1.822 * 1.822 ~= 3.320 * 1.822 ~= 6.05; sig(-1.8) = 1 / (1 + 6.05) = 1 / 7.05
~= 0.142. Below the 0.5 cutoff, E is called well -- about a 14-in-100 chance
of sick.
A cutoff is the chance value where you switch the label.
The default cutoff is 0.5.
If the chance is 0.5 or more, call sick.
If the chance is below 0.5, call well.
Patient A has chance 0.500, exactly at the fence -- essentially 50/50.
Patients B, C, D have chances 0.354, 0.083, 0.015, all below 0.5, so all called well.
The dial sum z before the squash has its own name: the LOG-ODDS.
Log-odds means the natural log of the odds.
Odds here = chance-sick divided by chance-well.
Natural log means log to base e, the inverse of e^z.
Writing beta for the whole list of dials, and x for the list of columns:
log( P(sick|x) / P(well|x) ) = b0 + x^T beta
P(sick|x) reads "chance of sick given the columns x"; P(well|x) is chance of well.
x^T beta is shorthand for x1*b1 + x2*b2 + ... + x30*b30, the dial sum without b0.
Each dial beta_j is the change in log-odds per one-unit move in its column,
with the other 29 columns held fixed.
For person A the dial sum was z = 0, so log-odds = 0.
Log-odds 0 means odds = 1, which means P(sick) = P(well) = 0.5.
SETTING THE DIALS: WHAT THE MACHINE MINIMISES
A machine that predicts a number scores itself by squared leftovers.
A leftover is the gap between the truth and the guess.
Squared leftovers means: take each gap, square it, add them up; this is called MSE,
the mean squared error.
That ruler fits a number answer.
But here the answer is a label, 0 or 1, not a number.
So squared distance is the wrong ruler.
Instead the machine maximises the likelihood of the observed labels.
Likelihood means how probable the real labels are under the machine's chances.
Maximising that is the same as minimising a CROSS-ENTROPY leftover.
Cross-entropy is a score that punishes a confident wrong chance very hard.
Writing y for the true label (0 or 1) and n for the number of lumps:
L(beta) = -(1/n) sum [ y*log sig(z) + (1-y)*log(1-sig(z)) ]
Take a lump that is truly sick, so y = 1.
If the machine outputs chance near 1, then log(near 1) is near 0 -- a small leftover.
If it outputs chance near 0 on that sick lump, then log of near-zero drops to bottomless.
That is an enormous leftover.
So the leftover punishes confidence in the wrong direction.
There is no formula that hands you the best dials in one shot.
So the dials are found by rolling downhill through the gradient.
The gradient is the list of slopes telling each dial which way lowers the leftover.
gradient of L w.r.t. beta: (1/n) X^T ( sig(X*beta) - y )
each step: beta <- beta - a * gradient (a = step size)
Here X is the whole table of columns, and a is the step size, how far each move goes.
Each move nudges every dial a little against its slope, lowering the leftover.
Count the clerk-strokes for ONE downhill move.
Per lump: 30 multiplies and 30 adds for the dial sum z.
Then one squash and one subtraction.
Then 30 multiplies and 30 adds for the gradient -- call it 125 strokes.
The Wisconsin sheet holds 569 lumps.
So one move costs 569 x 125 = 71,125 strokes.
And a thousand moves run near 1000 x 71,125 = 71 million strokes.
That is why the clerks, not you, hold the pencils here.
In code this is two lines.
One line makes an empty machine.
One line shows it the study pile so it rolls the dials downhill.
That code waits at the end of the post.
>> NOTE: WHY SCALED INPUTS?
The downhill move changes every dial by the step size times its column's values.
A column measured in thousands (area ~= 1000) takes dial moves 1000x larger
than one measured in hundredths (smoothness ~= 0.1).
So the machine lurches around the large column's dial.
And it barely moves the small column's dial.
Put every column on the same ruler first -- mean 0, spread 1.
Then the move sizes become comparable.
Scaling means: shift each column so its average is 0, then divide so its spread is 1.
The ruler touches the measured columns X ONLY -- never the answer y.
y is already a clean 0 or 1.
y is the key you grade against, not a measurement that needs a ruler.
Run a mean-and-spread ruler over the 0/1 column and you smear the answer key
into fractions.
So the scaler is fit and applied to X_train and X_test alone.
ENCODING THE LABELS
A machine eats numbers.
But the sheet holds letters: M for malignant, B for benign.
Malignant means the sick lump; benign means the well lump.
The translation is a shelf: M->1, B->0.
The bin you are HUNTING gets the 1.
Here the hunted bin is the sick lump.
That is because every score in this post counts the sick detections.
Recall counts them, precision counts them, F1 counts them; those scores are defined below.
The one-line shelf that does this M->1, B->0 swap is in the code at the end,
along with a warning about running it twice.
FOUR-BOX TABLE
IN HAND so far: a machine that adds 30 dialed columns into a number z.
It squashes z into a chance with sig(z) = 1 / (1 + e^-z); z = 0 gives 0.5, even money.
It chops that chance at a cutoff of 0.5: chance 0.5 or more is sick, below 0.5 is well.
Labels sit on the shelf M->1, B->0, so 1 means sick and 0 means well.
This section adds a table counting the four ways a verdict can land.
Every exam lump lands in exactly one of four boxes, set by truth and guess:
guess SICK(1) guess WELL(0)
truth SICK(1) CAUGHT | MISSED <- deadly if large
truth WELL(0) ALARM | CLEAR <- wasteful if large
rows = the truth (sick / well)
columns = the guess (shouted sick / said well)
each box = a plain count
A concrete 4-person example, by pencil:
exam pile has 4 lumps: 2 sick (truth=1), 2 well (truth=0)
lump truth machine chance cutoff 0.5 -> guess
----------------------------------------------------
1 sick 0.92 >= 0.5 -> sick (1) CAUGHT
2 sick 0.63 >= 0.5 -> sick (1) CAUGHT
3 well 0.78 >= 0.5 -> sick (1) ALARM
4 well 0.11 < 0.5 -> well (0) CLEAR
Four-box table with counts:
guess SICK guess WELL
truth SICK 2 (CAUGHT) 0 (MISSED) <- 2 truly sick
truth WELL 1 (ALARM) 1 (CLEAR) <- 2 truly well
Name the four boxes once.
CAUGHT = truly sick and guessed sick (the win).
MISSED = truly sick but guessed well (the deadly miss).
ALARM = truly well but guessed sick (the false alarm).
CLEAR = truly well and guessed well (the other win).
Now four scores, each defined the first time, then computed from these counts.
Accuracy = share of all guesses that were right = (CAUGHT + CLEAR) / everyone.
accuracy = (2 + 1) / 4 = 3/4 = 0.75
Recall = of the truly sick, the share the machine caught = CAUGHT / (CAUGHT + MISSED).
recall = 2 / (2 + 0) = 2/2 = 1.00 (caught every sick person)
Precision = of those it called sick, the share that really were = CAUGHT / (CAUGHT + ALARM).
precision = 2 / (2 + 1) = 2/3 = 0.67 (but 1 healthy person was alarmed)
F1 = one score that blends precision and recall = 2*precision*recall / (precision + recall).
F1 = 2*0.67*1.00 / (0.67 + 1.00) = 1.34 / 1.67 = 0.80
The machine caught 2 of 2 sick people, so recall = 1.00.
But it raised 1 false alarm, so precision = 0.67.
The four-box table shows EXACTLY which mistake happened.
Look at the bottom-left box, ALARM, and you see the cost in plain light.
It is not buried inside a single percentage.
A bigger exam pile (made-up): 10 lumps land as CAUGHT 4, MISSED 1, ALARM 2,
CLEAR 3. Work all four scores on your slate before reading on.
check your slate: everyone = 4 + 1 + 2 + 3 = 10; accuracy = (4 + 3) / 10 =
7/10 = 0.70; recall = 4 / (4 + 1) = 4/5 = 0.80; precision = 4 / (4 + 2) =
4/6 ~= 0.67; F1 = 2 * 0.80 * 0.67 / (0.80 + 0.67) = 1.072 / 1.47 ~= 0.73.
This machine let 1 sick lump walk out the door -- recall 0.80 says so first.
CAUGHT is the ideal: a sick lump correctly flagged.
MISSED is the catastrophe: cancer goes home untreated.
ALARM is the waste: a healthy person gets a scare and an unnecessary biopsy.
CLEAR is the other good outcome: a well lump correctly left alone.
Drawing this table from the guesses is in the code at the end.
So is the easy-to-miss gotcha about which way round sklearn wants the arguments.
FOUR SCORES, FOUR ANGLES
IN HAND so far: a four-box table.
On the 4-lump pile the boxes held CAUGHT 2, MISSED 0, ALARM 1, CLEAR 1.
Check the total: 2 + 0 + 1 + 1 = 4, every lump counted once.
This section adds four scores, each reading a different slice of those boxes.
No single score summarises what a bin-sorter actually does.
So use four scores, each reading a different slice of the four-box table.
Each is repeated here so you need not scroll back:
accuracy = (CAUGHT + CLEAR) / everyone <- share of all guesses that were right
recall = CAUGHT / (CAUGHT + MISSED) <- top row: share of sick caught
precision = CAUGHT / (CAUGHT + ALARM) <- left col: when we cry sick, real sick?
F1 = 2 * precision * recall / (precision + recall) <- blend of the two
The standard names for the four boxes use TP, FP, FN, TN.
TP = true positive = CAUGHT = truly sick, guessed sick.
FP = false positive = ALARM = truly well, guessed sick.
FN = false negative = MISSED = truly sick, guessed well.
TN = true negative = CLEAR = truly well, guessed well.
P stands for precision and R for recall in the F1 line below; n is the number of lumps.
Score Formula (counts) Punishes
--------- --------------------- ----------------------------
accuracy (TP + TN) / n any wrong answer equally
recall TP / (TP + FN) MISSED (deadly)
precision TP / (TP + FP) ALARM (wasteful)
F1 2PR / (P + R) either score being low
All four come straight from the four-box counts.
The code that reads them off is at the end of the post.
WHY ACCURACY ALONE LIES
Now the most important paragraph in the post.
Recall: accuracy = share of all guesses that were right = (CAUGHT + CLEAR) / everyone.
The Wisconsin sheet has roughly 63% benign lumps, that is 63% well.
So picture a machine that learns nothing whatsoever -- no dials, no squash.
It just shouts one lazy constant: "well, well, well".
It is right on every well lump, which is 63% of the pile.
So it walks away with 63% accuracy.
On paper it looks like it is passing.
In the exam room it is a catastrophe.
It never once catches a sick person.
Accuracy counted every box the same.
So a MISSED cancer weighed exactly as much as a needless ALARM.
But those two mistakes are not equal.
And pretending they are is how people get hurt.
fool machine: predict B (well) for everything
accuracy = 0.63 <- looks decent
recall = 0.00 <- catches nobody (CAUGHT = 0, so 0 / total sick = 0)
precision = N/A <- never shouts sick, no CAUGHT and no ALARM, so 0/0, undefined
IN CANCER SCREENING, RECALL IS THE NORTH STAR
Recall = of the truly sick, the share the machine caught = CAUGHT / (CAUGHT + MISSED).
Missing a sick person is the catastrophe.
You can tolerate some ALARM, since extra biopsies cost money, not lives.
So the first score to check is recall.
It tells you how large a share of the sick lumps the machine catches.
Accuracy tells you nothing about which mistake you are making.
A fair few wrong pictures cost me real time here; each one is a thing that looks
right. (A few names below belong to later posts, each defined right where it
appears.)
Wrap the scaler's input in extra brackets and it sees one dimension too many:
scaler.fit([X_train]) -> 3D -> "Found array with dim 3. expected <= 2"
scaler.fit(X_train) -> 2D -> fine (rows by columns)
The scaler wants a 2D table, not a 2D table tucked inside a list. Drop the [ ].
Run the M->1, B->0 cell once and it works; run it a second time and it poisons
everything:
first run: 'M' 'B' 'M' --map--> 1 0 1 (good)
second run: 1 0 1 --map--> NaN (no 'M'/'B' left to find)
NaN ("not a number") then spreads into every later step, with no recovery. Restart
the kernel and run top to bottom once.
The machine set its dials on SCALED numbers, so feed it raw numbers at predict time
and the dials misfire:
fit on: X_scaled (avg 0, spread 1)
predict on: X_test WRONG -- a different space
predict on: X_test_scaled RIGHT -- the space the dials learned in
sklearn always wants truth first, guess second -- swap them and the whole grid
flips:
confusion_matrix(y_test, y_pred) RIGHT (truth, guess)
confusion_matrix(y_pred, y_test) WRONG -- transposes the grid
Same order as accuracy_score and recall_score: truth, then guess.
predict_proba returns two columns; grab the wrong one and "sick" scores low:
[:, 0] = P(well) <- the wrong column
[:, 1] = P(sick) <- the one you want
Take column 0 and sick people score LOW, well people HIGH -- the ROC curve (true-
positive rate against false-positive rate as the cutoff slides) drops below the
diagonal, and its area, AUC, sinks under the 0.5 coin-flip line.
C small reads like "small = mild leash". It is the opposite, because C = 1/lambda,
and lambda is the strength pulling dials toward 0:
C = 0.1 -> lambda = 10 a heavy squeeze, tight leash
C = 1000 -> lambda = 0.001 nearly free
Small C means big lambda means a hard pull toward zero. Small C is strong, not mild.
"I scaled the columns, so the dials are already controlled" -- two different jobs:
scaling: fixes the INPUTS -- puts columns on one ruler
L2: fixes the DIALS -- adds sum of squared dials to the leftover, each pulled toward 0
Even on scaled inputs a dial can balloon if the machine over-trusts one column. You
need both.
Last, feed plain predict to the ROC curve and it collapses, because predict hands
back only 0/1:
predict -> {0, 1} -> ROC has 2 dots -> useless
predict_proba -> raw chance -> the full cutoff sweep
The ROC slides a cutoff across the raw chances; hard 0/1 labels leave nothing to
slide.
F1: WHY THE HARMONIC MEAN
Recall the two scores F1 blends.
Precision = of those it called sick, the share that really were.
Recall = of the truly sick, the share it caught.
F1 is the harmonic mean of precision and recall, not the plain average.
The plain average, the arithmetic mean, adds two numbers and halves them.
The harmonic mean is pulled down hard by whichever value is smaller.
Take recall = 0.98 but precision = 0.10.
The plain average is (0.10 + 0.98) / 2 = 0.54, which looks okay.
The harmonic mean F1 = 0.18, which correctly punishes the terrible precision.
So F1 collapses toward zero when either score is near zero.
You cannot paper over one bad score with one great score.
F1 = 2 * P * R / (P + R)
example: P=0.10, R=0.98
arithmetic mean = (0.10 + 0.98) / 2 = 0.54
harmonic mean = 2 * 0.10 * 0.98 / (0.10 + 0.98) = 0.18 <- honest
Nothing above needed a computer -- only pencils, clerks, and patience.
This last section is for the day you meet a computer: the same work, spoken in Python.
First the S-curve and four-box scores from the worked examples, hard-coded, one person per
line so you watch every number arrive:
import math
S-CURVE FOR 4 PEOPLE: B0 = 1.2, B1 = -30 (THE ONE-BMI-COLUMN EXAMPLE)
bmi_A, bmi_B, bmi_C, bmi_D = 0.04, 0.06, 0.12, 0.18
z_A = 1.2 + (-30)*bmi_A # 0.0
z_B = 1.2 + (-30)*bmi_B # -0.6
z_C = 1.2 + (-30)*bmi_C # -2.4
z_D = 1.2 + (-30)*bmi_D # -4.2
sig_A = 1/(1 + math.exp(-z_A)) # 0.500 (z=0 -> even money)
sig_B = 1/(1 + math.exp(-z_B)) # 0.354
sig_C = 1/(1 + math.exp(-z_C)) # 0.083
sig_D = 1/(1 + math.exp(-z_D)) # 0.015
print(round(sig_A,3), round(sig_B,3), round(sig_C,3), round(sig_D,3)) # 0.5 0.354 0.083 0.015
FOUR-BOX COUNTS FROM THE 4-LUMP EXAMPLE (CHANCES 0.92, 0.63, 0.78, 0.11; CUTOFF 0.5)
CAUGHT, MISSED, ALARM, CLEAR = 2, 0, 1, 1
accuracy = (CAUGHT + CLEAR) / (CAUGHT + MISSED + ALARM + CLEAR) # 0.75
recall = CAUGHT / (CAUGHT + MISSED) # 1.0 (caught all truly sick)
precision = CAUGHT / (CAUGHT + ALARM) # 0.667 (one false alarm)
F1 = 2*precision*recall / (precision + recall) # 0.800
print(round(accuracy,2), round(recall,2), round(precision,3), round(F1,3)) # 0.75 1.0 0.667 0.8
HARMONIC MEAN PUNISHES ONE BAD SCORE; ARITHMETIC MEAN HIDES IT
P, R = 0.10, 0.98
arithmetic_mean = (P + R) / 2 # 0.54 (looks ok)
harmonic_mean = 2*P*R / (P + R) # 0.181 (exposes the terrible precision)
print(round(arithmetic_mean,2), round(harmonic_mean,3)) # 0.54 0.181
Four people, four boxes, four scores -- all visible. The toolbox block below uses the
same steps on the full Wisconsin sheet:
Start to finish, the work has four short parts.
First, turn the M/B letters into 1/0.
Then fit the S-curve machine on scaled inputs.
Then build the four-box table.
Then read the four scores off it.
>> NEW TO PYTHON? Each named once:
df['col'] -- pull one named column out of a table (a pandas DataFrame)
.map({...}) -- swap each value using a {from: to} shelf (a dict)
df.drop(columns=) -- a copy of the table with some columns removed
First, encode the answer column. The bin you are HUNTING gets the 1:
df['diagnosis'] = df['diagnosis'].map({'M': 1, 'B': 0})
X = df.drop(columns='diagnosis') # 30 measured columns
y = df['diagnosis'] # 0 or 1
RUN THE MAP CELL ONLY ONCE
Run the encoding cell a second time and the column already holds 0 and 1.
The map searches for 'M' and 'B', finds nothing, and returns all NaN.
NaN means "not a number", an empty marker.
Every later step breaks on NaN.
Fix: restart the kernel and run top-to-bottom once.
There is no shortcut after the NaN has landed.
Fit the machine on scaled inputs:
log_reg = LogisticRegression(random_state=42)
log_reg.fit(X_train_scaled, y_train)
Build the four-box table from truth and guess:
conf_matrix = confusion_matrix(y_test, y_pred) # truth first, guess second
sns.heatmap(conf_matrix, annot=True, fmt='d')
grid shape: [[ CLEAR ALARM ] <- truth well (row 0)
[ MISSED CAUGHT ]] <- truth sick (row 1)
TRUTH FIRST, GUESS SECOND
sklearn's confusion_matrix, accuracy_score, recall_score, and precision_score
all want the truth first and the guess second.
Swapping them flips rows and columns.
So it misassigns every count.
Read the four scores off it:
y_pred = log_reg.predict(X_test_scaled)
accuracy = accuracy_score(y_test, y_pred)
recall = recall_score(y_test, y_pred)
precision = precision_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)
Plain term used above Standard label
----------------------------------- ------------------------------------------
sort into bins / bin-sorter classification / classifier
S-curve yes/no guesser logistic regression
squash curve sigmoid / logistic function
dial sum (z) log-odds / logit
cross-entropy leftover binary cross-entropy / log-loss
CAUGHT true positive (TP)
ALARM false positive (FP)
MISSED false negative (FN)
CLEAR true negative (TN)
four-box table confusion matrix
recall sensitivity / true positive rate (TPR)
put on one shared ruler standardisation / StandardScaler
----------------------------------------------------------------------------------------------
IN THIS CHAPTER (Chapter 3 -- Sorting Into Bins):
Part 1 (this post) .
Part 2 -- The Trade Curve .
Part 3 -- Leash and Cloud .
Part 4 -- Picking Settings, Skewed Piles
<- Back to all posts
----------------------------------------------------------------------------------------------
home . source on GitHub
==============================================================================================